一探 mas_updateConstraints 究竟

原文 : 与佳期的个人博客(gonghonglou.com)

Masonry 的链式编程对 iOS UI 添加约束简直好用的不得了,想必在使用上大家也都早已烂熟于心。只是对我来讲很早之前就有个更新约束的问题要好好搞搞清楚,就是题目里的 mas_updateConstraints: 方法。在 View 初始化时会添加一系列约束控制布局,而随时更改约束来移动位置也是日常需求,但其中关于这个方法的某些设计并不是直观想象的那样,现在,终于有时间写篇博客一探究竟。

当前环境:Xcode 10.1、Simulator iPhone XS 12.1、Masonry 1.1.0

探索

首先来看一段布局代码:topView 和 bottomView 上下排列、左右对齐,topView 和 rightView 左右排列、上下对齐。

[self.topView mas_makeConstraints:^(MASConstraintMaker *make) {
   make.centerY.equalTo(self.view);
   make.left.equalTo(self.view).offset(50);
   make.size.mas_equalTo(CGSizeMake(100, 30));
}];
[self.bottomView mas_makeConstraints:^(MASConstraintMaker *make) {
   make.top.equalTo(self.topView.mas_bottom).offset(50);
   make.left.equalTo(self.topView);
   make.size.mas_equalTo(self.topView);
}];
[self.rightView mas_makeConstraints:^(MASConstraintMaker *make) {
   make.top.equalTo(self.topView);
   make.right.equalTo(self.view).offset(-50);
   make.size.mas_equalTo(self.topView);
}];

从12行可以看出 rightView 的 top 依赖于 topView,现在有个需求是将 rightView 与 bottomView 顶部对齐。如图:


updateConstraints.png

记得我第一次遇到这种问题的时候想当然的将 rightView 的 top 依赖于 bottomView 了:

[self.rightView mas_updateConstraints:^(MASConstraintMaker *make) {
    make.top.equalTo(self.bottomView);
}];

结果是报错、崩溃还是页面错落我倒是忘记了,总之是不能满足需求。当前在 Xcode 10.1、Simulator iPhone XS 12.1 环境下我试了下是页面错落。所以我将代码改成了这样:

[self.rightView mas_updateConstraints:^(MASConstraintMaker *make) {
   make.top.equalTo(self.topView).offset(80);
}];

这里当然可以使用 mas_remakeConstraints: 方法,只不过该方法的意思是清空所有约束重新添加(下文会有源码体现),相比 mas_updateConstraints: 性能低了不少,况且项目开发中往往全部约束散落在各处,难免会有遗漏或者约束错乱的情况。所以我的做法是在 mas_updateConstraints: 方法里不更改约束依赖的对象,而通过计算出一个合适的偏移量来更改 offset 值。所以得出了一个结论:

Masonry 的 mas_updateConstraints: 方法不能更改约束依赖的对象,可以通过计算偏移量来更新布局。

但这究竟是为什么呢?让我们来看一下 Masonry 的 mas_updateConstraints: 方法都做了些什么工作:

- (NSArray *)mas_updateConstraints:(void(^)(MASConstraintMaker *))block {
    self.translatesAutoresizingMaskIntoConstraints = NO;
    MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self];
    constraintMaker.updateExisting = YES;
    block(constraintMaker);
    return [constraintMaker install];
}

constraintMaker 的 updateExisting 属性设置为 YES 之后执行了 install 方法:

- (NSArray *)install {
    if (self.removeExisting) {
        NSArray *installedConstraints = [MASViewConstraint installedConstraintsForView:self.view];
        for (MASConstraint *constraint in installedConstraints) {
            [constraint uninstall];
        }
    }
    NSArray *constraints = self.constraints.copy;
    for (MASConstraint *constraint in constraints) {
        constraint.updateExisting = self.updateExisting;
        [constraint install];
    }
    [self.constraints removeAllObjects];
    return constraints;
}

这段代码 mas_remakeConstraints: 方法也会调用,只不过是将 removeExisting 属性设为 YES,第2行的 if 判断正是上文提到的清空所有约束。后半段代码则是调用 install 方法更新约束,该方法则是调用自 MASConstraint 的子类:MASViewConstraint。下边是关键实现代码

    MASLayoutConstraint *existingConstraint = nil;
    if (self.updateExisting) {
        existingConstraint = [self layoutConstraintSimilarTo:layoutConstraint];
    }
    if (existingConstraint) {
        // just update the constant
        existingConstraint.constant = layoutConstraint.constant;
        self.layoutConstraint = existingConstraint;
    } else {
        [self.installedView addConstraint:layoutConstraint];
        self.layoutConstraint = layoutConstraint;
        [firstLayoutItem.mas_installedConstraints addObject:self];
    }
}

- (MASLayoutConstraint *)layoutConstraintSimilarTo:(MASLayoutConstraint *)layoutConstraint {
    // check if any constraints are the same apart from the only mutable property constant

    // go through constraints in reverse as we do not want to match auto-resizing or interface builder constraints
    // and they are likely to be added first.
    for (NSLayoutConstraint *existingConstraint in self.installedView.constraints.reverseObjectEnumerator) {
        if (![existingConstraint isKindOfClass:MASLayoutConstraint.class]) continue;
        if (existingConstraint.firstItem != layoutConstraint.firstItem) continue;
        if (existingConstraint.secondItem != layoutConstraint.secondItem) continue;
        if (existingConstraint.firstAttribute != layoutConstraint.firstAttribute) continue;
        if (existingConstraint.secondAttribute != layoutConstraint.secondAttribute) continue;
        if (existingConstraint.relation != layoutConstraint.relation) continue;
        if (existingConstraint.multiplier != layoutConstraint.multiplier) continue;
        if (existingConstraint.priority != layoutConstraint.priority) continue;

        return (id)existingConstraint;
    }
    return nil;
}

逻辑很简单,根据 updateExisting,也就是之前的标识更新布局的赋值来寻找已存在的约束,如果约束存在则执行更新操作,如果约束不存在则当成一条新的约束添加给 View。

关键就在于 layoutConstraintSimilarTo: 查找约束时的判断方法,22~29行,竟然是根据这些条件来判断,这就是为什么在 mas_updateConstraints: 方法里更改约束对象后造成页面错乱的原因,因为它找不到这条约束就把它当成一条新的约束添加到 View 上,导致约束冲突。

从这一点出发的话那我们首先想到的解决这个问题的方案就不再是更改偏移量了,而是通过设置约束的优先级来解决约束冲突的问题。

而且,如果遇到 View 出现重复约束时,比如:

make.top.equalTo(self.topView).offset(10);
make.top.equalTo(self.topView).offset(20);

仅仅通过在 mas_updateConstraints: 方法里更改某条约束的偏移量并不能起到精确控制的作用。所以这种情况下设置优先级也许是个更好的解决方案。关于设置优先级值得注意的有:

  • 不特殊设置时约束的优先级默认是 UILayoutPriorityRequired,也就是最高的
  • 优先级的设值范围在 0~1000 之间,超出这个范围则会崩溃:

2019-01-18 13:00:09.449167+0800 MasonryTest[54681:15704474] *** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'It's illegal to set priority:1200. Priorities must be greater than 0 and less or equal to NSLayoutPriorityRequired, which is 1000.000000.'

所以我们应该将想要更新的约束提前设置一个较低的优先级,再在 mas_updateConstraints: 方法里更新约束并对新的约束设置一个高于原来约束的优先级,且低于 1000。例如:

[self.rightView mas_makeConstraints:^(MASConstraintMaker *make) {
   make.top.equalTo(self.topView).priorityLow();
}];
    
[self.rightView mas_updateConstraints:^(MASConstraintMaker *make) {
   make.top.equalTo(self.bottomView).priorityHigh();
   // or
   // make.top.equalTo(self.bottomView).priority(800);
}];

最后,认识几种优先级类型:

// 1000
static const MASLayoutPriority MASLayoutPriorityRequired = UILayoutPriorityRequired;
// 750
static const MASLayoutPriority MASLayoutPriorityDefaultHigh = UILayoutPriorityDefaultHigh;
// 500
static const MASLayoutPriority MASLayoutPriorityDefaultMedium = 500;
// 250
static const MASLayoutPriority MASLayoutPriorityDefaultLow = UILayoutPriorityDefaultLow;
// 50
static const MASLayoutPriority MASLayoutPriorityFittingSizeLevel = UILayoutPriorityFittingSizeLevel;

后记

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,222评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,455评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,720评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,568评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,696评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,879评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,028评论 3 409
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,773评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,220评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,550评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,697评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,360评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,002评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,782评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,010评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,433评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,587评论 2 350

推荐阅读更多精彩内容