iOS 布局
Frame & Bounds
Frame定义:
表示一个View的位置和大小,使用其父视图坐标系。
Bounds定义:
表示一个View的位置和大小,使用其自身的坐标系。
注意点:
在其父视图坐标器中最小的矩形框。(当view的transform变更时), 这就意味着如果一个视图发生了形变,我们不应该再使用frame的值了。
官方文档
当我们修改transform属性的时,所有的变化都是基于视图view的center进行的。谈到center,能影响到的就是anchorPoint
。这里是官方描述
关于transfrom这里也提供一个链接
什么时候使用Bounds & 什么时候使用Frame ?
frame: 由于frame反应了一个view在其父视图的位置,所以当设置一些外部变化的时候使用,比如设置宽高,距离父视图或某个视图的距离。
bounds: view内在的一些变化需要使用bounds,比如:布局其子视图、绘制一些元素。另外当view发生形变之后获取宽高也用bounds。
Demo演示:
基于View实现ScrollView:
AutoLayout非常好,但是官方的API不容易使用, Masonry 解决了这个;
Masonry 简单类图
Masonry最简单的使用:
1 2 3 4 5
| [self.aView mas_makeConstraints:^(MASConstraintMaker *make) { make.left.equalTo(self.view) make.top.equalTo(self.view.mas_top).offset(100) make.width.height.mas_equalTo(200) }];
|
从这里 mas_makeConstraints
开始
MAS_VIEW 添加约束有三种方式:
1 2 3
| - (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *make))block; - (NSArray *)mas_updateConstraints:(void(^)(MASConstraintMaker *make))block; - (NSArray *)mas_remakeConstraints:(void(^)(MASConstraintMaker *make))block;
|
通过方法名,很容易知道这几个方法的差异
- 添加约束.
- 更新约束.
- 重新设置约束.
1 2 3 4 5 6
| - (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *))block { self.translatesAutoresizingMaskIntoConstraints = NO; MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self]; block(constraintMaker); return [constraintMaker install]; }
|
line 1: 设置translatesAutoresizingMaskIntoConstraints = NO
- YES: 系统会将一些属性例如Frame转换成constraints, Default Vlaue in Code View
- NO: IB Default Value
line 2: 使用当前view实例化MASConstraintMaker
line 3: 执行传进来block,配置maker
注意: 很多人有个疑惑,当使用Masonry时,我们经常在block中直接的引用self,为什么没有形成循环引用?
通过源码可以看出:
- 首先,block强引用了self
- 假如我们在VC中使用上面的代码, self = VC , block -> VC -> view ;
- 这个block没有任何对象持有,所以执行完以后就会销毁。
MASConstraintMaker
先看初始化方法:
1 2 3 4 5 6 7 8 9
| - (id)initWithView:(MAS_VIEW *)view { self = [super init]; if (!self) return nil;
self.view = view; self.constraints = NSMutableArray.new;
return self; }
|
MAS_VIEW
宏使用,为了框架跨平台 iOS & tvOS & macOS
1 2 3 4 5
| #if TARGET_OS_IPHONE || TARGET_OS_TV #define MAS_VIEW UIView #elif TARGET_OS_MAC #define MAS_VIEW NSView #endif
|
line 1: Maker对象引用(weak)了MAS_VIEW, 因为view如果不存在了,maker存在也没有意义,所以没有必要进行(strong)引用。
line 2: 实例化约束数组,存储了所有加到当前视图上的约束对象。
make.left
调用发生了什么:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| - (MASConstraint *)left { return [self addConstraintWithLayoutAttribute:NSLayoutAttributeLeft]; } - (MASConstraint *)addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute { return [self constraint:nil addConstraintWithLayoutAttribute:layoutAttribute]; } - (MASConstraint *)constraint:(MASConstraint *)constraint addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute { MASViewAttribute *viewAttribute = [[MASViewAttribute alloc] initWithView:self.view layoutAttribute:layoutAttribute]; MASViewConstraint *newConstraint = [[MASViewConstraint alloc] initWithFirstViewAttribute:viewAttribute]; ... if (!constraint) { newConstraint.delegate = self; [self.constraints addObject:newConstraint]; } return newConstraint; }
|
最终,都会调用到- constraint: addConstraintWithLayoutAttribute:
里,这里删除了跟make.left
无关的代码,后面会提到。
line 1: 设置初始化的MASViewConstraint的代理对象为self
line 2: 集合中添加这根约束
line 3: 返回Constraint对象
到此为止:make.left 这个方法调用执行完毕,生成了一个MASConstraint对象。
MASConstraint
声明:
1 2
| - (MASConstraint * (^)(id attr))equalTo;
|
make.left.equalTo(self.view)
调用时发生了什么?
乍一看,这个方法很奇怪,返回了一个block对象,这个block接受一个id类型参数并且返回MASConstraint类型对象。我们知道OC调用方法不能使用点语法(属性getter除外),事实上,可以把equalTo看成一个block类型的属性,这个就是属性的getter方法。
实现如下:
1 2 3 4 5
| - (MASConstraint * (^)(id))equalTo { return ^id(id attribute) { return self.equalToWithRelation(attribute, NSLayoutRelationEqual); }; }
|
我们直接进入- equalToWithRelation
方法中,发现实现体重只有MASMethodNotImplemented()
, 证明该方法是抽象想的在MASConstraint中,搜索发现有两个子类:
- MASViewConstraint
- MASCompositeConstraint
注意:抽象方法的实现很有意思,在OC中并没有关于抽象方法的定义,如果运行时直接调用到了MASConstraint,这里采用了抛出一个异常的方式,这会引起crash,这种实现抽象类的方式值得学习。
- 方法名决定了 NSLayoutRelation
- 参数决定attr
MASViewConstraint
先看简单MASViewConstraint对于equalToWithRelation实现:
1 2 3 4 5 6 7 8 9 10 11 12
| - (MASConstraint * (^)(id, NSLayoutRelation))equalToWithRelation { return ^id(id attribute, NSLayoutRelation relation) { if ([attribute isKindOfClass:NSArray.class]) { ... } else { NSAssert(!self.hasLayoutRelation || self.layoutRelation == relation && [attribute isKindOfClass:NSValue.class], @"Redefinition of constraint relation"); self.layoutRelation = relation; self.secondViewAttribute = attribute; return self; } }; }
|
我们这里就equalTo来看,直接进入else逻辑:
line 1:断言判断这里判断关系是否重复建立。这里有环境断言的坑
line 2:设置关系
line 3: 设置约束
事实上,者不难能理解,一个约束(除了size)需要包含3个东西,first ViewAttribute, secondViewAttribute, Layout Relation.
setter 方法中干的什么事情:
1 2 3 4 5 6 7 8 9 10 11
| - (void)setSecondViewAttribute:(id)secondViewAttribute { if ([secondViewAttribute isKindOfClass:NSValue.class]) { [self setLayoutConstantWithValue:secondViewAttribute]; } else if ([secondViewAttribute isKindOfClass:MAS_VIEW.class]) { _secondViewAttribute = [[MASViewAttribute alloc] initWithView:secondViewAttribute layoutAttribute:self.firstViewAttribute.layoutAttribute]; } else if ([secondViewAttribute isKindOfClass:MASViewAttribute.class]) { _secondViewAttribute = secondViewAttribute; } else { NSAssert(NO, @"attempting to add unsupported attribute: %@", secondViewAttribute); } }
|
启发?
为了对传入值灵活配置,我们可以将复杂的逻辑收敛起来。
这三种类型分别可以通过以下3条语句映射:
1 2 3
| make.width.mas_equalTo(100) make.left.equalTo(self.view) make.left.equalTo(self.view.mas_left)
|
可以从setSecondViewAttribute中看出来,line 2 & line3 基本上是等价的;
line 2 就是一个传入了MASViewAttribute,一个传入了MAS_VIEW并内部生成了一个MASViewAttribute,当然这里MAS_VIEW对应的 layoutAttribute 则采用用是firstView的。
line 3 直接传入的是MASViewAttribute,可以直接被持有。
MASViewAttribute
MASViewAttribute 干了三件事:
- 持有了MAS_VIEW对象
- id类型的item
- 持有了NSLayoutAttribute对象
MASViewAttribute提供了两个实例化:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| - (id)initWithView:(MAS_VIEW *)view layoutAttribute:(NSLayoutAttribute)layoutAttribute { self = [self initWithView:view item:view layoutAttribute:layoutAttribute]; return self; }
- (id)initWithView:(MAS_VIEW *)view item:(id)item layoutAttribute:(NSLayoutAttribute)layoutAttribute { self = [super init]; if (!self) return nil;
_view = view; _item = item; _layoutAttribute = layoutAttribute;
return self; }
|
通常情况下,item同view指向相同。当使用 VC-related interface 指向ID < UILayoutSupport>
Install
当maker配置完成之后,就轮到了install了,intall源码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| - (void)install { ... MAS_VIEW *firstLayoutItem = self.firstViewAttribute.item; NSLayoutAttribute firstLayoutAttribute = self.firstViewAttribute.layoutAttribute; MAS_VIEW *secondLayoutItem = self.secondViewAttribute.item; NSLayoutAttribute secondLayoutAttribute = self.secondViewAttribute.layoutAttribute; ... MASLayoutConstraint *layoutConstraint = [MASLayoutConstraint constraintWithItem:firstLayoutItem attribute:firstLayoutAttribute relatedBy:self.layoutRelation toItem:secondLayoutItem attribute:secondLayoutAttribute multiplier:self.layoutMultiplier constant:self.layoutConstant]; ... MAS_VIEW *closestCommonSuperview = [self.firstViewAttribute.view mas_closestCommonSuperview:self.secondViewAttribute.view]; self.installedView = closestCommonSuperview; ... [self.installedView addConstraint:layoutConstraint]; self.layoutConstraint = layoutConstraint; [firstLayoutItem.mas_installedConstraints addObject:self]; }
|
这里主要的逻辑是:
- 根据两个viewAttribute使用系统API生成了一个MASLayoutConstraint对象。
- 调用 mas_closestCommonSuperview 方法找到最近的共同的父视图。
- 将constraint添加到这个视图上。
- 一些记录操作,self持有了layoutConstraint, firstLayoutItem的mas_installedConstraints集合里添加constraint
其中:mas_closestCommonSuperview 实现可以看下;
当然,这里有很多判断,这里只讨论了最通用的流程,到此为止,添加约束的逻辑完成了。
MASCompositeConstraint
倒回去看,在分析 make.left 执行的过程中,忽略了一部分关于 MASCompositeConstraint 代码,这里接着分析下:
在一开始的例子中:make.width.height.mas_equalTo(200); 会执行到这部分逻辑里:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| - (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]) { NSArray *children = @[constraint, newConstraint]; MASCompositeConstraint *compositeConstraint = [[MASCompositeConstraint alloc] initWithChildren:children]; compositeConstraint.delegate = self; [self constraint:constraint shouldBeReplacedWithConstraint:compositeConstraint]; return compositeConstraint; } ... return newConstraint; }
|
当我们执行 make.width&left&right&top&bottom等等,会生成一个MASViewAttribute对象,当我们紧接着调用height的时候,会生成一个MASCompositeConstraint对象(这个对象持有了MASConstraint-type数组),然后使用CompoeConstraint替换original constrConstraint;
Masonry正是用这个对象去支持同时设置很多个constraint
有人会问,上面的判断貌似只能对第二次设置有效,make.top.left.bottom.right.equalTo(self.view); 像这样make.top.left 返回了MASCompositeConstraint,再继续调用bottom,就完全在MASCompositeConstraint内部处理了:
1 2 3 4 5 6 7
| - (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; }
|
在这里自己作为自己的代理,MASCompositeConstraint在第二次生成了 - constraint: addConstraint With LayoutAttribute: method.中添加了新增的约束,其中新增的约束是通过下面的代码实现的
1 2 3 4 5 6
| - (MASConstraint *)constraint:(MASConstraint *)constraint addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute { MASViewAttribute *viewAttribute = [[MASViewAttribute alloc] initWithView:self.view layoutAttribute:layoutAttribute]; MASViewConstraint *newConstraint = [[MASViewConstraint alloc] initWithFirstViewAttribute:viewAttribute]; ... return newConstraint; }
|
总结
Masonry 的代码量不大,因为逻辑复用比较多,各种block嵌套,抽象方法的使用。但是这库是非常优秀的,他教会了我们如果提供一个简单的接口方案。教会了我们如何将复杂的东西让人用起来如此简单;
调用链设计
Masony 使用了大量的block实现了链式调用,链式的使用节省了大段代码边写,且更容易理解。
抽象方法的实现
使用宏实现抽象方法:
1 2 3 4
| #define MASMethodNotImplemented() \ @throw [NSException exceptionWithName:NSInternalInconsistencyException \ reason:[NSString stringWithFormat:@"You must override %@ in a subclass.", NSStringFromSelector(_cmd)] \ userInfo:nil]
|
包装宏的使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| #define mas_equalTo(...) equalTo(MASBoxValue((__VA_ARGS__))) #define mas_greaterThanOrEqualTo(...) greaterThanOrEqualTo(MASBoxValue((__VA_ARGS__))) #define mas_lessThanOrEqualTo(...) lessThanOrEqualTo(MASBoxValue((__VA_ARGS__)))
#define mas_offset(...) valueOffset(MASBoxValue((__VA_ARGS__)))
@interface MASConstraint (AutoboxingSupport)
- (MASConstraint * (^)(id attr))mas_equalTo; - (MASConstraint * (^)(id attr))mas_greaterThanOrEqualTo; - (MASConstraint * (^)(id attr))mas_lessThanOrEqualTo;
- (MASConstraint * (^)(id offset))mas_offset;
@end
|
避免循环引用的方式
使用block有两个比较烦的地方
- 循环引用
- 回调地狱
Masonry 的完美设计, 让我们完全不用关心有这样的问题。
总之,这个库足够优秀;