星点匠心建站 星点匠心建站XD JIANXIN STUDIO
400-883-9527 免费定制方案
ARTICLE DETAIL

资讯详情

深耕新锐建站与运营推广的一线实战洞察。

Masonry 链式约束布局:原理、实操与冲突排查

Masonry 链式约束布局:原理、实操与冲突排查 纯 frame 布局的年代改一个间距要翻三个方法加一个控件要重算一整屏坐标到了约束时代NSLayoutConstraint那一长串constraintWithItem:attribute:relatedBy:toItem:attribute:multiplier:constant:又让人怀疑人生。Masonry 框架就是在这种撕裂感里被大量 Objective-C 工程选中的它把 Auto Layout 的约束声明包装成一段可读的链式调用make.left.equalTo(view).offset(16)这种写法几乎不用查文档就能看懂意图。这个库解决的核心问题很具体——让约束式布局的代码写起来像句子改起来像改数字。适合谁看正在维护 Objective-C 老工程、被NSLayoutConstraint折磨过的开发者从 frame 时代过渡过来、想系统搞懂约束优先级和更新机制的中间层同学以及准备自研一套布局 DSL、想先弄清成熟方案怎么设计的人。往下我会拆它的设计思路、内部机制、实操代码、复杂场景处理以及那些文档里不会写的冲突排查经验。1. 布局这件事为什么值得单独单独聊Masonry 的定位与思路1.1 从手写 frame 到约束式布局的演进代价手写 frame 的时代布局代码本质是一堆坐标算术。父视图宽度变了、字体变了、文案从两行变三行所有下游的CGRect都得跟着重算layoutSubviews里塞满减法和除法稍微复杂一点的卡片列表一个文件里能出现十几个魔法数字。它的优点是快——没有约束求解器纯 CPU 算术缺点是脆——需求一动整块代码推倒重来。Auto Layout 把位置从结果变成了关系你声明的是控件之间的相对关系求解器负责算出最终坐标。这是思维方式的迁移代价是声明成本陡增。一个宽度等于父视图一半、居中、距离顶部 12 点的视图原生写法要三行NSLayoutConstraint还得手动设置translatesAutoresizingMaskIntoConstraints NO。四条边加一个尺寸代码量直接翻倍而且几乎不可读——你没法一眼看出这段代码描述的是什么形状。Masonry 站在中间。它没有绕开 Auto Layout也没有自己实现一套布局引擎只是把约束声明这件事的书写成本压到了接近自然语言的程度。这个定位很关键它解决的是表达效率问题不是性能问题。理解这一点后面所有关于性能、关于要不要换方案的讨论才有基准。1.2 链式 DSL 到底省掉了什么把两种写法并排放差异一目了然。原生方案里每个约束都是独立对象firstItem、firstAttribute、relation、secondItem、secondAttribute、multiplier、constant七个参数按位置传写错了编译器未必报错运行时才崩。Masonry 把设置谁、什么关系、相对谁、差多少拆成四段连续的调用// 原生 NSLayoutConstraint *c [NSLayoutConstraint constraintWithItem:self.card attribute:NSLayoutAttributeLeft relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeLeft multiplier:1.0 constant:16]; [self.view addConstraint:c]; // Masonry [self.card mas_makeConstraints:^(MASConstraintMaker *make) { make.left.equalTo(self.view).offset(16); }];省掉的不只是行数。链式调用的每一段都是一个语义单元make.left说明改的是哪条边.equalTo(self.view)说明参照物.offset(16)说明偏移量。中间任何一段想调整直接改对应的调用就行不用重新数参数位置。这种一段一语义的结构直接决定了后期维护时定位问题的速度。还有一个隐性的好处mas_makeConstraints:的 block 结束时统一安装约束这给了框架一个批量处理的窗口。同一个 block 里的约束可以互相校验、可以整体设置优先级甚至可以等到 block 结束再决定装到哪个视图上。原生写法做不到这一点每条约束都是立即生效的。1.3 同名不同物先分清你说的是哪个 Masonry这里必须插一句因为搜索Masonry会撞出两个完全不同的东西。一个是 Objective-C 生态里的 Auto Layout 封装库跑在苹果设备上另一个是前端的瀑布流布局库跑在浏览器里通过绝对定位把不等高的卡片塞进列里。两者只是名字相同解决的问题、技术栈、API 形态毫无关系。判断方法很简单看上下文有没有UIView、UIViewController、mas_makeConstraints这些词。如果有是前者如果看到的是new Masonry(.grid, {itemSelector: .item})这种 JS 调用那就是后者。这篇聊的是前者也就是移动端约束布局那一套。为什么要专门提这一嘴因为我在技术社区里见过太多次提问者把两边的资料混着看然后困惑于为什么我的 Masonry 没有 offset 方法。1.4 影响范围什么项目适合用它先说结论Objective-C 为主的、界面复杂度中等偏上的、需要长期维护的工程收益最大。纯 Swift 工程没必要为了它桥接一层Swift 生态里有同类思路的链式库用起来更顺手界面极其简单的工具类应用手写 frame 反而更省事约束求解的开销和调试成本不划算而对帧率极致敏感的滚动列表约束布局本身就要谨慎使用这跟用不用 Masonry 没关系。还有一类容易被忽略的场景老工程渐进式改造。一个纯 frame 的历史项目不可能推倒重来但新页面可以逐步引入约束布局。Masonry 在这里的优势是侵入性低——它不改动 UIKit 的任何行为只是给UIView加了一批 category 方法没有全局 hook没有 method swizzling除了极少量的辅助逻辑新旧代码可以共存于同一个界面。这种可以局部采用的特性在真实工程里的价值往往比 API 优雅更高。2. 内部机制拆解一条链式语句怎么变成约束2.1 三个核心对象与调用时序搞懂 Masonry 只需要抓住三个角色MASConstraintMaker、MASConstraint实际装配时是MASViewConstraint、以及UIView上的一批 category 属性。你写下[view mas_makeConstraints:^(MASConstraintMaker *make) {...}]的时候框架先创建一个 maker把当前 view 塞进去。block 里每一次make.left、make.width这样的调用本质是在 maker 内部创建一条约束的描述对象并把它追加到一个数组里。注意这时候约束还没真正生效——数组只是在收集。block 执行完maker 调用install遍历数组把每条描述翻译成真正的NSLayoutConstraint找到两个视图最近的公共父视图addConstraint:上去。整个过程是先收集、后装配这就是为什么你在 block 里可以随意引用其他视图的mas_left框架会等到装配阶段才去解析。UIView那一侧的 category 属性同样值得说明。view.mas_left、view.mas_centerY这些不是存储属性是每次访问时现算出来的MASViewAttribute对象内部只记录哪个视图的哪个NSLayoutAttribute。所以它很轻不需要缓存也不会有生命周期问题。理解这一点你就不会纠结为什么我不能把mas_left存起来复用——能存但没必要。2.2 equalTo 与 mas_equalTo 的装箱差异这是新手最容易混的一对 API也是我见过最多约束没报错但布局不对的源头。equalTo接收的是id框架会根据传入对象的类型做不同处理传视图取它的对应属性传NSNumber当作常量。但如果你直接传一个CGFloat比如.equalTo(16.0)16.0是基本类型装箱成NSNumber的过程在id参数上会出问题——有些场景下能过有些场景下值会丢。mas_equalTo是一个宏内部调用了装箱函数专门处理CGFloat、NSInteger、CGPoint、CGSize、UIEdgeInsets这些值类型。它会先把值包成NSValue或NSNumber再交给equalTo处理。我的习惯很简单也推荐你照做凡是传数值、尺寸、内边距的一律用mas_equalTo凡是传视图或MASViewAttribute的用equalTo。这条规则不用记原理照着执行就不会踩坑。make.width.mas_equalTo(120); // 数值 make.height.mas_equalTo(CGSizeMake(100, 44).height); // 表达式 make.left.equalTo(self.view); // 视图 make.right.equalTo(otherView.mas_left).offset(-8); // 属性 make.edges.equalTo(self.view).insets(UIEdgeInsetsMake(8, 8, 8, 8));顺带说个细节make.edges.equalTo(self.view).insets(...)这一句等价于四条边各写一遍。insets的值是分别作用到 top、left、bottom、right 上的UIEdgeInsetsMake(8, 8, 8, 8)就是四周各缩进 8。这个复合写法在容器视图内边距的场景里特别省事。2.3 make、update、remake 的语义边界这三个方法长得像行为差别很大用错了会出现约束越积越多或者改了没生效两类典型问题。mas_makeConstraints:是新增。对同一个视图反复调用它约束会叠加而不是替换。两条width 100和width 120同时存在就是冲突日志里会看到求解器随机断掉一条表现是布局时对时错。mas_updateConstraints:是更新。框架会遍历该视图上已有的约束按 attribute 匹配找到对应的那条只改constant。它不会新增约束也不会删除约束。所以它只能改已存在的那条约束的数值改不了关系也没法把一个lessThanOrEqualTo变成equalTo。mas_remakeConstraints:是先清空再重建。所有该视图上由 Masonry 创建的约束先移除再按新 block 重新装配。它最彻底也最费——每条约束都要重新计算、重新装载。选择的判断标准很清晰运行时只改数值比如高度随内容变化、间距随状态切换用update布局结构本身变了比如横竖屏切换、控件增减用remake初始化第一次用make。注意mas_updateConstraints匹配的是 attribute不是你以为的那条约束。同一个视图如果有两条都关于height的约束比如一个有优先级、一个没有更新时匹配到哪一条是有歧义的。这种场景老老实实用remake或者给约束加key做精确查找。3. 落地实操把 Masonry 接进工程并跑通第一个界面3.1 依赖引入与编译配置主流方式还是依赖管理工具。Podfile 里加一行platform :ios, 11.0 target DemoApp do pod Masonry, ~ 1.1.0 end版本号建议锁小版本~ 1.1.0表示允许 1.1.x不允许跳到 1.2。布局库这种底层依赖一旦大版本升级改了内部实现排查成本极高锁住更稳妥。工程里的引入方式有两种。省事的是在全局前缀头文件里加一行#import Masonry.h所有文件直接用不用逐个 import。缺点是编译依赖变重前缀头改动会触发大面积重编。我个人偏向另一种用到的文件单独 import配合模块化改造的工程甚至可以做成私有库的子模块。这点取舍看你工程的编译时间敏不敏感。Swift 与 Objective-C 混编的工程要注意桥接头。Masonry 是纯 Objective-C 实现Swift 侧要用必须先通过 bridging header 暴露出来而且链式 API 在 Swift 里的调用体验会打折扣mas_makeConstraints的 block 语法转换后不算优雅。这种工程我一般建议新代码走原生 Anchor老代码继续用 Masonry别硬统一。3.2 第一个可运行的布局登录页卡片拿一个真实感强的场景练手一个居中的登录卡片宽度是屏幕宽度的 80%高度自适应内容内部从上到下依次是标题、两个输入框、一个按钮间距统一 16。- (void)setupUI { UIView *card [[UIView alloc] init]; card.backgroundColor [UIColor whiteColor]; card.layer.cornerRadius 12; [self.view addSubview:card]; self.cardView card; // 卡片本身水平居中宽度占屏幕 80%垂直居中 [card mas_makeConstraints:^(MASConstraintMaker *make) { make.center.equalTo(self.view); make.width.equalTo(self.view).multipliedBy(0.8); }]; UILabel *title [[UILabel alloc] init]; title.text 账号登录; [card addSubview:title]; self.titleLabel title; UITextField *account [[UITextField alloc] init]; account.placeholder 手机号; account.borderStyle UITextBorderStyleRoundedRect; [card addSubview:account]; UITextField *password [[UITextField alloc] init]; password.placeholder 密码; password.secureTextEntry YES; password.borderStyle UITextBorderStyleRoundedRect; [card addSubview:password]; UIButton *login [UIButton buttonWithType:UIButtonTypeSystem]; [login setTitle:登 录 forState:UIControlStateNormal]; [card addSubview:login]; self.loginButton login; // 标题 [title mas_makeConstraints:^(MASConstraintMaker *make) { make.top.equalTo(card).offset(24); make.left.equalTo(card).offset(20); make.right.equalTo(card).offset(-20); make.height.mas_equalTo(28); }]; // 账号输入框 [account mas_makeConstraints:^(MASConstraintMaker *make) { make.top.equalTo(title.mas_bottom).offset(16); make.left.right.equalTo(title); make.height.mas_equalTo(44); }]; // 密码输入框 [password mas_makeConstraints:^(MASConstraintMaker *make) { make.top.equalTo(account.mas_bottom).offset(16); make.left.right.equalTo(title); make.height.mas_equalTo(44); }]; // 按钮顶部对齐密码框底部底部留 24卡片高度就由内容链推出来 [login mas_makeConstraints:^(MASConstraintMaker *make) { make.top.equalTo(password.mas_bottom).offset(24); make.left.right.equalTo(title); make.height.mas_equalTo(48); make.bottom.equalTo(card).offset(-24); }]; }这段代码值得逐行看的有两处。一是make.width.equalTo(self.view).multipliedBy(0.8)。这里用的是比例而不是固定值屏幕宽度变化时卡片自动跟着缩放不用写任何适配代码。multipliedBy作用在参照视图的对应属性上最终生成的约束是card.width view.width * 0.8。二是最后那条make.bottom.equalTo(card).offset(-24)。这条约束的作用是把卡片的高度撑出来——卡片自己没写 height它的高度由内部的约束链决定title 顶部 24 28 16 44 16 44 24 48 24。这条链闭合了卡片就有了确定高度。这是约束布局里最常用的技巧后面讲自适应高度还会用到。3.3 常用写法速查与设计稿换算实际开发里高频出现的写法就那么十几种整理成表比翻文档快得多。需求写法四边贴父视图make.edges.equalTo(superview)四边带内边距make.edges.equalTo(superview).insets(UIEdgeInsetsMake(8, 8, 8, 8))水平垂直居中make.center.equalTo(superview)宽度占父视图一半make.width.equalTo(superview).multipliedBy(0.5)宽高比 4:3make.height.equalTo(view.mas_width).multipliedBy(0.75)尺寸固定make.size.mas_equalTo(CGSizeMake(100, 44))顶对齐 左对齐make.top.left.equalTo(superview).offset(16)右对齐带负偏移make.right.equalTo(superview).offset(-16)高于等于某个值make.height.mas_greaterThanOrEqualTo(44)优先级设定make.height.mas_equalTo(44).priorityHigh()设计稿换算这块很多团队的做法是写个宏#define SCREEN_WIDTH ([UIScreen mainScreen].bounds.size.width) #define MAS_SCALE(w) ((w) * SCREEN_WIDTH / 375.0)用的时候make.height.mas_equalTo(MAS_SCALE(44))。这套方案能用但我不太推荐大范围使用。原因有两个一是屏幕宽度变化时所有值线性缩放会导致在小屏上文字被挤、在大屏上留白过多视觉比例失真二是这个宏在横屏、分屏模式下会算出奇怪的值。更稳的思路是分级处理间距用固定值加少量断点调整宽度高度用比例约束字号用系统动态字体。真正需要等比的场景比如按设计稿精确还原的营销页用multipliedBy在约束层面表达比例比在数值层面做缩放要可靠得多。举个例子一张宽度占父视图 100%、高度按 16:9 的图片[imageView mas_makeConstraints:^(MASConstraintMaker *make) { make.left.right.equalTo(self.view); make.top.equalTo(self.view); make.height.equalTo(imageView.mas_width).multipliedBy(9.0 / 16.0); }];这条约束在任何屏幕宽度下都成立不需要计算任何像素值。4. 复杂场景等分、滚动视图、自适应高度与动画4.1 等分布局与间距分配的计算四个按钮横向等分是列表页、筛选栏、底部工具栏的标配。手写 frame 的算法是(总宽 - 总间距) / 4用约束表达则不需要算具体数值只需要表达彼此等宽、依次排开、首尾贴边。NSMutableArrayUIView * *items [NSMutableArray array]; for (NSInteger i 0; i 4; i) { UIView *item [[UIView alloc] init]; item.backgroundColor [UIColor lightGrayColor]; [self.containerView addSubview:item]; [items addObject:item]; } [items mas_distributeViewsAlongAxis:MASAxisTypeHorizontal withFixedSpacing:8 leadSpacing:16 tailSpacing:16]; [items mas_makeConstraints:^(MASConstraintMaker *make) { make.top.equalTo(self.containerView).offset(12); make.height.mas_equalTo(60); }];mas_distributeViewsAlongAxis:是 Masonry 提供的一个批量工具方法一行搞定轴向分布内部会自动处理首尾间距和中间等距。它的便利性很高但有个前提所有视图必须已经加入同一个父视图而且数量不能为 0否则会崩。这个检查最好自己加一层尤其是数组来自网络数据的时候。如果不想用这个工具方法手动写循环也完全可行逻辑是第一个贴左边后面的贴前一个的右边最后一个贴右边UIView *lastItem nil; for (NSInteger i 0; i 4; i) { UIView *item items[i]; [item mas_makeConstraints:^(MASConstraintMaker *make) { make.top.bottom.equalTo(self.containerView); if (lastItem) { make.left.equalTo(lastItem.mas_right).offset(8); make.width.equalTo(lastItem); } else { make.left.equalTo(self.containerView).offset(16); } }]; lastItem item; } [lastItem mas_makeConstraints:^(MASConstraintMaker *make) { make.right.equalTo(self.containerView).offset(-16); }];这个循环的关键在于make.width.equalTo(lastItem)。每新增一个视图就把它和上一个绑成等宽最终所有视图的宽度会被求解器统一成一个值——(容器宽 - 16 - 16 - 8 * 3) / 4。你不用自己算但你需要理解为什么它是这个值因为总宽被第一个的左边距、最后的右边距、以及中间三个 8 的间距和四个等宽的视图完全瓜分了约束系统求解的唯一解就是这个。注意循环里如果给每个视图都写了make.right.equalTo(...)会和等宽链条产生冲突。等分布局的核心是只约束一端另一端靠链条推导两边都锚死就会过约束。4.2 UIScrollView 与自适应高度的正确姿势滚动视图是约束布局最容易翻车的地方因为我见过太多人在UIScrollView上直接给子视图写约束然后发现滚不动。原因在于滚动视图的内容尺寸不是由它自己决定的而是由内部的子视图约束推出来的而子视图的约束必须相对于一个内容容器不能相对于滚动视图本身的可视区域。标准做法是加一层contentView让它和滚动视图的四边对齐并固定宽度竖向滚动场景UIScrollView *scrollView [[UIScrollView alloc] init]; [self.view addSubview:scrollView]; [scrollView mas_makeConstraints:^(MASConstraintMaker *make) { make.edges.equalTo(self.view); }]; UIView *contentView [[UIView alloc] init]; [scrollView addSubview:contentView]; [contentView mas_makeConstraints:^(MASConstraintMaker *make) { make.edges.equalTo(scrollView); make.width.equalTo(scrollView); // 竖向滚动宽度锁死高度自由 }];make.edges.equalTo(scrollView)这一句把 contentView 的四边和 scrollView 的可视区域绑在一起宽度那条则保证 contentView 不会被横向撑开。之后所有子视图都往 contentView 上加最后用最后一个视图的 bottom 约束到 contentView 的mas_bottom高度链就闭合了滚动区域自动算出来。横向滚动的轮播场景反过来make.height.equalTo(scrollView)锁高度宽度自由展开。这点别搞混搞混的表现是能滚动但是内容被压扁。至于UITableViewCell的自适应高度思路完全一致只是容器换成了contentView。要点有三个cell 内部从上到下形成一条完整的垂直约束链最后一个视图的 bottom 必须约束到cell.contentView.mas_bottom不要给 cell 设置固定高度不要在heightForRowAtIndexPath里返回一个具体数值让它走自动维度。Masonry 在这个场景里最大的价值是链条清晰二十行的约束代码能一眼看出上下依赖关系出了问题容易定位是哪一环断了。4.3 约束驱动的动画与优先级抢占约束布局的动画跟 frame 动画思路不同你不能直接改frame要改约束的constant然后在动画块里触发重新布局。- (void)toggleCardHeight:(BOOL)expanded { [self.cardView mas_updateConstraints:^(MASConstraintMaker *make) { make.height.mas_equalTo(expanded ? 240 : 120); }]; [UIView animateWithDuration:0.28 delay:0 options:UIViewAnimationOptionCurveEaseInOut animations:^{ [self.view layoutIfNeeded]; } completion:nil]; }顺序很重要先改约束再进动画块调layoutIfNeeded。如果反过来你会看到界面跳一下就结束没有过渡——因为约束变化和布局刷新不在同一次提交里。这个坑我踩过不止一次排查了半小时才意识到是调用顺序的问题。优先级在动画里也有用处。比如一个展开收起的面板收起时希望高度优先服从最大值展开时希望高度服从内容。可以用两条约束、不同优先级来表达// 内容撑开优先级稍低 [panel mas_makeConstraints:^(MASConstraintMaker *make) { make.height.mas_equalTo(contentHeight).priority(750); }]; // 最大高度优先级高 [panel mas_makeConstraints:^(MASConstraintMaker *make) { make.height.mas_lessThanOrEqualTo(300).priorityRequired(); }];优先级 750 是NSLayoutPriorityDefaultHigh低于必须满足的 1000。当内容高度超过 300 时高优先级的小于等于 300会赢面板被限制在 300内容不足 300 时750 那条生效高度跟着内容走。用优先级而不是 if-else 判断好处是切换状态时只改constant不用重装约束动画也更顺。5. 冲突排查与工程规范5.1 读懂冲突日志的三个线索约束冲突的日志看着吓人其实结构很固定抓住三个线索就能快速定位。第一个线索是Unable to simultaneously satisfy constraints后面那段约束列表。每条约束都会打印出类名、内存地址和数学表达式比如MASLayoutConstraint:0x7f8a UIView:0x7f90.height 44。MASLayoutConstraint这个类名说明约束是 Masonry 创建的不是系统或手写的。表达式直接告诉你哪条约束、什么关系、什么值。第二个线索是Will attempt to recover by breaking constraint后面那条。求解器在冲突时只能随机断一条来恢复断掉的往往是数值较小的那条不一定是你想断的。看到这行不要急着改它要找的是另一条和它矛盾的约束。第三个线索是地址。同一个视图的所有约束地址前缀通常相近因为它们是同批创建的。如果日志里出现了好几条关于同一个视图的height基本可以确定是mas_makeConstraints被调了两次。实操中最有效的排查手段是给约束加key[self.cardView mas_makeConstraints:^(MASConstraintMaker *make) { make.height.mas_equalTo(120).key(card.height); }];加了 key 之后日志和调试工具里都能看到这个标识比对着内存地址猜靠谱得多。团队里推广一下这条习惯排查效率提升非常明显。5.2 高频问题速查表下面这些是我在真实项目里反复遇到的按出现频率排了序。现象常见原因处理方式视图完全不显示没加addSubview或约束相对的是未入层级的视图检查加入父视图的顺序先 add 再 make布局改了没反应只调了mas_makeConstraints新增约束旧约束还在改用mas_updateConstraints或remake约束越积越多在layoutSubviews或cellForRow里无脑 make约束只在初始化时建一次之后只改数值滚动视图滚不动子视图约束相对 scrollView 而非 contentView加 contentView闭合高度链cell 高度都变成默认值垂直链没闭合缺 bottom 约束最后一项约束到contentView.mas_bottom动画瞬间完成先layoutIfNeeded后改约束改成先改约束再进动画块刷新偶发崩溃在 view 未加入父视图时引用了父视图的 mas 属性保证引用方已在层级中宽高比约束不生效用equalTo传了浮点比例值值类型一律用mas_equalTo约束冲突日志刷屏同一个属性有多条等值约束给约束加 key统一管理这张表可以贴在团队的知识库里新同学入职时看一遍能少踩一半的坑。5.3 团队协作下的约束管理约定一个界面两三个人写约束代码很快就会变成没人敢动的雷区。几个约定能让它一直保持可维护。约束代码集中在setupConstraints方法里不要散落在业务逻辑中间。视图的生命周期里约束只应该建立一次其他时候都是修改。把建立约束抽成一个独立方法修改约束的地方就只剩下明确的几处改界面时定位成本很低。给关键约束加 key尤其是那些后面会被update的。key 的命名用视图名.属性的格式比如card.height、button.leading团队里统一一下格式就行。避免在分类里用mas_之外的前缀。Masonry 自己占用了mas_前缀你在UIView上扩展方法时用别的规范前缀比如demo_能避免将来升级库版本时的方法名冲突。约束的数量要有个意识。一个 cell 里超过 30 条约束滚动性能就会开始受影响。这不是 Masonry 的问题是 Auto Layout 本身的求解成本。列表页的 cell 布局能简化的尽量简化能用edges就别写四条边能用size就别写宽高两条能用center就别写centerX加centerY。少一条约束就少一次求解。还有一个容易被忽略的点给约束加上注释说明这条为什么存在。比如这里必须留 8 而不是 16因为设计上需要和图标基线对齐——这种信息在半年后只有写的人记得而写的人半年后也忘了。我个人在几个项目里推这套约定的体会是最有效的不是规范本身而是把约束代码从业务代码里物理隔离出来。视图控制器里只保留布局相关的几行剩下全在子视图的初始化里闭环。这样改业务逻辑的时候不会误伤布局改布局的时候也不用在几百行代码里翻找。踩过几次改了个判断条件结果整个界面错位的坑之后你也会认同这一点。
返回列表