Masonry思路解析

2017-11-28  本文已影响54人  Mage

当前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地址

上一篇下一篇

猜你喜欢

热点阅读