Masonry 框架小结

Masonry 框架小结

自从 iOS 6.0 苹果引入自动布局以来,经过不断的发展,其已经成为我们进行 UI 布局的主要技术之一。在 Autolayout 之前,可以使用 Autoresizing 来约束视图的父子关系,但是局限性很大,很多任务无法实现,如其不能约束兄弟视图之间的关系。

相比之下,Autolayout 的功能则强大的多,其核心概念便是参照约束

NSLayoutConstraint

自动布局的关键是创造完整的约束,并将约束添加到正确的视图中。

NSLayoutConstraint 类是对约束的描述,其核心概念可以用一个公式表示,如下:

firstItem.firstAttribute {==,<=,>=} secondItem.secondAttribute * multiplier + constant

对于视图自身大小尺寸的约束而言,是不需要 secondItem 参照物的。不过,如果将此情况视为以自身为参照,那么就可以将约束理解为总是参照其他视图而存在的,即约束是相对而言的。

该类提供了两个类方法用来创建约束实例:

+(instancetype)constraintWithItem:(id)view1 attribute:(NSLayoutAttribute)attr1 relatedBy:(NSLayoutRelation)relation toItem:(nullable id)view2 attribute:(NSLayoutAttribute)attr2 multiplier:(CGFloat)multiplier constant:(CGFloat)c;

+ (NSArray<__kindof NSLayoutConstraint *> *)constraintsWithVisualFormat:(NSString *)format options:(NSLayoutFormatOptions)opts metrics:(nullable NSDictionary<NSString *,id> *)metrics views:(NSDictionary<NSString *, id> *)views;

第一个方法,基本上就是对核心公式的赋值。而第二个方法,则是使用 VFL(Visual Format Language) 字符串来创建约束。

要准确快速的使用约束,需要了解 NSLayoutAttributeNSLayoutRelationUILayoutPriority 的可选值。

对于 NSLayoutAttribute 而言,需要注意的是 NSLayoutAttributeLeftNSLayoutAttributeRight 总是表示视图的左、右两侧,但是 NSLayoutAttributeLeadingNSLayoutAttributeTrailing 却不总是表示视图的左、右两侧,其表示的含义同当地视图排版习惯相关。所以,其表示的含义可能是左、右两侧,也可能是右、左两侧。

NSLayoutAttribute***Margin 表示的参照位置总是同 UIViewlayoutMargins 属性相关。

创建的约束要添加到视图中才会生效,但是需要将 UIViewtranslatesAutoresizingMaskIntoConstraints 属性置为 NO,并且约束相关联的视图必须都添加到视图层级中,而且约束需要添加到相关视图的共同父视图中(将视图本身看作其本身的父视图,即对于有包含关系的相关视图,约束要添加到层级高的视图中,而只涉及自身的约束,则添加到其自身即可)。

约束的使用的确减少了视图的布局和适配工作,但是直接使用 NSLayoutConstraint 类定义约束是十分繁琐的,并且代码会变得十分臃肿,不易理解、查错和维护,所以第三方框架的使用必不可少。

Masonry

Masonry 框架十分易用,支持链式编程,其实现并不复杂,但是设计的十分巧妙。理解 Masonry 中约束的实现过程,可以从约束的核心公式入手。

MASViewAttribute

MASViewAttribute 类,可以理解为对 NSLayoutAttribute 的封装,其除了保存布局时要参照的位置外,还持有了具体的布局对象和参照对象。

@property (nonatomic, assign, readonly) NSLayoutAttribute layoutAttribute;

@property (nonatomic, weak, readonly) MAS_VIEW *view;
@property (nonatomic, weak, readonly) id item;

可见,布局对象是 UIView 对象,而参照对象则未必。并且,两者都是弱引用。

MASLayoutConstraint

MASLayoutConstraint 只是简单继承了 NSLayoutConstraint 类,可以预见,其是该框架中描述约束的类。另外,其定义了一个属性以便于调试,通过该属性,可以得知一个约束是否是通过该框架创建的。

@property (nonatomic, strong) id mas_key;

MASConstraint

MASConstraint 虽然是以 Constraint 结尾,但是其是 NSObject 的子类。

该类的巧妙设计使得 Masonry 框架中的方法可以链式调用,对于有参数的方法,无法使用点语法进行调用,所以在该类中通过返回包含参数的 block 来进行参数的传递。

可以将该类中的方法分为三类:

  1. 返回 block 的方法

    如下两个方法,分别表示对偏移量和约束属性的修改:

    - (MASConstraint * (^)(CGFloat))offset {
        return ^id(CGFloat offset){
            self.offset = offset;
            return self;
        };
    }
    
    - (MASConstraint * (^)(id))equalTo {
        return ^id(id attribute) {
            return self.equalToWithRelation(attribute, NSLayoutRelationEqual);
        };
    }	
    

    如同第一个方法,在返回的 block 中会使用调用时随后的指定参数对变量进行赋值,而对于类似第二个方法的对参照关系属性的修改,同样是在 block 中实现的。而其所使用的方法,在本类中实际是抽象方法,需要子类去具体实现。

  2. 返回本类实例的方法

    诸如 leftrighttopbottom 等方法,实际是返回了一个 MASConstraint 实例对象,从而可以进一步调用上面所说的第一种类型的方法。

    - (MASConstraint *)addConstraintWithLayoutAttribute:(NSLayoutAttribute __unused)layoutAttribute {
        MASMethodNotImplemented();
    }
    
    - (MASConstraint *)left {
        return [self addConstraintWithLayoutAttribute:NSLayoutAttributeLeft];
    }
    

    这些方法,实际都是对 addConstraintWithLayoutAttribute: 方法的调用,而其也是由子类实现的。

  3. 抽象方法

    该类中定义了诸多抽象方法,由子类来实现。

从上面的分析可知,该类的主要功能就是实现链式编程,而具体的约束创建则是由子类实现的。主要关注 addConstraintWithLayoutAttribute:equalToWithRelation 方法。

MASViewConstraint

MASViewConstraint 作为 MASConstraint 的子类,除了继承父类的相关属性外,还定义了 firstViewAttributesecondViewAttribute 两个属性,其实际就是约束相关的两个参照。

除了一个初始化方法外,还提供了一个类方法,用来获取所有通过该框架添加到一个视图中的约束。其是通过运行时函数,为 UIView 对象添加了一个属性 mas_installedConstraints 来保存所有通过该框架添加到该视图中的约束。

+ (NSArray *)installedConstraintsForView:(MAS_VIEW *)view {
    return [view.mas_installedConstraints allObjects];
}

虽然 MASViewConstraint 并不直接描述约束,但是其实际上包含了所有创建约束的条件,并且也仅仅表示一个约束。该类中定义了一个 NSLayoutConstraint 实例来保存该类的实例对象生成的具体约束,并且这个约束生效后,该实例对象会被添加到 mas_installedConstraints 属性中。不过,这个过程并不是自动的,而是需要调用 install 方法。

在 iOS 8.0 后,约束支持 active 属性,可以通过设置该属性的值来取代向视图中添加约束的操作。

if ([self supportsActiveProperty] && self.layoutConstraint) {
    self.layoutConstraint.active = YES;
    [self.firstViewAttribute.view.mas_installedConstraints addObject:self];
    return;
}

但是,self.layoutConstraint 第一次创建,或者不支持 active 时,仍然需要执行下面的操作。

if (!self.firstViewAttribute.isSizeAttribute && !self.secondViewAttribute) {
    secondLayoutItem = self.firstViewAttribute.view.superview;
    secondLayoutAttribute = firstLayoutAttribute;
}

增加一个判断,当不是对视图自身尺寸的约束,而又没有参照时,则默认参照父视图。

接着,使用相关属性创建约束 MASLayoutConstraint 实例。

然后,获取应该添加该约束的视图,如果要求更新与之相同的约束,那么先遍历所有约束,找到与之相同的约束,直接更新即可,否则,将该约束添加到视图中,所以每个约束生效之前不会主动进行查重,即可能存在重复的约束。

该类中重要的是重写的两个方法 addConstraintWithLayoutAttribute:equalToWithRelation

- (MASConstraint *)left {
    return [self addConstraintWithLayoutAttribute:NSLayoutAttributeLeft];
}

- (MASConstraint *)addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute {
    NSAssert(!self.hasLayoutRelation, @"Attributes should be chained before defining the constraint relation");

    return [self.delegate constraint:self addConstraintWithLayoutAttribute:layoutAttribute];
}
- (MASConstraint * (^)(id))equalTo {
    return ^id(id attribute) {
        return self.equalToWithRelation(attribute, NSLayoutRelationEqual);
    };
}	

- (MASConstraint * (^)(id, NSLayoutRelation))equalToWithRelation {
    return ^id(id attribute, NSLayoutRelation relation) {
        if ([attribute isKindOfClass:NSArray.class]) {
            NSAssert(!self.hasLayoutRelation, @"Redefinition of constraint relation");
            NSMutableArray *children = NSMutableArray.new;
            for (id attr in attribute) {
                MASViewConstraint *viewConstraint = [self copy];
                viewConstraint.layoutRelation = relation;
                viewConstraint.secondViewAttribute = attr;
                [children addObject:viewConstraint];
            }
            MASCompositeConstraint *compositeConstraint = [[MASCompositeConstraint alloc] initWithChildren:children];
            compositeConstraint.delegate = self.delegate;
            [self.delegate constraint:self shouldBeReplacedWithConstraint:compositeConstraint];
            return compositeConstraint;
        } else {
            NSAssert(!self.hasLayoutRelation || self.layoutRelation == relation && [attribute isKindOfClass:NSValue.class], @"Redefinition of constraint relation");
            self.layoutRelation = relation;
            self.secondViewAttribute = attribute;
            return self;
        }
    };
}

方法 equalToWithRelation 是没有参数的,但是 equalTo 方法返回的 block 中调用该方法时,似乎是传递了两个参数,这实际是同样的道理,参数实际是传递给 equalToWithRelation 函数返回的 block 的。

如果参数 attribute 是数组,即同时要添加多个约束,那么便创建一个 MASCompositeConstraint 实例来保存这些约束的相关属性。并且,这里将代理同样赋值给该实例。

从重写的 addConstraintWithLayoutAttribute: 可知,该代理实现了新增约束的操作,十分重要,其究竟是哪个类实现的呢!MASCompositeConstraint 作为 MASViewConstraint 的集合和代理,是否实现了新增约束的操作呢。

MASCompositeConstraint

MASCompositeConstraintMASConstraint 的子类,由一组 MASViewConstraint 初始化得到。其对父类抽象方法的实现,实际就是对其所持有的每个 MASViewConstraint 实例执行相应方法。即,该类可以对一组约束执行相同的操作。

- (MASConstraint *)addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute {
    [self constraint:self addConstraintWithLayoutAttribute:layoutAttribute];
    return self;
}

- (MASConstraint *)constraint:(MASConstraint __unused *)constraint addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute {
    id<MASConstraintDelegate> strongDelegate = self.delegate;
    MASConstraint *newConstraint = [strongDelegate constraint:self addConstraintWithLayoutAttribute:layoutAttribute];
    newConstraint.delegate = self;
    [self.childConstraints addObject:newConstraint];
    return newConstraint;
}

其实现的代理函数,依然只是将消息传递给了原本的代理对象。那么,这个真正的代理是谁,又执行了什么操作呢。

MASConstraintMaker

MASConstraintMaker 的属性来看,似乎同 MASConstraint 类很是相同,但它是 NSObject 的子类,和 MASConstraint 并没有继承关系。

类似的该类中有如下方法:

- (MASConstraint *)addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute {
    return [self constraint:nil addConstraintWithLayoutAttribute:layoutAttribute];
}

- (MASConstraint *)left {
    return [self addConstraintWithLayoutAttribute:NSLayoutAttributeLeft];
}

MASViewConstraint 中不同的是,这里的 addConstraintWithLayoutAttribute: 方法直接调用了自身的 constraint:addConstraintWithLayoutAttribute: 方法,而不是代理的相应方法。

这方法是 MASConstraintDelegate 代理声明的,且当前类实现如下:

- (MASConstraint *)constraint:(MASConstraint *)constraint addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute {
    MASViewAttribute *viewAttribute = [[MASViewAttribute alloc] initWithView:self.view layoutAttribute:layoutAttribute];
    MASViewConstraint *newConstraint = [[MASViewConstraint alloc] initWithFirstViewAttribute:viewAttribute];
    if ([constraint isKindOfClass:MASViewConstraint.class]) {
        //replace with composite constraint
        NSArray *children = @[constraint, newConstraint];
        MASCompositeConstraint *compositeConstraint = [[MASCompositeConstraint alloc] initWithChildren:children];
        compositeConstraint.delegate = self;
        [self constraint:constraint shouldBeReplacedWithConstraint:compositeConstraint];
        return compositeConstraint;
    }
    if (!constraint) {
        newConstraint.delegate = self;
        [self.constraints addObject:newConstraint];
    }
    return newConstraint;
}

- (void)constraint:(MASConstraint *)constraint shouldBeReplacedWithConstraint:(MASConstraint *)replacementConstraint {
    NSUInteger index = [self.constraints indexOfObject:constraint];
    NSAssert(index != NSNotFound, @"Could not find constraint %@", constraint);
    [self.constraints replaceObjectAtIndex:index withObject:replacementConstraint];
}

可知,其根据自身关联的 view 和参数提供的位置属性创建一个 MASViewConstraint 实例对象,并且如果该方法的执行是由存在的 MASViewConstraint 实例引发的(即 constraint 参数存在),那么将两者合为一个 MASCompositeConstraint 实例对象,并替代引发该操作的实例。

最后,将得到的实例保存到 self.constraints 中。

该类同样定义了 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;
}

但是,这里会先根据 self.removeExisting 属性来判断是否要将关联的 view 中约束清空。

而且属性 self.updateExisting 同样是判断是否更新已存在约束的依据,这里更新的只是 NSLayoutConstraintconstant 属性,实际如果其他属性不同,则不会被认为是同一个约束。

并且,该方法会清空 self.constraints 中的成员,表示所有约束创建完毕。

那么,我们现在可以直接创建 MASConstraint 子类对象,并调用 install 方法来创建约束,也可以使用 MASConstraintMaker 来创建约束,那么那种方式更好呢。

实际上,如果直接使用 MASConstraint 来创建约束是无法使用链式编程的,因为其返回的 block 执行时,实际是向代理传递了消息,而此时代理并没有设置。

View+MASAdditions.h

Masonry 框架为 UIView 创建了分类,方便约束的创建。理解上面的约束创建过程,再来看这几个方法,则一目了然了。

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

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

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

添加、更新、重置约束,都是根据 MASConstraintMaker 中的属性来控制的。

[self.button mas_makeConstraints:^(MASConstraintMaker *make) {
    make.left.top.equalTo(@20);
}];

在 block 中,make.left 返回的是 MASConstraint 实例,之后调用 top 方法,实际是调用了 delegate 的 constraint:addConstraintWithLayoutAttribute: 方法,而该代理实际就是 make 对象,然后返回了一个 MASCompositeConstraint 实例对象,调用 equalTo 方法,进而调用了如下方法:

- (MASConstraint * (^)(id, NSLayoutRelation))equalToWithRelation {
    return ^id(id attr, NSLayoutRelation relation) {
        for (MASConstraint *constraint in self.childConstraints.copy) {
            constraint.equalToWithRelation(attr, relation);
        }
        return self;
    };
}

这样,便实现了对多个约束设置相同偏移量的操作。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值