Masonry思路解析

当前iOS最流行的布局方式就是使用autoLayout,本文就时下最流行的Masonry进行分析,解剖其实现思路。
我们都知道UIKit自带的布局代码超级复杂,代码繁多不够简洁。

    UIView *view = [[UIView alloc]init];
    [self.view addSubview:view];
    view.translatesAutoresizingMaskIntoConstraints = NO;
    [[NSLayoutConstraint constraintWithItem:view attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeTop multiplier:1 constant:0] setActive:YES];
    [[NSLayoutConstraint constraintWithItem:view attribute:NSLayoutAttributeLeft relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeTop multiplier:1 constant:0] setActive:YES];
    [[NSLayoutConstraint constraintWithItem:view attribute:NSLayoutAttributeRight relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeTop multiplier:1 constant:0] setActive:YES];
    [[NSLayoutConstraint constraintWithItem:view attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeTop multiplier:1 constant:0] setActive:YES];

怎样才能实现简单的autoLayout实现方式呢?
Masonry引入的链式响应方式。如

// 通过一行或很少的代码即可完成autolayout,Masonry其实就是UIKit中NSLayoutConstraint的封装
view.top.left.right.equalTo(self.view.mas_top).offset(10)

咱们一步步实现自己的Masonry。

NSLayoutConstraint简单解析

由于是封装现有的UIKit的NSLayoutConstraint,所有我们要分析下UIKit自带的布局约束方法有什么特征。


/**
 约束方法

 @param view1 约束左边的view1
 @param attr1 view1需要约束的属性
 @param relation 关系描述
 @param view2 约束有边的view2
 @param attr2 view2的布局属性
 @param multiplier 比例关系
 @param c 偏移量
 @return 返回约束模型
 */
+(instancetype)constraintWithItem:(id)view1 attribute:(NSLayoutAttribute)attr1 relatedBy:(NSLayoutRelation)relation toItem:(nullable id)view2 attribute:(NSLayoutAttribute)attr2 multiplier:(CGFloat)multiplier constant:(CGFloat)c;

一个正常的约束的关系公式为:view1.attr1 = view2.attr2 * multiplier + c

一、MAAutoLayout(实现"view1.attr1")

我们需要给UIView添加一个布局管理属性 ma_layout(MAAutoLayout类型),方便之后布局管理。
MAAutoLayout管理自己的属性,需要知道view1和attr1,所以MAAutoLayout可以设计为

@interface MAAutoLayout : NSObject

- (nonnull instancetype)initWithView:(UIView * _Nonnull)view;

// 基本操作,依据NSLayoutAttribute创建所有的方法,用户链式响应的第一步
@property (nonatomic, strong, readonly) MAAutoLayoutMaker * _Nonnull left;
@property (nonatomic, strong, readonly) MAAutoLayoutMaker * _Nonnull top;
#####省略 right, bottom等,详细请看GitHub上的demo #####
// 激活
- (void)active;
// 取消
- (void)deactivate;
@end

此时我们就可以使用view.ma_layout.top 来替代公式中的view1.top。

二、MAAutoLayoutMaker(实现"= id * multiplier + c")

.equalTo(self.view)需要在MAAutoLayoutMaker中创建有关关系(NSLayoutRelation)的属性。

@interface MAAutoLayoutMaker : NSObject

@property (nullable, nonatomic,strong, readonly) NSLayoutConstraint *layoutConstraint;

- (nonnull instancetype)initWithFirstItem:(nonnull id)firstItem firstAttribute:(NSLayoutAttribute)firstAttribute;
// 偏移量
- (MAAutoLayoutMaker * _Nonnull (^_Nonnull)(CGFloat offset))offset;
// 关系
- (MAAutoLayoutMaker * _Nonnull (^_Nonnull)(id _Nonnull attr))equalTo;
- (MAAutoLayoutMaker * _Nonnull (^_Nonnull)(id _Nonnull attr))greaterThanOrEqualTo;
- (MAAutoLayoutMaker * _Nonnull (^_Nonnull)(id _Nonnull attr))lessThanOrEqualTo;
// 赋值
- (MAAutoLayoutMaker * _Nonnull (^_Nonnull)(CGFloat constant))kf_equal;
- (MAAutoLayoutMaker * _Nonnull (^_Nonnull)(CGFloat constant))kf_greaterThanOrEqual;
- (MAAutoLayoutMaker * _Nonnull (^_Nonnull)(CGFloat constant))kf_lessThanOrEqual;
// 倍数
- (MAAutoLayoutMaker * _Nonnull (^_Nonnull)(CGFloat multiplier))multiplier;
// 权重
- (MAAutoLayoutMaker * _Nonnull (^_Nonnull)(UILayoutPriority priority))priority;

- (BOOL)isActive;

- (nonnull NSLayoutConstraint *)active;
- (void)deactivate;
@end

有了MAAutoLayoutMaker,我们就能轻易的实现view.top.equalTo().offset(10).multiplier(2).但是equalTo中的属性,也就是view2.attr2还没发表示。

三、MAViewAttribute(实现"view2.attr2")

view2.attr2需要在MAViewAttribute保存view2的NSLayoutAttribute。

@interface UIView (MAAutoLayout)

@property (nonatomic, strong, readonly) MAViewAttribute * _Nonnull ma_left;
@property (nonatomic, strong, readonly) MAViewAttribute * _Nonnull ma_top;
#####省略 right, bottom等,详细请看GitHub上的demo #####
//iOS11 safeArea
@property (nonatomic, strong, readonly) MAViewAttribute * _Nonnull ma_safeAreaLayoutGuideTop;
@property (nonatomic, strong, readonly) MAViewAttribute * _Nonnull ma_safeAreaLayoutGuideBottom;
@property (nonatomic, strong, readonly) MAViewAttribute * _Nonnull ma_safeAreaLayoutGuideLeft;
@property (nonatomic, strong, readonly) MAViewAttribute * _Nonnull ma_safeAreaLayoutGuideRight;

@end

@interface UIViewController (MAAutoLayout)

@property (nonatomic, strong, readonly) MAViewAttribute * _Nonnull ma_topLayoutGuide;
@property (nonatomic, strong, readonly) MAViewAttribute * _Nonnull ma_bottomLayoutGuide;
@property (nonatomic, strong, readonly) MAViewAttribute * _Nonnull ma_topLayoutGuideTop;
@property (nonatomic, strong, readonly) MAViewAttribute * _Nonnull ma_topLayoutGuideBottom;
@property (nonatomic, strong, readonly) MAViewAttribute * _Nonnull ma_bottomLayoutGuideTop;
@property (nonatomic, strong, readonly) MAViewAttribute * _Nonnull ma_bottomLayoutGuideBottom;

@property (nonatomic, strong, readonly) MAViewAttribute * _Nonnull ma_safeAreaTopLayoutGuide;
@property (nonatomic, strong, readonly) MAViewAttribute * _Nonnull ma_safeAreaBottomLayoutGuide;

@end

此时调用布局代码的方式为

view.kf_layout.top.equalTo(self.view.ma_top).offset(10).active;
view.kf_layout.left.equalTo(self.view.ma_left).offset(10).active;
view.kf_layout.bottom.equalTo(self.view.ma_bottom).offset(-10).active;
view.kf_layout.right.equalTo(self.view.ma_right).offset(-10).active;

四、UIView封装Block

按照上面的方式布局没有任何问题,但总显得不够优雅,可以将布局代码包装在一个block中,代码更集中也更便于管理。

// 在UIView的Category中添加下面的方法
- (void)ma_makeConstraints:(void(^_Nonnull)(MAAutoLayout * _Nonnull make))make;
- (void)ma_remakeConstraints:(void(^_Nonnull)(MAAutoLayout * _Nonnull make))make;

此时的调用方式就变为:

     [view ma_makeConstraints:^(MAAutoLayout * _Nonnull make) {
        make.top.equalTo(self.view.ma_top).offset(10).active;
        make.left.equalTo(self.view.ma_left).offset(10).active;
        make.bottom.equalTo(self.view.ma_bottom).offset(-10).active;
        make.right.equalTo(self.view.ma_right).offset(-10).active;
    }];

五、代码的实现

步骤四已经基本实现了链式布局的方式,但是每行都要调用一次active方法,总显得不够优雅。我们可以想办法在Block执行完成时,一次性激活所有约束,因此需要在MAAutoLayout中添加一个数组用于保存所有的约束。
MAAutoLayout.m的实现如下:


@interface MAAutoLayout()
// 用于保存约束的数组
@property (nonatomic,strong) NSMutableArray<MAAutoLayoutMaker *> *constraints;
@property (nonatomic,weak) id view;

@end

@implementation MAAutoLayout
// 初始化时,保存view1
- (id)initWithView:(UIView *)view{
    self = [super init];
    if (!self) return nil;
    
    self.view = view;
    // 使用autoLayout需要手动设置translatesAutoresizingMaskIntoConstraints为NO
    view.translatesAutoresizingMaskIntoConstraints = NO;
    self.constraints = [NSMutableArray array];
    return self;
}

#pragma mark - standard Attributes
- (MAAutoLayoutMaker *)top {
    return [self addConstraintWithLayoutAttribute:NSLayoutAttributeTop];
}
#######   篇幅有限仅显示top的实现,left等相同的类比调用addConstraintWithLayoutAttribute实现#########
- (MAAutoLayoutMaker *)addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute {
    MAAutoLayoutMaker *maker = [[MAAutoLayoutMaker alloc] initWithFirstItem:self.view firstAttribute:layoutAttribute];
    // 将这个约束添加到数组中
    [self.constraints addObject:maker];
    return maker;
}
// 激活所有的约束
- (void)active{
    for (MAAutoLayoutMaker *maker in self.constraints) {
        if (!maker.isActive) {
            [maker active];
        }
    }
}
// 撤销所有的约束
- (void)deactivate{
    [self.constraints makeObjectsPerformSelector:@selector(deactivate)];
    [self.constraints removeAllObjects];
}
@end

MAAutoLayoutMaker需要保存所有的相关NSLayoutConstraint的属性,是MAAutoLayout的核心。
MAAutoLayoutMaker.m的实现如下:

@interface MAAutoLayoutMaker()
@property (nullable, nonatomic,weak) id firstItem;
@property (nonatomic, assign) NSLayoutAttribute firstAttribute;
@property (nullable, nonatomic,weak) id secondItem;
@property (nonatomic, assign) NSLayoutAttribute secondAttribute;
@property (nonatomic, assign) NSLayoutRelation relation;
@property (nonatomic, assign) CGFloat multiplierValue;
@property (nonatomic, assign) CGFloat constant;
@property (nonatomic, assign) UILayoutPriority priorityValue;
@property (nonatomic,strong) NSLayoutConstraint *layoutConstraint;
@end

@implementation MAAutoLayoutMaker
- (instancetype)initWithFirstItem:(id)firstItem firstAttribute:(NSLayoutAttribute)firstAttribute{
    self = [super init];
    if (!self) return nil;
    self.firstItem = firstItem;
    self.firstAttribute = firstAttribute;
    self.secondItem = nil;
    self.secondAttribute = NSLayoutAttributeNotAnAttribute;
    self.multiplierValue = 1.0;
    self.constant = 0;
    self.priorityValue = UILayoutPriorityRequired;
    return self;
}
- (MAAutoLayoutMaker *(^)(CGFloat))offset{
    return ^id(CGFloat offset){
        self.constant = offset;
        return self;
    };
}
- (MAAutoLayoutMaker * _Nonnull (^)(id _Nonnull))equalTo{
    return ^id(id attribute) {
        return self.equalToWithRelation(attribute, NSLayoutRelationEqual);
    };
}
- (MAAutoLayoutMaker * _Nonnull (^)(CGFloat))kf_equal{
    return ^id(CGFloat constant) {
        return self.equalToWithRelation(@(constant), NSLayoutRelationEqual);
    };
}
- (MAAutoLayoutMaker * _Nonnull (^)(UILayoutPriority))priority{
    return ^(UILayoutPriority priority) {
        self.priorityValue = priority;
        return self;
    };
}
- (MAAutoLayoutMaker * _Nonnull (^)(CGFloat))multiplier{
    return ^(CGFloat multiplier) {
        self.multiplierValue = multiplier;
        return self;
    };
}

- (BOOL)isActive{
    return self.layoutConstraint != nil;
}
// 核心方法,通过上面的便携方法给firstItem,firstAttribute,secondItem, secondAttribute, relation, multiplierValue, constant, priorityValue,在active方法里创建layoutConstraint并激活。
- (NSLayoutConstraint *)active{
    if (self.layoutConstraint) self.layoutConstraint.active = NO;
    if (self.firstItem) {
        self.layoutConstraint = [NSLayoutConstraint constraintWithItem:self.firstItem attribute:self.firstAttribute relatedBy:self.relation toItem:self.secondItem attribute:self.secondAttribute multiplier:self.multiplierValue constant:self.constant];
        self.layoutConstraint.priority = self.priorityValue;
        self.layoutConstraint.active = YES;
    }
    return self.layoutConstraint;
}

- (void)deactivate{
    [self.layoutConstraint setActive:NO];
    self.layoutConstraint = nil;
}

#pragma mark private
- (MAAutoLayoutMaker * (^)(id, NSLayoutRelation))equalToWithRelation {
    return ^id(id attribute, NSLayoutRelation relation) {
        self.relation = relation;
        if ([attribute isKindOfClass:[UIView class]]) {
            self.secondItem = attribute;
            self.secondAttribute = self.firstAttribute;
        }else if ([attribute isKindOfClass:[MAViewAttribute class]]){
            self.secondItem = ((MAViewAttribute *)attribute).item;
            self.secondAttribute = ((MAViewAttribute *)attribute).layoutAttribute;
        }else if ([attribute isKindOfClass:[NSNumber class]]){
            self.secondItem = nil;
            self.secondAttribute = NSLayoutAttributeNotAnAttribute;
            self.constant = ((NSNumber *)attribute).floatValue;
        }else{
            NSAssert(attribute, @"格式不正确,必须是UIView或MAAutoLayoutMaker或NSNumber");
        }
        self.relation = relation;
        return self;
    };
}
@end

UIView+MAAutoLayout.m的实现如下:

@implementation UIView (MAAutoLayout)
static char kInstalledMAAutoLayoutKey;
- (MAAutoLayout *)ma_layout {
    MAAutoLayout *autolayout = objc_getAssociatedObject(self, &kInstalledMAAutoLayoutKey);
    if (!autolayout) {
        autolayout = [[MAAutoLayout alloc] initWithView:self];
        objc_setAssociatedObject(self, &kInstalledMAAutoLayoutKey, autolayout, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    return autolayout;
}
- (void)ma_makeConstraints:(void (^)(MAAutoLayout *))make{
    make(self.ma_layout);
    [self.ma_layout active];
}
- (void)ma_remakeConstraints:(void (^)(MAAutoLayout * _Nonnull))make{
    [self.ma_layout deactivate];
    [self ma_makeConstraints:make];
}
- (MAViewAttribute *)ma_left{
    return [self viewAttribute:NSLayoutAttributeLeft];
}
#####省略right等,详细请看GitHub上的demo#####
#pragma mark - iOS11 safeArea
- (MAViewAttribute *)ma_safeAreaLayoutGuideTop{
    return [self safeAreaViewAttribute:NSLayoutAttributeTop];
}
#####省略safeAreaLeft等,详细请看GitHub上的demo#####
- (UIEdgeInsets)ma_safeAreaInsets{
    UIEdgeInsets safeInsets = UIEdgeInsetsZero;
#ifdef __IPHONE_11_0
    if (@available(iOS 11.0, *)) {
        safeInsets = self.safeAreaInsets;
    }
#endif
    return safeInsets;
}
#pragma mark - private
- (MAViewAttribute *)viewAttribute:(NSLayoutAttribute)layoutAttribute {
    return [[MAViewAttribute alloc] initWithItem:self layoutAttribute:layoutAttribute];
}
- (MAViewAttribute *)safeAreaViewAttribute:(NSLayoutAttribute)layoutAttribute {
    id item = self;
#ifdef __IPHONE_11_0
    if (@available(iOS 11.0, *)) {
        item = self.safeAreaLayoutGuide;
    }
#endif
    return [[MAViewAttribute alloc] initWithItem:item layoutAttribute:layoutAttribute];
}

@end

UIViewController+MAAutoLayout.m的实现如下:

@implementation UIViewController (MAAutoLayout)

- (MAViewAttribute *)ma_topLayoutGuideTop{
    return [[MAViewAttribute alloc] initWithItem:self.topLayoutGuide layoutAttribute:NSLayoutAttributeTop];
}
#####省略topLayoutGuideBottom等,详细请看GitHub上的demo#####
- (MAViewAttribute *)ma_safeAreaTopLayoutGuide{
    id item = self.topLayoutGuide;
    NSLayoutAttribute attribute = NSLayoutAttributeBottom;
#ifdef __IPHONE_11_0
    if (@available(iOS 11.0, *)) {
        item = self.view.safeAreaLayoutGuide;
        attribute = NSLayoutAttributeTop;
    }
#endif
    return [[MAViewAttribute alloc] initWithItem:item layoutAttribute:attribute];
}

- (MAViewAttribute *)ma_safeAreaBottomLayoutGuide{
    id item = self.bottomLayoutGuide;
    NSLayoutAttribute attribute = NSLayoutAttributeTop;
#ifdef __IPHONE_11_0
    if (@available(iOS 11.0, *)) {
        item = self.view.safeAreaLayoutGuide;
        attribute = NSLayoutAttributeBottom;
    }
#endif
    return [[MAViewAttribute alloc] initWithItem:item layoutAttribute:attribute];
}

@end

此时完成了AutoLayout的简单封装。这样封装还有一个不便的地方,就是每个view每次都需要四个约束才能完成布局。像Masonry就提供了很多遍历方法,比如 make.top.left.right.bottom.equalTo(self.view).offset(10);或make.edges.equalTo(self.view).insets(UIEdgeInsetsMake(10, 10, 10, 10));这是一条语句添加了多个约束,所以Masonry添加了MASCompositeConstraint的概念。
MAAutoLayout通过一个文件已经实现了链式响应的处理,如果实现的autoLayout比较简单,引入Masonry这么大的库有些浪费,可以使用MAAutoLayout,简单使用也挺好。
以上只是我的理解,有错误的地方请大家指出,大家多沟通交流。
附上demo地址

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