zoukankan      html  css  js  c++  java
  • Masonry使用注意事项

    1 理解自身内容尺寸约束与抗压抗拉

    自身内容尺寸约束:一般来说,要确定一个视图的精确位置,至少需要4个布局约束(以确定水平位置x、垂直位置y、宽度w和高度h)。但是,某些用来展现内容的用户控件,例如文本控件UILabel、按钮UIButton、图片视图UIImageView等,它们具有自身内容尺寸(Intrinsic Content Size),此类用户控件会根据自身内容尺寸添加布局约束。也就是说,如果开发者没有显式给出其宽度或者高度约束,则其自动添加的自身内容约束将会起作用。因此看似“缺失”约束,实际上并非如此。

    关于自身内容尺寸约束,简单来说就是某些用来展现内容的用户控件,它们会根据自身内容尺寸添加布局约束。

    自身内容尺寸约束的抗挤压与抗拉抻效果。弹簧会有自身固有长度,当有外力作用时,弹簧会抵抗外力作用,尽量接近固有长度。

    抗拉抻:当外力拉长弹簧时,弹簧长度大于固有长度,且产生向内收的力阻止外力拉抻,且尽量维持长度接近自身固有长度。

    抗挤压:当外力挤压弹簧时,弹簧长度小于固有长度,且产生向外顶的力阻止外力挤压,且尽量维持长度接近自身固有长度。

    关于抗压抗拉,就是布局冲突需要牺牲某些控件的某些宽度或者高度约束时,抗压高的控件越不容易被压缩,抗拉高的控件越不容易被拉升。即自身布局对抗外界布局的能力。

    样例:

    一种常见的业务场景是用户修改地址,在输入新地址之前先读取用户之前的地址作为填充。UI实现是水平平行的UILabel和UITextField。 代码实现如下:

     
    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表现正常。

    但,当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

  • 相关阅读:
    LeetCode Single Number
    Leetcode Populating Next Right Pointers in Each Node
    LeetCode Permutations
    Leetcode Sum Root to Leaf Numbers
    LeetCode Candy
    LeetCode Sort List
    LeetCode Remove Duplicates from Sorted List II
    LeetCode Remove Duplicates from Sorted List
    spring MVC HandlerInterceptorAdapter
    yum
  • 原文地址:https://www.cnblogs.com/MasterPeng/p/5825086.html
Copyright © 2011-2022 走看看