<简书 — 刘小壮> http://www.jianshu.com/p/ea74b230c70dgit
目前
iOS
开发中大多数页面都已经开始使用Interface Builder
的方式进行UI
开发了,可是在一些变化比较复杂的页面,仍是须要经过代码来进行UI
开发的。并且有不少比较老的项目,自己就还在采用纯代码的方式进行开发。github而如今
iPhone
和iPad
屏幕尺寸愈来愈多,虽然开发者只须要根据屏幕点进行开发,而不须要基于像素点进行UI
开发。但若是在项目中根据不一样屏幕尺寸进行各类判断,写死坐标的话,这样开发起来是很吃力的。objective-c因此通常用纯代码开发
UI
的话,通常都是配合一些自动化布局的框架进行屏幕适配。苹果为咱们提供的适配框架有:VFL
、UIViewAutoresizing
、Auto Layout
、Size Classes
等。编程其中
Auto Layout
是使用频率最高的布局框架,可是其也有弊端。就是在使用NSLayoutConstraint
的时候,会发现代码量不少,并且大多都是重复性的代码,以致于好多人都不想用这个框架。数组后来
Github
上的出现了基于NSLayoutConstraint
封装的第三方布局框架Masonry
,Masonry
使用起来很是方便,本篇文章就详细讲一下Masonry
的使用。框架
这篇文章只是简单介绍Masonry
,以及Masonry
的使用,而且会举一些例子出来。但并不会涉及到Masonry
的内部实现,之后会专门写篇文章来介绍其内部实现原理,包括顺便讲一下链式语法。less
Masonry
是一个对系统NSLayoutConstraint
进行封装的第三方自动布局框架,采用链式编程的方式提供给开发者API
。系统AutoLayout
支持的操做,Masonry
都支持,相比系统API
功能来讲,Masonry
是有过之而无不及。函数
Masonry
采起了链式编程的方式,代码理解起来很是清晰易懂,并且写完以后代码量看起来很是少。以前用NSLayoutConstraint
写不少代码才能实现的布局,用Masonry
最少一行代码就能够搞定。下面看到Masonry
的代码就会发现,太简单易懂了。布局
Masonry
是同时支持Mac
和iOS
两个平台的,在这两个平台上均可以使用Masonry
进行自动布局。咱们能够从MASUtilities.h
文件中,看到下面的定义,这就是Masonry
经过宏定义的方式,区分两个平台独有的一些关键字。性能
#if TARGET_OS_IPHONE #import <UIKit/UIKit.h> #define MAS_VIEW UIView #define MASEdgeInsets UIEdgeInsets #elif TARGET_OS_MAC #import <AppKit/AppKit.h> #define MAS_VIEW NSView #define MASEdgeInsets NSEdgeInsets #endif
Github地址: https://github.com/SnapKit/Masonry
Masonry
支持CocoaPods
,能够直接经过podfile
文件进行集成,须要在CocoaPods
中添加下面代码:
pod 'Masonry'
在UI
开发中,纯代码和Interface Builder
我都是用过的,在开发过程当中也积累了一些经验。对于初学者学习纯代码AutoLayout
,我建议仍是先学会Interface Builder
方式的AutoLayout
,领悟苹果对自动布局的规则和思想,而后再把这套思想嵌套在纯代码上。这样学习起来更好入手,也能够避免踩好多坑。
在项目中设置的AutoLayout
约束,起到对视图布局的标记做用。设置好约束以后,程序运行过程当中建立视图时,会根据设置好的约束计算frame
,并渲染到视图上。
因此在纯代码状况下,视图设置的约束是否正确,要以运行以后显示的结果和打印的log
为准。
在使用Masonry
进行约束时,有一些是须要注意的。
Masonry
添加约束以前,须要在addSubview
以后才能使用,不然会致使崩溃。log
排查。Interface Builder
添加约束,若是约束有错误直接就能够看出来,而且会以红色或者黄色警告体现出来。而Masonry
则不会直观的体现出来,而是以运行过程当中崩溃或者打印异常log
体现,因此这也是手写代码进行AutoLayout
的一个缺点。 这个问题只能经过多敲代码,积攒纯代码进行AutoLayout
的经验,慢慢就用起来愈来愈驾轻就熟了。mas_makeConstraints() 添加约束 mas_remakeConstraints() 移除以前的约束,从新添加新的约束 mas_updateConstraints() 更新约束,写哪条更新哪条,其余约束不变 equalTo() 参数是对象类型,通常是视图对象或者mas_width这样的坐标系对象 mas_equalTo() 和上面功能相同,参数能够传递基础数据类型对象,能够理解为比上面的API更强大 width() 用来表示宽度,例如表明view的宽度 mas_width() 用来获取宽度的值。和上面的区别在于,一个表明某个坐标系对象,一个用来获取坐标系对象的值
上面例如equalTo
或者width
这样的,有时候须要涉及到使用mas_
前缀,这在开发中须要注意做区分。 若是在当前类引入#import "Masonry.h"
以前,用下面两种宏定义声明一下,就不须要区分mas_
前缀。
// 定义这个常量,就能够不用在开发过程当中使用"mas_"前缀。 #define MAS_SHORTHAND // 定义这个常量,就可让Masonry帮咱们自动把基础数据类型的数据,自动装箱为对象类型。 #define MAS_SHORTHAND_GLOBALS
Masonry
为了让代码使用和阅读更容易理解,因此直接经过点语法就能够调用,还添加了and
和with
两个方法。这两个方法内部实际上什么都没干,只是在内部将self
直接返回,功能就是为了更加方便阅读,对代码执行没有实际做用。 例以下面的例子:
make.top.and.bottom.equalTo(self.containerView).with.offset(padding);
其内部代码实现,实际上就是直接将self
返回。
- (MASConstraint *)with { return self; }
关于更新约束布局相关的API
,主要用如下四个API
:
- (void)updateConstraintsIfNeeded 调用此方法,若是有标记为须要从新布局的约束,则当即进行从新布局,内部会调用updateConstraints方法 - (void)updateConstraints 重写此方法,内部实现自定义布局过程 - (BOOL)needsUpdateConstraints 当前是否须要从新布局,内部会判断当前有没有被标记的约束 - (void)setNeedsUpdateConstraints 标记须要进行从新布局
关于UIView
从新布局相关的API
,主要用如下三个API
:
- (void)setNeedsLayout 标记为须要从新布局 - (void)layoutIfNeeded 查看当前视图是否被标记须要从新布局,有则在内部调用layoutSubviews方法进行从新布局 - (void)layoutSubviews 重写当前方法,在内部完成从新布局操做
Masonry本质上就是对系统AutoLayout进行的封装,包括里面不少的API,都是对系统API进行了一次二次包装。 typedef NS_OPTIONS(NSInteger, MASAttribute) { MASAttributeLeft = 1 << NSLayoutAttributeLeft, MASAttributeRight = 1 << NSLayoutAttributeRight, MASAttributeTop = 1 << NSLayoutAttributeTop, MASAttributeBottom = 1 << NSLayoutAttributeBottom, MASAttributeLeading = 1 << NSLayoutAttributeLeading, MASAttributeTrailing = 1 << NSLayoutAttributeTrailing, MASAttributeWidth = 1 << NSLayoutAttributeWidth, MASAttributeHeight = 1 << NSLayoutAttributeHeight, MASAttributeCenterX = 1 << NSLayoutAttributeCenterX, MASAttributeCenterY = 1 << NSLayoutAttributeCenterY, MASAttributeBaseline = 1 << NSLayoutAttributeBaseline, };
/** 设置yellow视图和self.view等大,而且有10的内边距。 注意根据UIView的坐标系,下面right和bottom进行了取反。因此不能写成下面这样,不然right、bottom这两个方向会出现问题。 make.edges.equalTo(self.view).with.offset(10); 除了下面例子中的offset()方法,还有针对不一样坐标系的centerOffset()、sizeOffset()、valueOffset()之类的方法。 */ [self.yellowView mas_makeConstraints:^(MASConstraintMaker *make) { make.left.equalTo(self.view).with.offset(10); make.top.equalTo(self.view).with.offset(10); make.right.equalTo(self.view).with.offset(-10); make.bottom.equalTo(self.view).with.offset(-10); }];
// 下面的方法和上面例子等价,区别在于使用insets()方法。 [self.blueView mas_makeConstraints:^(MASConstraintMaker *make) { // 下、右不须要写负号,insets方法中已经为咱们作了取反的操做了。 make.edges.equalTo(self.view).with.insets(UIEdgeInsetsMake(10, 10, 10, 10)); }];
// 设置greenView的center和size,这样就能够达到简单进行约束的目的 [self.greenView mas_makeConstraints:^(MASConstraintMaker *make) { make.center.equalTo(self.view); // 这里经过mas_equalTo给size设置了基础数据类型的参数,参数为CGSize的结构体 make.size.mas_equalTo(CGSizeMake(300, 300)); }]; // 为了更清楚的看出约束变化的效果,在显示两秒后更新约束。 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.f * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ // 指定更新size,其余约束不变。 [self.greenView mas_updateConstraints:^(MASConstraintMaker *make) { make.size.mas_equalTo(CGSizeMake(100, 100)); }]; });
[self.textLabel mas_makeConstraints:^(MASConstraintMaker *make) { make.center.equalTo(self.view); // 设置宽度小于等于200 make.width.lessThanOrEqualTo(@200); // 设置高度大于等于10 make.height.greaterThanOrEqualTo(@(10)); }]; self.textLabel.text = @"这是测试的字符串。能看到一、二、3个步骤,第一步固然是上传照片了,要上传正面近照哦。上传后,网站会自动识别你的面部,若是以为识别的不许,你还能够手动修改一下。左边能够看到16项修改参数,最上面是总体修改,你也能够根据本身的意愿单独修改某项,将鼠标放到选项上面,右边的预览图会显示相应的位置。";
textLabel
只须要设置一个属性便可
self.textLabel.numberOfLines = 0;
/** 若是想使用基础数据类型当作参数,Masonry为咱们提供了"mas_xx"格式的宏定义。 这些宏定义会将传入的基础数据类型转换为NSNumber类型,这个过程叫作封箱(Auto Boxing)。 "mas_xx"开头的宏定义,内部都是经过MASBoxValue()函数实现的。 这样的宏定义主要有四个,分别是mas_equalTo()、mas_offset()和大于等于、小于等于四个。 */ [self.redView mas_makeConstraints:^(MASConstraintMaker *make) { make.center.equalTo(self.view); make.width.mas_equalTo(100); make.height.mas_equalTo(100); }];
/** Masonry为咱们提供了三个默认的方法,priorityLow()、priorityMedium()、priorityHigh(),这三个方法内部对应着不一样的默认优先级。 除了这三个方法,咱们也能够本身设置优先级的值,能够经过priority()方法来设置。 */ [self.redView mas_makeConstraints:^(MASConstraintMaker *make) { make.center.equalTo(self.view); make.width.equalTo(self.view).priorityLow(); make.width.mas_equalTo(20).priorityHigh(); make.height.equalTo(self.view).priority(200); make.height.mas_equalTo(100).priority(1000); }];
Masonry也帮咱们定义好了一些默认的优先级常量,分别对应着不一样的数值,优先级最大数值是1000。 static const MASLayoutPriority MASLayoutPriorityRequired = UILayoutPriorityRequired; static const MASLayoutPriority MASLayoutPriorityDefaultHigh = UILayoutPriorityDefaultHigh; static const MASLayoutPriority MASLayoutPriorityDefaultMedium = 500; static const MASLayoutPriority MASLayoutPriorityDefaultLow = UILayoutPriorityDefaultLow; static const MASLayoutPriority MASLayoutPriorityFittingSizeLevel = UILayoutPriorityFittingSizeLevel;
// 设置当前约束值乘以多少,例如这个例子是redView的宽度是self.view宽度的0.2倍。 [self.redView mas_makeConstraints:^(MASConstraintMaker *make) { make.center.equalTo(self.view); make.height.mas_equalTo(30); make.width.equalTo(self.view).multipliedBy(0.2); }];
/** 下面的例子是经过给equalTo()方法传入一个数组,设置数组中子视图及当前make对应的视图之间等高。 须要注意的是,下面block中设置边距的时候,应该用insets来设置,而不是用offset。 由于用offset设置right和bottom的边距时,这两个值应该是负数,因此若是经过offset来统一设置值会有问题。 */ CGFloat padding = LXZViewPadding; [self.redView mas_makeConstraints:^(MASConstraintMaker *make) { make.left.right.top.equalTo(self.view).insets(UIEdgeInsetsMake(padding, padding, 0, padding)); make.bottom.equalTo(self.blueView.mas_top).offset(-padding); }]; [self.blueView mas_makeConstraints:^(MASConstraintMaker *make) { make.left.right.equalTo(self.view).insets(UIEdgeInsetsMake(0, padding, 0, padding)); make.bottom.equalTo(self.yellowView.mas_top).offset(-padding); }]; /** 下面设置make.height的数组是关键,经过这个数组能够设置这三个视图高度相等。其余例如宽度之类的,也是相似的方式。 */ [self.yellowView mas_makeConstraints:^(MASConstraintMaker *make) { make.left.right.bottom.equalTo(self.view).insets(UIEdgeInsetsMake(0, padding, padding, padding)); make.height.equalTo(@[self.blueView, self.redView]); }];
/** 要求:(这个例子是在其余人博客里看到的,而后按照要求本身写了下面这段代码) 两个视图相对于父视图垂直居中,而且两个视图以及父视图之间的边距均为10,高度为150,两个视图宽度相等。 */ CGFloat padding = 10.f; [self.blueView mas_makeConstraints:^(MASConstraintMaker *make) { make.centerY.equalTo(self.view); make.left.equalTo(self.view).mas_offset(padding); make.right.equalTo(self.redView.mas_left).mas_offset(-padding); make.width.equalTo(self.redView); make.height.mas_equalTo(150); }]; [self.redView mas_makeConstraints:^(MASConstraintMaker *make) { make.centerY.equalTo(self.view); make.right.equalTo(self.view).mas_offset(-padding); make.width.equalTo(self.blueView); make.height.mas_equalTo(150); }];
在iOS
UI
开发过程当中,UITableView
的动态Cell
高度一直都是个问题。实现这样的需求,实现方式有不少种,只是实现起来复杂程度和性能的区别。
在不考虑性能的状况下,tableView
动态Cell
高度,能够采起估算高度的方式。若是经过估算高度的方式实现的话,不管是纯代码仍是Interface Builder
,都只须要两行代码就能够完成Cell
自动高度适配。
实现方式: 须要设置tableView
的rowHeight
属性,这里设置为自动高度,告诉系统Cell
的高度是不固定的,须要系统帮咱们进行计算。而后设置tableView
的estimatedRowHeight
属性,设置一个估计的高度。(我这里用的代理方法,实际上都同样)
原理: 这样的话,在tableView
被建立以后,系统会根据estimatedRowHeight
属性设置的值,为tableView
设置一个估计的值。而后在Cell
显示的时候再获取Cell
的高度,并刷新tableView
的contentSize
。
实现代码: UITableView部分
- (void)tableViewConstraints { [self.tableView mas_makeConstraints:^(MASConstraintMaker *make) { make.edges.equalTo(self.view); }]; } - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { return self.dataList.count; } - (MasonryTableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { MasonryTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:LXZTableViewCellIdentifier]; [cell reloadViewWithText:self.dataList[indexPath.row]]; return cell; } // 须要注意的是,这个代理方法和直接返回当前Cell高度的代理方法并不同。 // 这个代理方法会将当前全部Cell的高度都预估出来,而不是只计算显示的Cell,因此这种方式对性能消耗仍是很大的。 // 因此经过设置estimatedRowHeight属性的方式,和这种代理方法的方式,最后性能消耗都是同样的。 - (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath { return 50.f; } - (UITableView *)tableView { if (!_tableView) { _tableView = [[UITableView alloc] initWithFrame:CGRectZero style:UITableViewStylePlain]; _tableView.delegate = self; _tableView.dataSource = self; // 设置tableView自动高度 _tableView.rowHeight = UITableViewAutomaticDimension; [_tableView registerClass:[MasonryTableViewCell class] forCellReuseIdentifier:LXZTableViewCellIdentifier]; [self.view addSubview:_tableView]; } return _tableView; }
UITableViewCell部分
// 自定义了一个UIImageView和UILabel控件,而且经过Masonry进行约束。 [self.avatarImageView mas_makeConstraints:^(MASConstraintMaker *make) { make.size.mas_equalTo(CGSizeMake(40, 40)); make.top.left.equalTo(self.contentView).mas_offset(CellPadding); }]; [self.detailLabel mas_makeConstraints:^(MASConstraintMaker *make) { make.left.equalTo(self.avatarImageView.mas_right).mas_offset(CellPadding); make.top.equalTo(self.contentView).mas_offset(CellPadding); make.right.bottom.equalTo(self.contentView).mas_offset(-CellPadding); make.height.greaterThanOrEqualTo(@30); }];
以前听不少人说过UIScrollView
很麻烦,然而我并无感受到有多麻烦(并不是装逼)。我感受说麻烦的人可能根本就没试过吧,只是以为很麻烦而已。 我这里就讲一下两种进行UIScrollView
自动布局的方案,而且会讲一下自动布局的技巧,只要掌握技巧,布局其实很简单。
布局小技巧: 给
UIScrollView
添加的约束是定义其frame
,设置contentSize
是定义其内部大小。UIScrollView
进行addSubview
操做,都是将其子视图添加到contentView
上。 因此,添加到UIScrollView
上的子视图,对UIScrollView
添加的约束都是做用于contentView
上的。只须要按照这样的思路给UIScrollView
设置约束,就能够掌握设置约束的技巧了。
// 提早设置好UIScrollView的contentSize,并设置UIScrollView自身的约束 self.scrollView.contentSize = CGSizeMake(1000, 1000); [self.scrollView mas_makeConstraints:^(MASConstraintMaker *make) { make.edges.equalTo(self.view); }]; // 虽然redView的get方法内部已经执行过addSubview操做,可是UIView始终以最后一次添加的父视图为准,也就是redView始终是在最后一次添加的父视图上。 [self.scrollView addSubview:self.redView]; [self.redView mas_makeConstraints:^(MASConstraintMaker *make) { make.left.top.equalTo(self.scrollView); make.width.height.mas_equalTo(200); }]; [self.scrollView addSubview:self.blueView]; [self.blueView mas_makeConstraints:^(MASConstraintMaker *make) { make.left.equalTo(self.redView.mas_right); make.top.equalTo(self.scrollView); make.width.height.equalTo(self.redView); }]; [self.scrollView addSubview:self.greenView]; [self.greenView mas_makeConstraints:^(MASConstraintMaker *make) { make.left.equalTo(self.scrollView); make.top.equalTo(self.redView.mas_bottom); make.width.height.equalTo(self.redView); }];
上面的例子是提早设置好UIScrollView
的contentSize
的内部size
,而后直接向里面addSubview
。可是这有个要求就是,须要提早知道contentSize
的大小,否则无法设置。 这个例子中将会展现动态改变contentSize
的大小,内部视图有多少contentSize
就自动扩充到多大。
这种方式的实现,主要是依赖于建立一个containerView
内容视图,并添加到UIScrollView
上做为子视图。UIScrollView
原来的子视图都添加到containerView
上,而且和这个视图设置约束。 由于对UIScrollView
进行addSubview
操做的时候,本质上是往其contentView
上添加。也就是containerView
的父视图是contentView
,经过containerView
撑起contentView
视图的大小,以此来实现动态改变contentSize
。
// 在进行约束的时候,要对containerView的上下左右都添加和子视图的约束,以便确认containerView的边界区域。 [self.scrollView mas_makeConstraints:^(MASConstraintMaker *make) { make.edges.equalTo(self.view); }]; CGFloat padding = LXZViewPadding; [self.containerView mas_makeConstraints:^(MASConstraintMaker *make) { make.edges.equalTo(self.scrollView).insets(UIEdgeInsetsMake(padding, padding, padding, padding)); }]; [self.containerView addSubview:self.greenView]; [self.greenView mas_makeConstraints:^(MASConstraintMaker *make) { make.top.left.equalTo(self.containerView).offset(padding); make.size.mas_equalTo(CGSizeMake(250, 250)); }]; [self.containerView addSubview:self.redView]; [self.redView mas_makeConstraints:^(MASConstraintMaker *make) { make.top.equalTo(self.containerView).offset(padding); make.left.equalTo(self.greenView.mas_right).offset(padding); make.size.equalTo(self.greenView); make.right.equalTo(self.containerView).offset(-padding); }]; [self.containerView addSubview:self.yellowView]; [self.yellowView mas_makeConstraints:^(MASConstraintMaker *make) { make.left.equalTo(self.containerView).offset(padding); make.top.equalTo(self.greenView.mas_bottom).offset(padding); make.size.equalTo(self.greenView); make.bottom.equalTo(self.containerView).offset(-padding); }];