一、安装Masonry框架

第一步:创建一个工程‘MasonryDemo’

第二步:在项目文件下,创建一个PodFile文件

touch PodFile

 

第三步:用vim命令打开PodFile

vim PodFile

第四步:输入以下内容

platform :ios, '14.4'   #系统编号,自己更改
target 'MasonryDemo' do   #'MasonryDemo'工程名字,自己根据自己的工程名字更改
pod 'Masonry' 
end

ios系统的框架 苹果框架下载安装_学习

第五步:输入pod install命令,运行结果如下

ios系统的框架 苹果框架下载安装_ios系统的框架_02

运行成功会出现一个MasonryDemo.xcworkspace文件,我们打开MasonryDemo.xcworkspace文件进行编写代码运行即可。

ios系统的框架 苹果框架下载安装_学习_03

二、Masonry基础

2.1 Masonry基础API

mas_makeConstraints()    添加约束
mas_remakeConstraints()  移除之前的约束,重新添加新的约束
mas_updateConstraints()  更新约束,写哪条更新哪条,其他约束不变

equalTo()       参数是对象类型,一般是视图对象或者mas_width这样的坐标系对象
mas_equalTo()   和上面功能相同,参数可以传递基础数据类型对象,可以理解为比上面的API更强大
[vi mas_makeConstraints:^(MASConstraintMaker *make) {
        make.size.mas_equalTo(CGSizeMake(200, 200));
        make.center.equalTo(self.view);
    }];

2.2 设置内边距

yellow视图与vi视图大小等大,并且有一些内边距,可以自己自行设计,代码如下:

[self.yellowView mas_makeConstraints:^(MASConstraintMaker *make) {
        make.left.equalTo(vi).with.offset(10);
        .equalTo(vi).with.offset(40);
        make.right.equalTo(vi).with.offset(-10);
        make.bottom.equalTo(vi).with.offset(-10);
    }];

另一种写法:

[self.yellowView mas_makeConstraints:^(MASConstraintMaker *make) {
        // 下、右不需要写负号,insets方法中已经为我们做了取反的操作了。
        make.edges.equalTo(vi).insets(UIEdgeInsetsMake(40, 10, 10, 10));
    }];

结果如下:

ios系统的框架 苹果框架下载安装_ios系统的框架_04

2.3 大于等于和小于等于某个值的约束

[self.textLable mas_makeConstraints:^(MASConstraintMaker *make) {
        make.center.equalTo(self.view);
        // 设置宽度小于等于200
        make.width.lessThanOrEqualTo(@200);
        // 设置高度大于等于10
        make.height.greaterThanOrEqualTo(@10);
}];
    
self.textLable.text = @"这是测试的字符串。这是测试的字符串。这是测试的字符串。这是测试的字符串。这是测试的字符串。这是测试的字符串。这是测试的字符串。这是测试的字符串。这是测试的字符串。这是测试的字符串。这是测试的字符串。这是测试的字符串。这是测试的字符串。这是测试的字符串。这是测试的字符串。这是测试的字符串。这是测试的字符串。这是测试的字符串。这是测试的字符串。这是测试的字符串。这是测试的字符串。";
    
self.textLable.numberOfLines = 0;  //可以自己试验一下这行去掉了是什么效果,和更改数字的效果

ios系统的框架 苹果框架下载安装_学习_05

参考文档:

深入剖析Auto Layout

系统理解 iOS 自动布局

三、VFL布局

3.1 VFL的思想

  • VFL的思想与其他的实现方法有所不同,它更为宏观化,它将约束分成了两块
  • 水平方向(H:)
  • 垂直方向(V:)
  • 也就是说,大家在创建约束的时候,得把水平与垂直方向的约束用字符串一并表达出来,而不是一个一个的添加

3.2 代码解析

首先我们需要创建两个view,一个红view,一个蓝view;这两个view的中间间距是20,距离左边距,下边距,右边距距离也是30,代码如下:

UIView *redView = [[UIView alloc] init];
    UIView *blueView = [[UIView alloc] init];
    redView.backgroundColor = [UIColor redColor];
    blueView.backgroundColor = [UIColor blueColor];
    
    redView.translatesAutoresizingMaskIntoConstraints = NO;
    blueView.translatesAutoresizingMaskIntoConstraints = NO;
    
    [self.view addSubview:redView];
    [self.view addSubview:blueView];
    
    NSDictionary *views = NSDictionaryOfVariableBindings(redView,blueView);
    
    NSDictionary *spaceMetrics = @{@"space": @30,
                                   @"height" : @100};
    //水平
    NSString *hRedBlueVFL = \
    @"H:|-space-[blueView]-space-[redView(==blueView)]-space-|";
    
    NSArray *hLaoutConstraints = \
    [NSLayoutConstraint constraintsWithVisualFormat:hRedBlueVFL
                                            options:NSLayoutFormatAlignAllTop | NSLayoutFormatAlignAllBottom
                                            metrics:spaceMetrics
                                              views:views];
    
    [self.view addConstraints:hLaoutConstraints];
    
    //竖直
    NSString *vRedBlueVFL = @"V:[blueView(50)]-space-|";
    NSArray *vLayoutConstraints = \
    [NSLayoutConstraint constraintsWithVisualFormat:vRedBlueVFL
                                            options:kNilOptions
                                            metrics:spaceMetrics
                                              views:views];
    
    [self.view addConstraints:vLayoutConstraints];

结果如下:

ios系统的框架 苹果框架下载安装_ios_06

好的,让我们现在来分析一下代码:

  • 首先我们来介绍一下API
/**
 *  VFL创建约束的API
 *
 *  @param format  传入某种格式构成的字符串,用以表达想要添加的约束,如@"H:|-margin-[redView(50)]",水平方向上,redView与父控件左边缘保持“margin”间距,redView的宽为50
 *  @param opts    对齐方式,是个枚举值
 *  @param metrics 一般传入以间距为KEY的字典,如: @{ @"margin":@20},KEY要与format参数里所填写的“margin”相同
 *  @param views   传入约束中提到的View,也是要传入字典,但是KEY一定要和format参数里所填写的View名字相同,如:上面填的是redView,所以KEY是@“redView”
 *
 *  @return 返回约束的数组
 */
+ (NSArray *)constraintsWithVisualFormat:(NSString *)format options:(NSLayoutFormatOptions)opts metrics:(NSDictionary *)metrics views:(NSDictionary *)views;
 
//部分NSLayoutFormatOptions的枚举选项
/*
NSLayoutFormatAlignAllLeft = (1 << NSLayoutAttributeLeft),//左边缘对齐
    NSLayoutFormatAlignAllRight = (1 << NSLayoutAttributeRight),//右边缘对齐
    NSLayoutFormatAlignAllTop = (1 << NSLayoutAttributeTop),
    NSLayoutFormatAlignAllBottom = (1 << NSLayoutAttributeBottom),
    NSLayoutFormatAlignAllLeading = (1 << NSLayoutAttributeLeading),//左边缘对齐
    NSLayoutFormatAlignAllTrailing = (1 << NSLayoutAttributeTrailing),//右边缘对齐
    NSLayoutFormatAlignAllCenterX = (1 << NSLayoutAttributeCenterX),//垂直方向中心对齐
    NSLayoutFormatAlignAllCenterY = (1 << NSLayoutAttributeCenterY),//水平方向中心对齐
*/
  • 我们需要一下属性translatesAutoresizingMaskIntoConstraints,该属性是将自动布局更改为Constraints布局,该属性默认值是YES,我们需要手动置为NO
  • 接下我们需要连接一下那两行字符串的意思
//水平
NSString *hRedBlueVFL = \
@"H:|-space-[blueView]-space-[redView(==blueView)]-space-|";
    
//竖直
NSString *vRedBlueVFL = @"V:[blueView(50)]-space-|";
  1. ”H“代表的是水平的样式 ”V“代表的是竖直的样式
  2. space表示的是距离,也可以直接用具体的数字代替,也可以用其他的字符串代替,这都是可以自己定义的,定义的数据可以存到字典里面,传达API里的metrics就可以了
  3. [redView(==blueView)] 这句话代表的意思是redView的size跟blueView的size是一样的
  4. 在”H“的字符串里面写blueView(50):代表的是 blueView的宽是50;在”V“的字符串里面写blueView(50):代表的是blueView的高的50
  5. ”|“ 这个代表的是边界

我们这会同意翻译一下这两句话:

  • @"H:|-space-[blueView]-space-[redView(==blueView)]-space-|"

blueView的左边距的大小是space,redView的右边距的大小是space,blueView与redView的大小相等,并且二者之间的距离为space

  • @"V:[blueView(50)]-space-|"

blueView的高的50,并且距离下边距的距离为space

四、使用Masonry实现动画

点击按钮,视图View可以上下移动,代码如下:

- (void)moveView {
    [self.view setNeedsUpdateConstraints];
    [self.view updateConstraintsIfNeeded];
    if (self.count%2==0) {
        [UIView animateWithDuration:5 animations:^{
            [self.animaView mas_updateConstraints:^(MASConstraintMaker *make) {
                .equalTo(self.view).offset(700);
            }];
            [self.view layoutIfNeeded];
        }];
    }
    else {
        [UIView animateWithDuration:5 animations:^{
            [self.animaView mas_updateConstraints:^(MASConstraintMaker *make) {
                .equalTo(self.view).offset(418);
            }];
            [self.view layoutIfNeeded];
        }];
    }
    self.count++;
    
}

想要更新约束时添加动画,就需要调用关键的一行代码:setNeedsUpdateConstraints,这是选择对应的视图中的约束需要更新。

对于updateConstraintsIfNeeded这个方法并不是必须的,但是有时候不调用就无法起到我们的效果。但是,官方都是这么写的,从约束的更新原理上讲,这应该写上。我们要使约束立即生效,就必须调用layoutIfNeeded此方法。

问题:为什么要加layoutIfNeeded方法?

我们把[self.view layoutIfNeeded];这代代码注释掉,将self.animaView的信息进行打印,结果如下。

ios系统的框架 苹果框架下载安装_objective-c_07

 我们发现打印出来的第一条数据,仍是初始化的数据,因为在使用Masonry的时候,默认情况下,设置的约束并不会立即生效。接下来我们将注释解开,结果如下:

ios系统的框架 苹果框架下载安装_objective-c_08

我们发现打印出来的第一条数据,已经是我们动画之后的数据了,所以layoutIfNeeded的作用就是使设置控件的约束立即生效。

五、源码解析

5.1 类的作用

类图:

ios系统的框架 苹果框架下载安装_ios_09

 

 5.1.1 View+MASAdditions类的介绍

该类主要包含两个部分,一部分是View的扩展属性,另一部分是View的扩展方法。后面会介绍扩展方法的逻辑。

5.1.2 MASViewAttribute类的介绍

该类主要包含了三个属性,三个方法。从名字我们大致能猜到,该类是对UIView与NSLayoutAttribute的封装。用一个等式来表示就是:MASViewAttribute = UIView + NSLayoutAttribute + item。在MASViewAttribute中view表示约束的对象,item表示该对象被约束的部分。对于UIView来说该item就是UIView本身。该类中除了两个构造器外还有一个isSizeAttribute方法,该方法用来判断MASViewAttribute类中的layoutAttribute属性是否是NSLayoutAttributeWidth或者NSLayoutAttributeHeight,如果是Width或者Height的话,那么约束就添加到当前View上,而不是添加在父视图上。

5.1.3 MASViewConstraint类的介绍

该类是对NSLayoutConstriant类的进一步封装。MASViewConstraint做的最核心的一件事情就是初始化NSLayoutConstriant对象,并将该对象添加在相应的视图上。因为NSLayoutConstriant在初始化时需要NSLayoutAttribute和所约束的View,而MASViewAttribute正是对View与NSLayoutAttribute进行的封装,所以MASViewConstraint类要依赖于MASViewAttribute类。如上图所示。

5.1.4 MASConstraintMaker类的介绍

该类就是一个工厂类,负责创建MASConstraint类型的对象(依赖于MASConstraint接口,而不依赖于具体实现)。mas_makeConstraints方法中的Block的参数就是MASConstraintMaker的对象。用户可以通过该Block回调过来的MASConstraintMaker对象给View指定要添加的约束以及该约束的值。该工厂中的constraints属性数组就记录了该工厂创建的所有MASConstraint对象。

5.2 点语法

.equalTo(vi).with.offset(40);

在使用Masonry框架,我们会使用很多这种调用的方法,他是如何实现的呢?

在MASConstraint中的top,button等约束的getter的方法都会调用下面的这个方法,而这个方法所做的事情就是用过delegate调用MASConstraintMaker中的constaint这个方法,来根据layoutAttribute创建对应的MASConstraint对象。

ios系统的框架 苹果框架下载安装_ios_10

 

我们以.left.right为例。此处的maker, 就是我们的MASConstraintMaker工厂对象,会返回带有NSLayoutAttributeTop属性的MASViewConstraint类的对象,我们先做一个转换:newConstraint = 。那么.left 等价于newConstraint.left,需要注意的是此刻调用的left方法就不在是我们工厂MASConstraintMaker中的left的getter方法了,而是被换到MASViewConstraint类中的left属性的getter方法了。给newConstraint设置代理就是为了可以在MASViewConstraint类中通过代理来调用MASConstraintMaker工厂类的工厂方法来完成创建。

ios系统的框架 苹果框架下载安装_ios系统的框架_11

而想offset(40)这种调用方式是如何实现的呢?

- (MASConstraint * (^)(CGFloat))offset {
    return ^id(CGFloat offset){
        self.offset = offset;
        return self;
    };
}

通过代码,我们发现offset函数的返回值是一个匿名block,这匿名的block有个CGFloat参数,返回值是MASConstraint对象。

总结一下: 会通过delegate的方式创建一个MASConstraint对象,该对象会继续访问equalTo属性,执行equalTo的get方法,会执行get方法里面的block,block的返回值是MASConstraint对象,该对象就会继续通过以上两种方式进行执行。

5.3 对约束方法的源码解析

- (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];
}

首先我们来看一下mas_makeConstraints方法。

我们通过使用mas_makeConstraints对当前视图进行添加的约束,mas_makeConstraints的返回值是一个数组,该数组存放的是对当前视图添加的所有约束。因为MASViewConstraint是对NSLayoutConstraint的封装,所以该数组存储的是MASViewConstraint对象。

该函数的参数是一个block,该block的作用就是用过block给MASConstraintMaker对象中的MAConstraint属性进行初始化。

- (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *))block {
    //关闭自动添加约束,这块是需要手动添加的,默认是YES
    self.translatesAutoresizingMaskIntoConstraints = NO;
    //初始化MASConstraintMaker
    MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self];
    //给maker中的各种属性赋值,通过block进行值的回调
    block(constraintMaker);
    //进行约束的添加,并返回install的约束数组(Array<MAConstraint>)
    return [constraintMaker install];
}

更新与删除的原理是一样的,就是这两个函数都多了两个属性。mas_updateConstraints中将constraintMaker中的updateExisting设置为YES, 也就是说当添加约束时要先检查约束是否已经被安装了,如果被添加了就更新,如果没有被添加就新添加。而mas_remakeConstraints中所做的事情是将removeExisting属性设置成YES, 表示将当前视图上的旧约束进行移除,然后添加上新的约束。

5.4 工厂类中的install

该工厂类不仅仅创建MASConstraint对象,还负责调用MASConstraint对象的install方法来将相应的约束安装在想要的视图上面。该工厂类中的install函数就是遍历工厂对象所创建所有约束对象并调用每个约束对象的install方法来进行约束的安装。

ios系统的框架 苹果框架下载安装_ios_12

5.5 MASViewConstraint中install

MASViewConstraint中install方法负责创建MASLayoutConstraint对象,并且将该对象添加到相应的View上。

- (void)install {
    //如果已经添加过约束,就return
    if (self.hasBeenInstalled) {
        return;
    }
    
    //如果layoutConstraint可以使用“isActive”方法,并且self.layoutConstraint不为nil
    if ([self supportsActiveProperty] && self.layoutConstraint) {
        
        //激活约束
        self.layoutConstraint.active = YES;
        
        //将已激活的约束添加到当前View的已安装约束的数组中
        [self.firstViewAttribute.view.mas_installedConstraints addObject:self];
        return;
    }
    
    MAS_VIEW *firstLayoutItem = self.firstViewAttribute.item;
    NSLayoutAttribute firstLayoutAttribute = self.firstViewAttribute.layoutAttribute;
    
    MAS_VIEW *secondLayoutItem = self.secondViewAttribute.item;
    NSLayoutAttribute secondLayoutAttribute = self.secondViewAttribute.layoutAttribute;

    if (!self.firstViewAttribute.isSizeAttribute && !self.secondViewAttribute) {
        secondLayoutItem = self.firstViewAttribute.view.superview;
        secondLayoutAttribute = firstLayoutAttribute;
    }
    
    
#pragma -- Mark 使用NSLayoutConstraint添加约束
    
    //创建约束
    MASLayoutConstraint *layoutConstraint
        = [MASLayoutConstraint constraintWithItem:firstLayoutItem
                                        attribute:firstLayoutAttribute
                                        relatedBy:self.layoutRelation
                                           toItem:secondLayoutItem
                                        attribute:secondLayoutAttribute
                                       multiplier:self.layoutMultiplier
                                         constant:self.layoutConstant];
    
    layoutConstraint.priority = self.layoutPriority;
    layoutConstraint.mas_key = self.mas_key;
    
    //寻找约束添加的View
    if (self.secondViewAttribute.view) {
        //寻找两个视图的公共父视图
        MAS_VIEW *closestCommonSuperview = [self.firstViewAttribute.view
                                            mas_closestCommonSuperview:self.secondViewAttribute.view];
        self.installedView = closestCommonSuperview;
    } else if (self.firstViewAttribute.isSizeAttribute) {
        self.installedView = self.firstViewAttribute.view;
    } else {
        self.installedView = self.firstViewAttribute.view.superview;
    }


    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];      //约束所在的View增加被添加的约束
    }
}

参考文档

iOS开发之Masonry框架源码解析