项目干货挖掘4——如何优雅地使用AutoLayout自动布局

附注:标题中“优雅地”三个字体现了作者对逼格的不懈追求。呵呵,装个逼,下面开始正题。


自动布局简介

一开始iOS写界面布局是通过frame固定位置及尺寸的。因为当时iPhone手机的屏幕尺寸是统一的,但后来随着iPhone手机尺寸变多,苹果新增了autoresizingMask这个东西,它其实就是UIView的一个属性,意为“自动伸缩”。通过它能进行简单的自动布局,主要是完成父子视图之间的自动布局,但对于非父子视图等比较复杂的视图之间的自动布局,就显得力不从心了。随后,苹果便推出了功能更加强大的AutoLayout,它几乎能完成任何复杂关系的布局。这便是自动布局的发展简介。

UIView的autoresizingMask属性的定义:

typedef NS_OPTIONS(NSUInteger, UIViewAutoresizing) {
    UIViewAutoresizingNone                 = 0,
    UIViewAutoresizingFlexibleLeftMargin   = 1 << 0,
    UIViewAutoresizingFlexibleWidth        = 1 << 1,
    UIViewAutoresizingFlexibleRightMargin  = 1 << 2,
    UIViewAutoresizingFlexibleTopMargin    = 1 << 3,
    UIViewAutoresizingFlexibleHeight       = 1 << 4,
    UIViewAutoresizingFlexibleBottomMargin = 1 << 5
};

可以看到它是支持位运算符的枚举,也即可以像下面这样使用。它代表该视图greenView相对于父视图,它的宽和高是可以伸缩的,并非固定的。

greenView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;

AutoLayout的约束——NSLayoutConstraint

一句话解释AutoLayout是通过给某视图添加“约束”来布局的,这些“约束”确定了该视图与其他视图的位置关系及自身尺寸等问题。

好了,现在假如我们想把一个UIView以这样的形式布局:该UIView的顶部距离父视图顶部100px,左边距离父视图的左边20px,右边距离父视图也20px,而其高度为300px。
AutoLayout的代码要这样写:

    UIView *greenView = [[UIView alloc] init];
    greenView.backgroundColor = [UIColor greenColor];
    [self.view addSubview:greenView];
    
    
    // greenView上边距view上边80
    NSLayoutConstraint *constraint_Top = [NSLayoutConstraint constraintWithItem:greenView
                                                                      attribute:NSLayoutAttributeTop
                                                                      relatedBy:NSLayoutRelationEqual
                                                                         toItem:self.view
                                                                      attribute:NSLayoutAttributeTop
                                                                     multiplier:1.f
                                                                       constant:80.f];
    // greenView左边距view左边20
    NSLayoutConstraint *constraint_Left = [NSLayoutConstraint constraintWithItem:greenView
                                                                  attribute:NSLayoutAttributeLeft
                                                                  relatedBy:NSLayoutRelationEqual
                                                                     toItem:self.view
                                                                  attribute:NSLayoutAttributeLeft
                                                                 multiplier:1.f
                                                                   constant:20.f];
    // greenView右边距view右边20
    NSLayoutConstraint *constraint_Right = [NSLayoutConstraint constraintWithItem:greenView
                                                                       attribute:NSLayoutAttributeRight
                                                                       relatedBy:NSLayoutRelationEqual
                                                                          toItem:self.view
                                                                       attribute:NSLayoutAttributeRight
                                                                      multiplier:1.f
                                                                        constant:-20.f];
    // greenView的高度为300
    NSLayoutConstraint *constraint_Height = [NSLayoutConstraint constraintWithItem:greenView
                                                                         attribute:NSLayoutAttributeHeight
                                                                         relatedBy:NSLayoutRelationEqual
                                                                            toItem:nil
                                                                         attribute:0
                                                                        multiplier:1.f
                                                                          constant:400.f];
    greenView.translatesAutoresizingMaskIntoConstraints = NO;
    [greenView addConstraint:constraint_Height];
    [self.view addConstraints:@[constraint_Top, constraint_Left, constraint_Right]];

看完上面的代码你也许该说该“What The Fuck?!”了,心想,写个这么简单的布局竟然要写这么一大段代码吗?
确实,如果在项目实践中,直接使用苹果提供的这一套API进行开发,简直是不可想象的,估计没人会这么做吧。但我们可以基于此进行封装,使其提供的API对于程序员来说语义明了,而且最好短小精悍。

不过在进行封装前,我们还得先把一些东西搞清楚。请继续往下看。


约束应该添加给哪个UIView?

可以注意到上面的代码里,与父视图有相对关系的“约束”是添加给父视图self.view的。而表示greenView高度的约束却是添加给greenView的。这又是为什么?这里面有什么道理呢?
其中的道理是:如果是设置宽、高的固定长度,而和别的视图没有相对关系,就把该约束添加给自己;若该约束是和别的视图的一种相对关系,则添加给两个视图的最小公共父视图。

上面的例子里,greenView的高度只是设置自己的高度,和其他视图并无关系,所以把约束添加给greenView自己,而其他三个约束分别代表和父视图self.viewtop/left/right的三个方向边距,是和别的视图有相对关系的,所以要添加给两者中的公共父视图,即self.view

拷贝几张图来体会:

项目干货挖掘4——如何优雅地使用AutoLayout自动布局_第1张图片
01.png
项目干货挖掘4——如何优雅地使用AutoLayout自动布局_第2张图片
02.png
项目干货挖掘4——如何优雅地使用AutoLayout自动布局_第3张图片
03.png

开始封装AutoLayout

我们进行布局时,常常描述为:bView的top距离aView的bottom60px。或者bView的left距离aView的right20px。
总之它的思维方式是bView的top/bottom/left/right和aView的top/bottom/left/right的一种距离关系。
我们新建一个AutoLayoutObject类来表示“约束”。

** AutoLayoutObject.h **

#import 
#import 

#define LAYEqual(a1, a2, m, c)          [a1 addEqualConstraint:a2 multiplier:m constant:c]
#define LAYLessThan(a1, a2, m, c)       [a1 addLessThanConstraint:a2 multiplier:m constant:c]
#define LAYMoreThan(a1, a2, m, c)       [a1 addMoreThanConstraint:a2 multiplier:m constant:c]
#define LAYEqualC(a, c)                 [a addEqualConstant:c]
#define LAYLessThanC(a, c)              [a addLessThanConstant:c]
#define LAYMoreThanC(a, c)              [a addMoreThanConstant:c]
#define LAYM(a, m)              [a addEqualMultiplier:m]



@interface AutoLayoutObject : NSObject


@property (nonatomic, strong)id                         item; // 某视图
@property (nonatomic, assign)NSLayoutAttribute          attribute; // 属性,top/bottom/left/right/centerX/centerY等
@property (nonatomic, assign)NSLayoutRelation           relation; // 等于 or 小于等于 or 大于等于
@property (nonatomic, assign)CGFloat                    multiplier; // 伸缩形变量
@property (nonatomic, assign)CGFloat                    constant; // 常量,代表长度或距离


// 添加等于某固定长度的约束 (某UIView的高为200)
- (void)addEqualConstant:(CGFloat)constant;

// 添加小于等于某固定长度的约束 (某UIView的高最大为200)
- (void)addLessThanConstant:(CGFloat)constant;

// 添加大于等于某固定长度的约束 (某UIView的高最小为200)
- (void)addMoreThanConstant:(CGFloat)constant;

// 添加两者等于的约束 (bView的top和aView的bottom的间距等于20)
- (void)addEqualConstraint:(AutoLayoutObject *)secondLayoutObj multiplier:(CGFloat)multiplier constant:(CGFloat)constant;

// 添加A小于等于B的约束 (bView的top和aView的bottom的间距最大为20)
- (void)addLessThanConstraint:(AutoLayoutObject *)secondLayoutObj multiplier:(CGFloat)multiplier constant:(CGFloat)constant;

// 添加A大于等于B的约束 (bView的top和aView的bottom的间距最小为20)
- (void)addMoreThanConstraint:(AutoLayoutObject *)secondLayoutObj multiplier:(CGFloat)multiplier constant:(CGFloat)constant;



@end

** AutoLayoutObject.m **

#import "AutoLayoutObject.h"
#import "UIView+AutoLayout.h"



@implementation AutoLayoutObject


- (id)init
{
    self = [super init];
    if(self)
    {
        // 赋初值(默认值)
        _item = nil;
        _attribute = NSLayoutAttributeNotAnAttribute;
        _relation = NSLayoutRelationEqual; // 默认是相等
        _multiplier = 1.f;
        _constant = 0.f;
    }
    
    return self;
}



// 添加等于某固定长度的约束 (某UIView的高为200)
- (void)addEqualConstant:(CGFloat)constant
{
    
    UIView *superView = (UIView *)[self.item superview];
    
    switch (self.attribute)
    {
        // left/right/top/bottom/centerX/centerY这些在不指定secondItem情况下,是firstItem以同样的状态把父类作为secondItem的。
        case NSLayoutAttributeLeft:{
            [self addEqualConstraint:superView.left multiplier:1.f constant:constant];
            break;
        }
        case NSLayoutAttributeRight:{
            [self addEqualConstraint:superView.right multiplier:1.f constant:constant];
            break;
        }
        case NSLayoutAttributeTop:{
            [self addEqualConstraint:superView.top multiplier:1.f constant:constant];
            break;
        }
        case NSLayoutAttributeBottom:{
            [self addEqualConstraint:superView.bottom multiplier:1.f constant:constant];
            break;
        }
        case NSLayoutAttributeCenterX:{
            [self addEqualConstraint:superView.centerX multiplier:1.f constant:constant];
            break;
        }
        case NSLayoutAttributeCenterY:{
            [self addEqualConstraint:superView.centerY multiplier:1.f constant:constant];
            break;
        }
        
        // 设width 和 heigth为常量值
        case NSLayoutAttributeWidth:
        case NSLayoutAttributeHeight:{
            [self addEqualConstraint:nil multiplier:1.f constant:constant];
            break;
        }
            
            
        default:
            break;
    }
}


// 添加小于等于某固定长度的约束 (某UIView的高最大为200)
- (void)addLessThanConstant:(CGFloat)constant
{
    UIView *superView = (UIView *)[self.item superview];
    
    switch (self.attribute)
    {
            // left/right/top/bottom/centerX/centerY这些在不指定secondItem情况下,是firstItem以同样的状态把父类作为secondItem的。
        case NSLayoutAttributeLeft:{
            [self addLessThanConstraint:superView.left multiplier:1.f constant:constant];
            break;
        }
        case NSLayoutAttributeRight:{
            [self addLessThanConstraint:superView.right multiplier:1.f constant:constant];
            break;
        }
        case NSLayoutAttributeTop:{
            [self addLessThanConstraint:superView.top multiplier:1.f constant:constant];
            break;
        }
        case NSLayoutAttributeBottom:{
            [self addLessThanConstraint:superView.bottom multiplier:1.f constant:constant];
            break;
        }
        case NSLayoutAttributeCenterX:{
            [self addLessThanConstraint:superView.centerX multiplier:1.f constant:constant];
            break;
        }
        case NSLayoutAttributeCenterY:{
            [self addLessThanConstraint:superView.centerY multiplier:1.f constant:constant];
            break;
        }
            
            // 设width 和 heigth为常量值
        case NSLayoutAttributeWidth:
        case NSLayoutAttributeHeight:{
            [self addLessThanConstraint:nil multiplier:1.f constant:constant];
            break;
        }
            
            
        default:
            break;
    }
}


// 添加大于等于某固定长度的约束 (某UIView的高最小为200)
- (void)addMoreThanConstant:(CGFloat)constant
{
    // 省略...
}


// 添加两者等于的约束 (bView的top和aView的bottom的间距等于20)
- (void)addEqualConstraint:(AutoLayoutObject *)secondLayoutObj multiplier:(CGFloat)multiplier constant:(CGFloat)constant
{
    [self addGenericConstraint:secondLayoutObj multiplier:multiplier constant:constant relation:NSLayoutRelationEqual];
}


// 添加A小于等于B的约束 (bView的top和aView的bottom的间距最大为20)
- (void)addLessThanConstraint:(AutoLayoutObject *)secondLayoutObj multiplier:(CGFloat)multiplier constant:(CGFloat)constant
{
    [self addGenericConstraint:secondLayoutObj multiplier:multiplier constant:constant relation:NSLayoutRelationLessThanOrEqual];
}


// 添加A大于等于B的约束 (bView的top和aView的bottom的间距最小为20)
- (void)addMoreThanConstraint:(AutoLayoutObject *)secondLayoutObj multiplier:(CGFloat)multiplier constant:(CGFloat)constant
{
    [self addGenericConstraint:secondLayoutObj multiplier:multiplier constant:constant relation:NSLayoutRelationGreaterThanOrEqual];
}



// 全能方法
- (void)addGenericConstraint:(AutoLayoutObject *)secondLayoutObj multiplier:(CGFloat)multiplier constant:(CGFloat)constant relation:(NSLayoutRelation)relation
{
    if(!self.item){
        return;
    }

    // 确定出视图aView和bView的最小公共父类ancestorView,最终要给该视图添加约束
    UIView *aView = self.item;
    UIView *bView = secondLayoutObj.item;
    UIView *ancestorView;
    if([aView isDescendantOfView:bView]){
        ancestorView = bView;
    }else if([bView isDescendantOfView:aView]){
        ancestorView = aView;
    }else if([aView isDescendantOfView:bView.superview]){
        ancestorView = bView.superview;
    }else if([bView isDescendantOfView:aView.superview]){
        ancestorView = aView.superview;
    }else{
        ancestorView = aView.superview;
    }
    
    NSLayoutConstraint *layoutConstraint = [NSLayoutConstraint constraintWithItem:self.item
                                                                        attribute:self.attribute
                                                                        relatedBy:relation
                                                                           toItem:secondLayoutObj.item
                                                                        attribute:secondLayoutObj.attribute
                                                                       multiplier:multiplier
                                                                         constant:constant];
    
    [ancestorView addConstraint:layoutConstraint];
}
@end


但是为了得到某UIView的这个自定义的“约束”对象,我们得提前新建一个UIView的分类:

#import 
#import "AutoLayoutObject.h"


@interface UIView (AutoLayout)


+ (id)create;


- (AutoLayoutObject *)top;
- (AutoLayoutObject *)bottom;
- (AutoLayoutObject *)left;
- (AutoLayoutObject *)right;
- (AutoLayoutObject *)leading;
- (AutoLayoutObject *)trailing;

- (AutoLayoutObject *)width;
- (AutoLayoutObject *)height;
- (AutoLayoutObject *)centerX;
- (AutoLayoutObject *)centerY;
- (AutoLayoutObject *)baseline;
- (AutoLayoutObject *)notAnAttribute;



- (void)removeAllConstraint;

- (void)removeAutoLayoutObject:(AutoLayoutObject *)constraint;


@end


UIView+AutoLayout中我们不仅写了返回view各种“约束”对象的接口方法,而且也提供了初始化UIView的类方法,这不但使初始化一个UIView更简洁,而且主要的目的是为了关闭view的autoresizingMask属性。

#import "UIView+AutoLayout.h"

@implementation UIView (AutoLayout)



+ (id)create
{
    UIView *aView = [[self alloc] init];
    if(aView){
        aView.translatesAutoresizingMaskIntoConstraints = NO; // 默认是支持autoresizingMask的,但它可能会和autoLayout冲突,所以我们关闭autoresizingMask属性。
    }
    
    return aView;
}


- (AutoLayoutObject *)top
{
    return [self autoLayoutObjWithAttribute:NSLayoutAttributeTop];
}

- (AutoLayoutObject *)bottom
{
    return [self autoLayoutObjWithAttribute:NSLayoutAttributeBottom];
}

- (AutoLayoutObject *)left
{
    return [self autoLayoutObjWithAttribute:NSLayoutAttributeLeft];
}

- (AutoLayoutObject *)right
{
    return [self autoLayoutObjWithAttribute:NSLayoutAttributeRight];
}

- (AutoLayoutObject *)leading
{
    return [self autoLayoutObjWithAttribute:NSLayoutAttributeLeading];
}

- (AutoLayoutObject *)trailing
{
    return [self autoLayoutObjWithAttribute:NSLayoutAttributeTrailing];
}


- (AutoLayoutObject *)width
{
    return [self autoLayoutObjWithAttribute:NSLayoutAttributeWidth];
}

- (AutoLayoutObject *)height
{
    return [self autoLayoutObjWithAttribute:NSLayoutAttributeHeight];
}

- (AutoLayoutObject *)centerX
{
    return [self autoLayoutObjWithAttribute:NSLayoutAttributeCenterX];
}

- (AutoLayoutObject *)centerY
{
    return [self autoLayoutObjWithAttribute:NSLayoutAttributeCenterY];
}

- (AutoLayoutObject *)baseline
{
    return [self autoLayoutObjWithAttribute:NSLayoutAttributeBaseline];
}

- (AutoLayoutObject *)notAnAttribute
{
    return [self autoLayoutObjWithAttribute:NSLayoutAttributeNotAnAttribute];
}



- (void)removeAllConstraint
{
    [self removeAllConstraint];
}

- (void)removeAutoLayoutObject:(AutoLayoutObject *)autoLayoutObj
{
    for(NSLayoutConstraint *constranit in self.constraints)
    {
        if(autoLayoutObj.attribute == constranit.firstAttribute){
            [self removeConstraint:constranit];
        }
    }
}



// 生成当前UIView的autoLayoutObj对象
- (AutoLayoutObject *)autoLayoutObjWithAttribute:(NSLayoutAttribute)attribute
{
    AutoLayoutObject *autoLayoutObj = [[AutoLayoutObject alloc] init];
    autoLayoutObj.item = self;
    autoLayoutObj.attribute = attribute;
    
    return autoLayoutObj;
}


@end

以上AutoLayoutObject类和UIView分类UIView+AutoLayout是便是完成自动布局的全部东西。

好了,我们通过这个封装后的自动布局来完成本文开头的那个布局。是不是精炼了很多。(我们把已然封装后提供的接口方法再次进行了宏定义,最终我们使用与布局方法相对应的宏来进行布局,这样更简短,更明了。)

    UIView *greenViewiew = [UIView create];
    greenViewiew.backgroundColor = [UIColor greenColor];
    [self.view addSubview:greenViewiew];
    
    LAYEqual(greenViewiew.top, self.view.top, 1, 80.f);
    LAYEqual(greenViewiew.left, self.view.left, 1, 20.f);
    LAYEqual(greenViewiew.right, self.view.right, 1, -20.f);
    LAYEqualC(greenViewiew.height, 300.f);

约束为一个范围的情况

我们在实际开发中经常会碰到,某视图布局的需求是最大不得超过多少,且最小不得小于多少。是在一个范围内的。
比如一个UILabel,它里面的文字很多时,正常情况下系统会自动根据文本的多少来更新高度,而不需要你自己添加约束。

    UILabel *lab = [UILabel create];
    lab.numberOfLines = 0;
    lab.text = @"经济学的理论建立在这样一种假设上:人们在经济交往中企图以最小的代价获取最大的利润。但市场中的交换其实不过是社会交换中的一种典型。广泛的社会交换同样建立在这样假设上:人们在社会生活总也是企图以最小代价获取最大利益的。所不同的是经济生活中的代价和报偿是有形的,表现为金钱和物质,而社会交往中的代价和报偿在很多时间和场合中是无形的,如友情,义务,声望,权威。";
    lab.backgroundColor = [UIColor greenColor];
    [self.view addSubview:lab];
    LAYEqual(lab.left, self.view.left, 1, 30.f);
    LAYEqual(lab.right, self.view.right, 1, -30.f);
    LAYEqual(lab.top, self.view.top, 1, 100.f);

此时不需要你约束该lab的高度,它会根据文本多少来自动更新高度。

项目干货挖掘4——如何优雅地使用AutoLayout自动布局_第4张图片
lab01.png

但是往往需求中,产品经理要求这里文字很多时,该视图也不能无限制变高,得有个限度;当文字很少时,高度也不能太小,也要有个限度。这个在我们上面的封装的自动布局中是支持的。

    UILabel *lab = [UILabel create];
    lab.numberOfLines = 0;
    lab.text = @"经济学的理论建立在这样一种假设上:人们在经济交往中企图以最小的代价获取最大的利润。但市场中的交换其实不过是社会交换中的一种典型。广泛的社会交换同样建立在这样假设上:人们在社会生活总也是企图以最小代价获取最大利益的。所不同的是经济生活中的代价和报偿是有形的,表现为金钱和物质,而社会交往中的代价和报偿在很多时间和场合中是无形的,如友情,义务,声望,权威。";
    lab.backgroundColor = [UIColor greenColor];
    [self.view addSubview:lab];
    LAYEqual(lab.left, self.view.left, 1, 30.f);
    LAYEqual(lab.right, self.view.right, 1, -30.f);
    LAYEqual(lab.top, self.view.top, 1, 100.f);
    LAYMostC(lab.height, 100.f);

即便文字很多,高度也为100.f这个上限:


项目干货挖掘4——如何优雅地使用AutoLayout自动布局_第5张图片
lab02.png

关于自动布局的代码我也上传至GitHub上了,求Star支持。
YWAutoLayoutGitHub地址

你可能感兴趣的:(项目干货挖掘4——如何优雅地使用AutoLayout自动布局)