Masonry使用注意事項

1 理解自身內容尺寸約束與抗壓抗拉網絡

自身內容尺寸約束:通常來講,要肯定一個視圖的精確位置,至少須要4個佈局約束(以肯定水平位置x、垂直位置y、寬度w和高度h)。可是,某些用來展示內容的用戶控件,例如文本控件UILabel、按鈕UIButton、圖片視圖UIImageView等,它們具備自身內容尺寸(Intrinsic Content Size),此類用戶控件會根據自身內容尺寸添加布局約束。也就是說,若是開發者沒有顯式給出其寬度或者高度約束,則其自動添加的自身內容約束將會起做用。所以看似「缺失」約束,實際上並不是如此。app

關於自身內容尺寸約束,簡單來講就是某些用來展示內容的用戶控件,它們會根據自身內容尺寸添加布局約束。ide

自身內容尺寸約束的抗擠壓與抗拉抻效果。彈簧會有自身固有長度,當有外力做用時,彈簧會抵抗外力做用,儘可能接近固有長度。函數

抗拉抻:當外力拉長彈簧時,彈簧長度大於固有長度,且產生向內收的力阻止外力拉抻,且儘可能維持長度接近自身固有長度。佈局

抗擠壓:當外力擠壓彈簧時,彈簧長度小於固有長度,且產生向外頂的力阻止外力擠壓,且儘可能維持長度接近自身固有長度。fetch

關於抗壓抗拉,就是佈局衝突須要犧牲某些控件的某些寬度或者高度約束時,抗壓高的控件越不容易被壓縮,抗拉高的控件越不容易被拉昇。即自身佈局對抗外界佈局的能力。優化

樣例:ui

一種常見的業務場景是用戶修改地址,在輸入新地址以前先讀取用戶以前的地址做爲填充。UI實現是水平平行的UILabel和UITextField。 代碼實現以下:this

 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
- (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表現正常。atom

但,當houseNumberTextField.text = [self aLongAddress]時會出現address UILabel被擠壓掉的狀況,以下圖所示:

1.jpg

緣由是address Label的水平抗壓縮沒有設置。

在address Label建立的時候添加以下代碼[address setContentCompressionResistancePriority:UILayoutPriorityRequired forAxis:UILayoutConstraintAxisHorizontal]則顯示正常。

另,在某些狀況下存在view被拉昇,極有多是沒有設置抗拉昇,此處不一一列舉。

附,抗壓抗拉相關API以下:

1
- (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即自動佈局的約束類,它是自動佈局的關鍵之一。該類有以下屬性咱們須要重點關注。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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找到以下代碼能夠佐證:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
- (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;}

樣例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@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

當按鈕被按下時,控制檯出現以下警告

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
- (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);
     }];}

針對以下代碼塊來講

1
2
UIView *v2 = [UIView  new ];[v0 addSubview:v2];[v2 mas_makeConstraints:^(MASConstraintMaker *make) {
     make.size.equalTo(v1);}];

v2是被Masonry佈局的view,v1是比較view,v0是共同的祖先view。

樣例2:

1
2
3
4
5
6
7
@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以下:

1
2
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@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。僞代碼以下:

優化前:

1
2
3
4
5
6
7
8
9
10
11
12
13
@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

優化後:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@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頗有可能產生布局衝突。

樣例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// 頭文件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使用注意事項來進行說明。

本文梳理了一下自動佈局和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

相關文章
相關標籤/搜索