1 理解自身內容尺寸約束與抗壓抗拉
自身內容尺寸約束:一般來說,要確定一個視圖的精確位置,至少需要4個布局約束(以確定水平位置x、垂直位置y、寬度w和高度h)。但是,某些用來展現內容的用戶控件,例如文本控件UILabel、按鈕UIButton、圖片視圖UIImageView等,它們具有自身內容尺寸(Intrinsic Content Size),此類用戶控件會根據自身內容尺寸添加布局約束。也就是說,如果開發者沒有顯式給出其寬度或者高度約束,則其自動添加的自身內容約束將會起作用。因此看似“缺失”約束,實際上並非如此。
關於自身內容尺寸約束,簡單來說就是某些用來展現內容的用戶控件,它們會根據自身內容尺寸添加布局約束。
自身內容尺寸約束的抗擠壓與抗拉抻效果。彈簧會有自身固有長度,當有外力作用時,彈簧會抵抗外力作用,盡量接近固有長度。
抗拉抻:當外力拉長彈簧時,彈簧長度大於固有長度,且產生向內收的力阻止外力拉抻,且盡量維持長度接近自身固有長度。
抗擠壓:當外力擠壓彈簧時,彈簧長度小於固有長度,且產生向外頂的力阻止外力擠壓,且盡量維持長度接近自身固有長度。
關於抗壓抗拉,就是布局沖突需要犧牲某些控件的某些寬度或者高度約束時,抗壓高的控件越不容易被壓縮,抗拉高的控件越不容易被拉升。即自身布局對抗外界布局的能力。
樣例:
一種常見的業務場景是用戶修改地址,在輸入新地址之前先讀取用戶之前的地址作為填充。UI實現是水平平行的UILabel和UITextField。 代碼實現如下:
- (NSString *)aLongAddress{ return @"A long long long long long long long long long address";}- (NSString *)aShortAddress{ return @"A short address";}- (void)sampleCode{ UIView *layoutView = [UIView new]; layoutView.frame = CGRectMake(0, 200, [UIScreen mainScreen].bounds.size.width, 100); layoutView.backgroundColor = [[UIColor alloc] initWithRed:0.5 green:0.5 blue:0.5 alpha:0.5]; [self.view addSubview:layoutView]; UILabel *address = [[UILabel alloc] init]; [layoutView addSubview:address]; address.text = @"地址:"; address.backgroundColor = [UIColor blueColor]; [address mas_makeConstraints:^(MASConstraintMaker *make) { make.centerY.equalTo(layoutView); make.left.equalTo(layoutView).offset(10); }]; UITextField *addressTextField = [[UITextField alloc] init]; [layoutView addSubview:addressTextField]; addressTextField.returnKeyType = UIReturnKeyDone; addressTextField.font = [UIFont systemFontOfSize:15]; addressTextField.clearButtonMode = UITextFieldViewModeWhileEditing; addressTextField.layer.borderWidth = 1 / [UIScreen mainScreen].scale; addressTextField.layer.borderColor = [[[UIColor alloc] initWithRed:1 green:1 blue:0 alpha:1] CGColor]; addressTextField.layer.cornerRadius = 3; addressTextField.text = [self aLongAddress]; [addressTextField mas_makeConstraints:^(MASConstraintMaker *make) { make.height.equalTo(address); make.centerY.equalTo(address); make.right.equalTo(layoutView.mas_right).offset(-10); make.left.equalTo(address.mas_right).offset(10); }];}
此處使用了UILabel的自身內容尺寸約束,當houseNumberTextField.text = [self aShortAddress]UI表現正常。
但,當houseNumberTextField.text = [self aLongAddress]時會出現address UILabel被擠壓掉的情況,如下圖所示:
原因是address Label的水平抗壓縮沒有設置。
在address Label創建的時候添加如下代碼[address setContentCompressionResistancePriority:UILayoutPriorityRequired forAxis:UILayoutConstraintAxisHorizontal]則顯示正常。
另,在某些情況下存在view被拉升,極有可能是沒有設置抗拉升,此處不一一列舉。
附,抗壓抗拉相關API如下:
- (UILayoutPriority)contentHuggingPriorityForAxis:(UILayoutConstraintAxis)axis NS_AVAILABLE_IOS(6_0);- (void)setContentHuggingPriority:(UILayoutPriority)priority forAxis:(UILayoutConstraintAxis)axis NS_AVAILABLE_IOS(6_0);- (UILayoutPriority)contentCompressionResistancePriorityForAxis:(UILayoutConstraintAxis)axis NS_AVAILABLE_IOS(6_0);- (void)setContentCompressionResistancePriority:(UILayoutPriority)priority forAxis:(UILayoutConstraintAxis)axis NS_AVAILABLE_IOS(6_0);
2 NSLayoutConstraint只能修改constant
NSLayoutConstraint即自動布局的約束類,它是自動布局的關鍵之一。該類有如下屬性我們需要重點關注。
NS_CLASS_AVAILABLE_IOS(6_0) @interface NSLayoutConstraint : NSObject // other code @property UILayoutPriority priority; @property BOOL shouldBeArchived; /* accessors firstItem.firstAttribute {==,=} secondItem.secondAttribute * multiplier + constant */ @property (readonly, assign) id firstItem; @property (readonly) NSLayoutAttribute firstAttribute; @property (readonly) NSLayoutRelation relation; @property (nullable, readonly, assign) id secondItem; @property (readonly) NSLayoutAttribute secondAttribute; @property (readonly) CGFloat multiplier; /* Unlike the other properties, the constant may be modified after constraint creation. Setting the constant on an existing constraint performs much better than removing the constraint and adding a new one that's just like the old but for having a new constant. */ @property CGFloat constant; /* The receiver may be activated or deactivated by manipulating this property. Only active constraints affect the calculated layout. Attempting to activate a constraint whose items have no common ancestor will cause an exception to be thrown. Defaults to NO for newly created constraints. */ @property (getter=isActive) BOOL active NS_AVAILABLE(10_10, 8_0); // other code @end
布局公式:firstItem.firstAttribute {==,<=,>=} secondItem.secondAttribute * multiplier + constant
解釋:firstItem與secondItem分別是界面中受約束的視圖與被參照的視圖。
注意:當使用代碼來修改約束時,只能修改約束的常量值constant。一旦創建了約束,其他只讀屬性都是無法修改的,特別要注意的是比例系數multiplier也是只讀的。
Masonry是基於NSLayoutConstraint等類的封裝,也正是如此,我們在調用- (NSArray *)mas_updateConstraints:(void(^)(MASConstraintMaker *))block的時候也只能更新NSLayoutConstraint中的@property CGFloat constant。
在MASViewConstraint找到如下代碼可以佐證:
- (void)install { // other code MASLayoutConstraint *existingConstraint = nil; if (self.updateExisting) { //如果是update,則去匹配對應的existingConstraint existingConstraint = [self layoutConstraintSimilarTo:layoutConstraint]; } if (existingConstraint) { //找到了existingConstraint,最終也只更新了existingConstraint.constant // just update the constant existingConstraint.constant = layoutConstraint.constant; self.layoutConstraint = existingConstraint; } else { //沒有找到existingConstraint,添加一個新的約束 [self.installedView addConstraint:layoutConstraint]; self.layoutConstraint = layoutConstraint; [firstLayoutItem.mas_installedConstraints addObject:self]; }}// 除了constant,其它都一樣的約束是Similar約束- (MASLayoutConstraint *)layoutConstraintSimilarTo:(MASLayoutConstraint *)layoutConstraint { // check if any constraints are the same apart from the only mutable property constant // go through constraints in reverse as we do not want to match auto-resizing or interface builder constraints // and they are likely to be added first. for (NSLayoutConstraint *existingConstraint in self.installedView.constraints.reverseObjectEnumerator) { if (![existingConstraint isKindOfClass:MASLayoutConstraint.class]) continue; if (existingConstraint.firstItem != layoutConstraint.firstItem) continue; if (existingConstraint.secondItem != layoutConstraint.secondItem) continue; if (existingConstraint.firstAttribute != layoutConstraint.firstAttribute) continue; if (existingConstraint.secondAttribute != layoutConstraint.secondAttribute) continue; if (existingConstraint.relation != layoutConstraint.relation) continue; if (existingConstraint.multiplier != layoutConstraint.multiplier) continue; if (existingConstraint.priority != layoutConstraint.priority) continue; return (id)existingConstraint; } return nil;}
樣例:
@interface ViewController ()@property (nonatomic, strong) UILabel *lbl;@property (nonatomic, strong) UIButton *btn;@end@implementation ViewController- (void)viewDidLoad { [super viewDidLoad]; // Do any additional setup after loading the view, typically from a nib. self.btn = [UIButton buttonWithType:UIButtonTypeCustom]; self.btn.backgroundColor = [UIColor blueColor]; [self.btn setTitle:@"按鈕" forState:UIControlStateNormal]; [self.btn addTarget:self action:@selector(onTest:) forControlEvents:UIControlEventTouchDown]; [self.view addSubview:self.btn]; [self.btn mas_updateConstraints:^(MASConstraintMaker *make) { make.top.equalTo(self.view).offset(200); make.centerX.equalTo(self.view); make.size.mas_equalTo(CGSizeMake(100, 33)); }]; self.lbl = [[UILabel alloc] init]; self.lbl.text = @"一個label"; self.lbl.backgroundColor = [UIColor redColor]; self.lbl.textAlignment = NSTextAlignmentCenter; [self.view addSubview:self.lbl]; [self.lbl mas_updateConstraints:^(MASConstraintMaker *make) { make.top.equalTo(self.view).offset(300); make.centerX.equalTo(self.view); make.size.equalTo(self.btn); }];}- (void)onTest:(id)sender{ [self.lbl mas_updateConstraints:^(MASConstraintMaker *make) { make.size.mas_equalTo(CGSizeMake(200, 100)); }];}@end
當按鈕被按下時,控制台出現如下警告
2016-08-03 18:49:13.110 layout[47924:2886276] Unable to simultaneously satisfy constraints. Probably at least one of the constraints in the following list is one you don't want. Try this: (1) look at each constraint and try to figure out which you don't expect; (2) find the code that added the unwanted constraint or constraints and fix it. ( "", "", "" ) Will attempt to recover by breaking constraintMake a symbolic breakpoint at UIViewAlertForUnsatisfiableConstraints to catch this in the debugger. The methods in the UIConstraintBasedLayoutDebugging category on UIView listed in may also be helpful. 2016-08-03 18:49:13.111 layout[47924:2886276] Unable to simultaneously satisfy constraints. Probably at least one of the constraints in the following list is one you don't want. Try this: (1) look at each constraint and try to figure out which you don't expect; (2) find the code that added the unwanted constraint or constraints and fix it. ( "", "", "" ) Will attempt to recover by breaking constraintMake a symbolic breakpoint at UIViewAlertForUnsatisfiableConstraints to catch this in the debugger. The methods in the UIConstraintBasedLayoutDebugging category on UIView listed in may also be helpful.
原因是,lbl創建時其size約束是make.size.equalTo(self.btn),但btn被點擊時,企圖去update size約束為make.size.mas_equalTo(CGSizeMake(200, 100)),然而無法找到existingConstraint,因此實際上是額外添加了一個約束make.size.mas_equalTo(CGSizeMake(200, 100))出現了布局沖突。
這件事可以這麼看,NSLayoutConstraint只能修改constant決定了mas_updateConstraints的實現方式為:找到既有約束就去改變constant找不到既有約束就添加新約束。
3 被Masonry布局的view一定要與比較view有共同的祖先view
這句話比較拗口,其中涉及三類view,解釋如下。
被Masonry布局的view:執行了- (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *make))block、- (NSArray *)mas_updateConstraints:(void(^)(MASConstraintMaker *make))block 、- (NSArray *)mas_remakeConstraints:(void(^)(MASConstraintMaker *make))block等函數的view。
比較view:以上3函數block塊裡面出現的view。
共同的祖先view:【1】和【2】的共同祖先view。
樣例1:
- (void)sampleCode{ UIView *v0 = [UIView new]; [self.view addSubview:v0]; UIView *v1 = [UIView new]; [v0 addSubview:v1]; [v1 mas_makeConstraints:^(MASConstraintMaker *make) { make.size.mas_equalTo(CGSizeMake(10, 10)); }]; UIView *v2 = [UIView new]; [v0 addSubview:v2]; [v2 mas_makeConstraints:^(MASConstraintMaker *make) { make.size.equalTo(v1); }];}
針對如下代碼塊來說
UIView *v2 = [UIView new];[v0 addSubview:v2];[v2 mas_makeConstraints:^(MASConstraintMaker *make) { make.size.equalTo(v1);}];
v2是被Masonry布局的view,v1是比較view,v0是共同的祖先view。
樣例2:
@implementation AutoLayoutViewController- (void)viewDidLoad{ [super viewDidLoad]; [self useMasonryWithoutSuperView];}- (void)useMasonryWithoutSuperView{ UIView *masView = [UIView new]; [masView mas_makeConstraints:^(MASConstraintMaker *make) { make.center.equalTo(self.view); }];}@end
以上代碼執行時會crash,crash log如下:
2016-08-04 00:52:47.542 CommonTest[1731:22953] *** Assertion failure in -[MASViewConstraint install], /Users/shuncheng/SourceCode/SampleCode/AutoLayout/Pods/Masonry/Masonry/MASViewConstraint.m:338 2016-08-04 00:52:47.548 CommonTest[1731:22953] *** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'couldn't find a common superview for and '
crash的原因顯而易見,即,masView(被Masonry布局的view)與self.view(比較view)沒有共同祖先view,因為masView沒有父view,所以它和self.view必然沒有共同祖先view。
被Masonry布局的view沒有添加到superview上其實比較容易被發現,最怕的是出現如樣例3一樣的鬼畜情況。
樣例3:
@implementation AutoLayoutViewController- (void)viewDidLoad{ [super viewDidLoad]; [self sampleCode];}- (void)sampleCode{ AutoLayoutViewController * __weak weakSelf = self; [fooNetworkModel fetchData:^{ AutoLayoutViewController * self = weakSelf; [AutoLayoutViewController showSampleViewAtView:self.view]; }];}+ (void)showSampleViewAtView:(UIView *)view{ UIView *v1 = [UIView new]; [view addSubview:v1]; [v1 mas_makeConstraints:^(MASConstraintMaker *make) { make.size.mas_equalTo(CGSizeMake(10, 10)); }]; UIView *v2 = [UIView new]; [view addSubview:v2]; [v2 mas_makeConstraints:^(MASConstraintMaker *make) { make.size.equalTo(v1); }];}@end
以上代碼通常不會出錯,但是一種異常情況是:在AutoLayoutViewController析構後,網絡數據返回,此時AutoLayoutViewController * self = weakSelf則self == nil。執行[AutoLayoutViewController showSampleViewAtView:nil],則會出現【樣例2】一樣的crash。
原因是:v1和v2都沒有添加到view上去(因為view為空)所以make.size.equalTo(v1)會出錯(v1和v2沒有共同的父view)。由此也引申到weakSelf的副作用,即必須要確保weakSelf是nil時,執行邏輯完全沒有問題(目前已經兩次被坑)。
4 不要被update迷惑
這裡說的update有兩層含義:
UIView的方法- (void)updateConstraints NS_AVAILABLE_IOS(6_0)
Masonry的方法- (NSArray *)mas_updateConstraints:(void(^)(MASConstraintMaker *make))block
這裡先來討論一下UIView的- (void)updateConstraints方法。
- (void)updateConstraints方法是用來更新view約束的,它有一個常見的使用場景——批量更新約束。比如你的多個約束是由多個不同的property決定,每次設置property都會直接更新局部約束。這樣效率不高。不如直接override- (void)updateConstraints方法,在方面裡面對property進行判斷,每次設置property的時候調用一下- (void)setNeedsUpdateConstraints。偽代碼如下:
優化前:
@implementation AutoLayoutView- (void)setFactor1:(NSInteger)factor1{ _factor1 = factor1; if (_factor1滿足條件) { 更新約束1 }}- (void)setFactor2:(NSInteger)factor2{ _factor2 = factor2; if (_factor2滿足條件) { 更新約束2 }}- (void)setFactor3:(NSInteger)factor3{ _factor3 = factor3; if (_factor3滿足條件) { 更新約束3 }}@end
優化後:
@implementation AutoLayoutView- (void)setFactor1:(NSInteger)factor1{ _factor1 = factor1; [self setNeedsUpdateConstraints];}- (void)setFactor2:(NSInteger)factor2{ _factor2 = factor2; [self setNeedsUpdateConstraints];}- (void)setFactor3:(NSInteger)factor3{ _factor3 = factor3; [self setNeedsUpdateConstraints];}- (void)updateConstraints{ if (self.factor1滿足) { 更新約束1 } if (self.factor2滿足) { 更新約束2 } if (self.factor3滿足) { 更新約束3 } [super updateConstraints];}@end
注意:一種有誤區的寫法是在- (void)updateConstraints方法中進行初次constraint設置,這是不被推薦的。推薦的寫法是在init或者viewDidLoad中進行view的初次constraint設置。
Masonry的方法- (NSArray *)mas_updateConstraints:(void(^)(MASConstraintMaker *make))block我們在第二節已經討論過了。剛接觸自動布局和Masonry的同學很容易跟著感覺在- (void)updateConstraints函數裡面調用Masonry的- (NSArray *)mas_updateConstraints:(void(^)(MASConstraintMaker *make))block。實際上兩者並沒有必然聯系。大多數情況在- (void)updateConstraints裡面調用- (NSArray *)mas_updateConstraints:(void(^)(MASConstraintMaker *make))block很有可能產生布局沖突。
樣例
// 頭文件typedef NS_ENUM(NSUInteger, AutoLayoutType) { HorizontalLayout, VerticalLayout,};@interface AutoLayoutView : UIView@property (nonatomic, strong) UILabel *name;@property (nonatomic, strong) UILabel *address;@property (nonatomic, assign) AutoLayoutType layoutType;@end// 實現文件@implementation AutoLayoutView- (instancetype)initWithFrame:(CGRect)frame{ if (self = [super initWithFrame:frame]) { _name = [[UILabel alloc] init]; [self addSubview:_name]; _address = [[UILabel alloc] init]; [self addSubview:_address]; [_name mas_updateConstraints:^(MASConstraintMaker *make) { make.left.top.equalTo(self); }]; } return self;}- (void)updateConstraints{ if (self.layoutType == HorizontalLayout) { // // 此處誤用mas_updateConstraints [self.address mas_updateConstraints:^(MASConstraintMaker *make) { make.top.equalTo(self.name); make.left.equalTo(self.name.mas_right).offset(10); }]; } else { // 此處誤用mas_updateConstraints [self.address mas_updateConstraints:^(MASConstraintMaker *make) { make.left.equalTo(self.name); make.top.equalTo(self.name.mas_bottom).offset(10); }]; } [super updateConstraints];}- (void)setLayoutType:(AutoLayoutType)layoutType{ _layoutType = layoutType; [self setNeedsUpdateConstraints];}@end// 外部調用代碼- (void)sampleCode{ AutoLayoutView *view = [[AutoLayoutView alloc] init]; view.name.text = @"name"; view.address.text = @"address"; [self.view addSubview:view]; [view mas_makeConstraints:^(MASConstraintMaker *make) { make.center.equalTo(self.view); make.size.mas_equalTo(CGSizeMake(200, 300)); }]; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ view.layoutType = VerticalLayout; //修改布局方式後,出現布局沖突 });}
5 總結
本文梳理了一下自動布局和Masonry使用的誤區。在基本概念沒搞清的情況下,很容易犯錯。總結起來就如下4點:
理解自身內容尺寸約束與抗壓抗拉
NSLayoutConstraint只能修改constant和- (NSArray *)mas_updateConstraints:(void(^)(MASConstraintMaker *make))block實現細節之間的關系
被Masonry布局的view一定要與比較view有共同的祖先view
區分UIView的- (void)updateConstraints方法和- (NSArray *)mas_updateConstraints:(void(^)(MASConstraintMaker *make))block
6 參考資料
WWDC-Mysteries of Auto Layout, Part 2