iOS自動佈局——Masonry詳解

歡迎你們前往騰訊雲+社區,獲取更多騰訊海量技術實踐乾貨哦~javascript

本文由 鵝廠新鮮事兒發表於 雲+社區專欄

做者:oceanlong | 騰訊 移動客戶端開發工程師前端

前言

UI佈局是整個前端體系裏不可或缺的一環。代碼的佈局是設計語言與用戶視覺感覺溝通的橋樑,不論它看起來多麼簡單或是瑣碎,但不得不認可,絕大部分軟件開發的問題,都是界面問題。那麼,如何高效的完成UI開發,也是軟件行業一直在克服的問題。java

img

因此,軟件界面開發的核心點便是:如何減小UI設計稿的建模難度和減小建模轉化到代碼的實現難度git

最初iOS提供了平面直角座標系的方式,來解決佈局問題,即所謂的手動佈局。平面直角座標系確實是一套完備在理論,這在數學上已經驗證過了,只要咱們的屏幕仍是平面,它就確定是有效的。但有效不必定高效,咱們在平常的生活中,不多會用平面直角座標系來向人描述位置關係。更多的是依靠相對位置。github

所幸,iOS爲咱們提供自動佈局的方法,來解決這一困境。app

img

自動佈局的基本理念

其實說到本質,它和手動佈局是同樣的。對一個控件放在哪裏,咱們依然只關心它的(x, y, width, height)。但手動佈局的方式是,一次性計算出這四個值,而後設置進去,完成佈局。但當父控件或屏幕發生變化時,子控件的計算就要從新來過,很是麻煩。框架

所以,在自動佈局中,咱們再也不關心(x, y, width, height)的具體值,咱們只關心(x, y, width, height)四個量對應的約束。機器學習

約束

那麼何爲約束呢?ide

obj1.property1 =(obj2.property2 * multiplier)+ constant value

子控件的某一個量必定與另外一個控件的某一個量呈線性關係,這就是約束。函數

那麼,給(x, y, width, height)四個量,分別給一個約束,就能夠肯定一個控件的最終位置。

//建立左邊約束
    NSLayoutConstraint *leftLc = [NSLayoutConstraint constraintWithItem:blueView attribute:NSLayoutAttributeLeft relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeLeft multiplier:1.0 constant:20];
    [self.view addConstraint:leftLc];

這一段代碼便是:控件(blueView)的 x = rootView的x * 1.0 + 20這裏必定要注意,這樣的一條約束,涉及了子控件和父控件,因此這條約束必定要添加到父控件中。

添加約束的規則:

  • 若是兩個控件是父子控件,則添加到父控件中。
  • 若是兩個控件不是父子控件,則添加到層級最近的共同父控件中。

示例

//關閉Autoresizing
    blueView.translatesAutoresizingMaskIntoConstraints = NO;
    
    //建立左邊約束
    NSLayoutConstraint *leftLc = [NSLayoutConstraint constraintWithItem:blueView attribute:NSLayoutAttributeLeft relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeLeft multiplier:1.0 constant:20];
    [self.view addConstraint:leftLc];
    
    //建立右邊約束
    NSLayoutConstraint *rightLc = [NSLayoutConstraint constraintWithItem:blueView attribute:NSLayoutAttributeRight relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeRight multiplier:1.0 constant:-20];
    [self.view addConstraint:rightLc];
    
    //建立底部約束
    NSLayoutConstraint *bottomLc = [NSLayoutConstraint constraintWithItem:blueView attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeBottom multiplier:1.0 constant:-20];
    [self.view addConstraint:bottomLc];
    
    //建立高度約束
    NSLayoutConstraint *heightLc = [NSLayoutConstraint constraintWithItem:blueView attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:0.0 constant:50];
    [blueView addConstraint: heightLc];

img

咱們注意到,自動佈局其實工做分兩步:

  1. 建立視圖的約束
  2. 將約束添加到合適的位置約束關係從上面的描述中,已經很是清晰了。那麼如何尋找約束添加的合適位置呢?

img


到這裏,咱們只是解決了如何減小UI設計稿的建模難度的問題,顯然,減小建模轉化到代碼的實現難度這個效果沒能達成。關於如何解決減小建模轉化到代碼的實現難度的問題,

開源庫

上面的代碼,咱們能夠看到,雖然自動佈局已經比手動佈局優雅很多了,但它依然行數較多。每條約束大約都須要三行代碼,面對複雜的頁面,這樣開發出來,會很難閱讀。

Masonry則爲咱們解決了這個問題。

Masonry地址

引入Masonry

咱們選擇使用Cocoapods的方式。引入比較簡單:

  1. 咱們先在工程目錄下,建立Podfile文件:

img

2.編輯Podfile

img

其中,'IosOcDemo'就是咱們工程的名字,根據須要,咱們自行替換。

3.添加依賴

完成後,執行指令pod install。CocoaPods就會爲咱們自動下載並添加依賴。

實踐

img

這樣的一個代碼,用手動佈局,咱們大體的代碼應該是這樣:

-(void)initBottomView
{
    self.bottomBarView = [[UIView alloc]initWithFrame:CGRectZero];
    self.bottomButtons = [[NSMutableArray alloc]init];
    _bottomBarView.backgroundColor = [UIColor yellowColor];
    [self addSubview:_bottomBarView];
    for(int i = 0 ; i < 3 ; i++)
    {
        UIButton *button = [[UIButton alloc]initWithFrame:CGRectZero];
        button.backgroundColor = [UIColor redColor];
        [_bottomButtons addObject:button];
       [self addSubview:button];
    }
}


-(void)layoutBottomView
{
    _bottomBarView.frame = CGRectMake(20, _viewHeight - 200, _viewWidth - 40, 200);
    for (int i = 0 ; i < 3; i++) {
        UIButton *button = _bottomButtons[i];
        CGFloat x = i * (_viewWidth - 40 - 20 * 4) / 3 + 20*(i+1) + 20;
        CGFloat y = _viewHeight - 200;
        CGFloat width = (_viewWidth - 40 - 20 * 4) / 3;
        CGFloat height = 200;
        button.frame = CGRectMake(x, y, width, height);
        
    }
}

咱們來看一下,在Masonry的幫助下,咱們能夠把剛剛的代碼寫成什麼樣的:

-(void)initBottomView
{
        _bottomBarView = [[UIView alloc]initWithFrame:CGRectZero];
        _bottomBarView.backgroundColor = [UIColor yellowColor];
        _bottomBarView.translatesAutoresizingMaskIntoConstraints = NO;
        [self addSubview:_bottomBarView];
        [_bottomBarView mas_makeConstraints:^(MASConstraintMaker *make) {
            make.left.equalTo(self).with.offset(20);
            make.right.equalTo(self).with.offset(-20);
            make.height.mas_equalTo(200);
            make.bottom.equalTo(self);
        }];
    
    _bottomButtons = [[NSMutableArray alloc]init];
    for(int i = 0 ; i < 3 ; i++)
    {
        UIButton *button = [[UIButton alloc]initWithFrame: CGRectZero];
        button.backgroundColor = [UIColor redColor];
        button.translatesAutoresizingMaskIntoConstraints = NO;
        [_bottomButtons addObject:button];
        [_bottomBarView addSubview:button];
        [button mas_makeConstraints:^(MASConstraintMaker *make) {
            if (i == 0) {
                make.left.mas_equalTo(20);
            }else{
                UIButton *previousButton = _bottomButtons[i-1];
                make.left.equalTo(previousButton.mas_right).with.offset(20);
            }
            make.top.mas_equalTo(_bottomBarView.mas_top);
            make.width.equalTo(_bottomBarView.mas_width).with.multipliedBy(1.0f/3).offset(-20*4/3);
            make.height.equalTo(_bottomBarView.mas_height);
        }];
        
    }
}

咱們能夠看到在Masonry的封裝下,代碼變得很是簡練易讀,須要行數略有增長,可是計算過程減小了,咱們能更加關注於多個UIView間的位置關係,這與當前的UI設計語言是契合的。因此Masonry可否讓咱們更直觀地表達UI。

源碼解讀

Masonry的封裝頗有魅力,那麼,咱們能夠簡單地來看一下,它是如何封裝的。咱們再仔細看一下Masonry的API會發現,咱們是直接在UIView上進行調用的。也就是說,Masonry對UIView進行了擴展。

View+MASUtilities.h中:

#if TARGET_OS_IPHONE || TARGET_OS_TV

    #import <UIKit/UIKit.h>
    #define MAS_VIEW UIView
    #define MAS_VIEW_CONTROLLER UIViewController
    #define MASEdgeInsets UIEdgeInsets

而後在View+MASAdditions.h中,咱們看到了Masonry的擴展:

#import "MASUtilities.h"
#import "MASConstraintMaker.h"
#import "MASViewAttribute.h"

/**
 *  Provides constraint maker block
 *  and convience methods for creating MASViewAttribute which are view + NSLayoutAttribute pairs
 */
@interface MAS_VIEW (MASAdditions)

/**
 *  following properties return a new MASViewAttribute with current view and appropriate NSLayoutAttribute
 */
@property (nonatomic, strong, readonly) MASViewAttribute *mas_left;
@property (nonatomic, strong, readonly) MASViewAttribute *mas_top;
@property (nonatomic, strong, readonly) MASViewAttribute *mas_right;
@property (nonatomic, strong, readonly) MASViewAttribute *mas_bottom;
@property (nonatomic, strong, readonly) MASViewAttribute *mas_leading;
@property (nonatomic, strong, readonly) MASViewAttribute *mas_trailing;
@property (nonatomic, strong, readonly) MASViewAttribute *mas_width;
@property (nonatomic, strong, readonly) MASViewAttribute *mas_height;
@property (nonatomic, strong, readonly) MASViewAttribute *mas_centerX;
@property (nonatomic, strong, readonly) MASViewAttribute *mas_centerY;
@property (nonatomic, strong, readonly) MASViewAttribute *mas_baseline;
@property (nonatomic, strong, readonly) MASViewAttribute *(^mas_attribute)(NSLayoutAttribute attr);

...

/**
 *  Creates a MASConstraintMaker with the callee view.
 *  Any constraints defined are added to the view or the appropriate superview once the block has finished executing
 *
 *  @param block scope within which you can build up the constraints which you wish to apply to the view.
 *
 *  @return Array of created MASConstraints
 */
- (NSArray *)mas_makeConstraints:(void(NS_NOESCAPE ^)(MASConstraintMaker *make))block;

一些,適配的代碼,我省略了,先看核心代碼。在剛剛的例子中,咱們正是調用的mas_makeConstraints方法。

- (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *))block {
    self.translatesAutoresizingMaskIntoConstraints = NO;
    MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self];
    block(constraintMaker);
    return [constraintMaker install];
}

mas_makeConstraints方法比較簡單,只是封裝了MASConstraintMaker初始化,設置約束和安裝。這裏的block就是咱們剛剛在外層設置的約束的函數指針。也就是這一串:

^(MASConstraintMaker *make) {
        make.left.equalTo(self.view).with.offset(10);
        make.right.equalTo(self.view).with.offset(-10);
        make.height.mas_equalTo(50);
        make.bottom.equalTo(self.view).with.offset(-10);
    }

因爲約束條件的設置比較複雜,咱們先來看看初始化和安裝。

初始化

- (id)initWithView:(MAS_VIEW *)view {
    self = [super init];
    if (!self) return nil;
    
    self.view = view;
    self.constraints = NSMutableArray.new;
    
    return self;
}

初始化的代碼比較簡單,將傳入的view放入MASConstraintMaker成員,而後建立MASConstraintMaker的約束容器(NSMutableArray)。

安裝

- (NSArray *)install {
    if (self.removeExisting) {
        NSArray *installedConstraints = [MASViewConstraint installedConstraintsForView:self.view];
        for (MASConstraint *constraint in installedConstraints) {
            [constraint uninstall];
        }
    }
    NSArray *constraints = self.constraints.copy;
    for (MASConstraint *constraint in constraints) {
        constraint.updateExisting = self.updateExisting;
        [constraint install];
    }
    [self.constraints removeAllObjects];
    return constraints;
}

安裝的代碼分爲三塊:

  1. 判斷是否須要移除已有的約束。若是須要,會遍歷已有約束,而後逐個uninstall
  2. copy已有的約束,遍歷,並逐一install
  3. remove掉全部約束,並將已添加的constraints返回。

install的方法,仍是繼續封裝到了Constraint中,咱們繼續跟進閱讀:

咱們會發現Constraint只是一個接口,Masonry中對於Constraint接口有兩個實現,分別是:MASViewConstraintMASCompositeConstraint。這兩個類,分別是單個約束和約束集合。在上面的例子中,咱們只是對單個UIView進行約束,因此咱們先看MASViewConstraint的代碼。如下代碼MASViewConstraint進行了必定程度的簡化,省略了一些擴展屬性,只展現咱們的例子中,會執行的代碼:

- (void)install {
    if (self.hasBeenInstalled) {
        return;
    }
    ...
    
    MAS_VIEW *firstLayoutItem = self.firstViewAttribute.item;
    NSLayoutAttribute firstLayoutAttribute = self.firstViewAttribute.layoutAttribute;
    MAS_VIEW *secondLayoutItem = self.secondViewAttribute.item;
    NSLayoutAttribute secondLayoutAttribute = self.secondViewAttribute.layoutAttribute;

    // alignment attributes must have a secondViewAttribute
    // therefore we assume that is refering to superview
    // eg make.left.equalTo(@10)
    if (!self.firstViewAttribute.isSizeAttribute && !self.secondViewAttribute) {
        secondLayoutItem = self.firstViewAttribute.view.superview;
        secondLayoutAttribute = firstLayoutAttribute;
    }
    
    MASLayoutConstraint *layoutConstraint
        = [MASLayoutConstraint constraintWithItem:firstLayoutItem
                                        attribute:firstLayoutAttribute
                                        relatedBy:self.layoutRelation
                                           toItem:secondLayoutItem
                                        attribute:secondLayoutAttribute
                                       multiplier:self.layoutMultiplier
                                         constant:self.layoutConstant];
    
    layoutConstraint.priority = self.layoutPriority;
    layoutConstraint.mas_key = self.mas_key;
    
   if (self.secondViewAttribute.view) {
        MAS_VIEW *closestCommonSuperview = [self.firstViewAttribute.view mas_closestCommonSuperview:self.secondViewAttribute.view];
        NSAssert(closestCommonSuperview,
                 @"couldn't find a common superview for %@ and %@",
                 self.firstViewAttribute.view, self.secondViewAttribute.view);
        self.installedView = closestCommonSuperview;
    } else if (self.firstViewAttribute.isSizeAttribute) {
        self.installedView = self.firstViewAttribute.view;
    } else {
        self.installedView = self.firstViewAttribute.view.superview;
    }


    MASLayoutConstraint *existingConstraint = nil;
    ...
    else {
        [self.installedView addConstraint:layoutConstraint];
        self.layoutConstraint = layoutConstraint;
        [firstLayoutItem.mas_installedConstraints addObject:self];
    }
}

自動佈局是一種相對佈局,因此,絕大部分狀況下,須要兩個UIView(約束方與參照方)。在上面的方法中:

  • firstLayoutItem是約束方,secondLayoutItem是參照方
  • firstLayoutAttribute是約束方的屬性,secondLayoutAttribute是參照方的屬性。
  • MASLayoutConstraint就是NSLayoutConstraint的子類,只是添加了mas_key屬性。到這裏,咱們就與系統提供的API對應上了。
NSLayoutConstraint *leftLc = [NSLayoutConstraint constraintWithItem:blueView attribute:NSLayoutAttributeLeft relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeLeft multiplier:1.0 constant:20];
    [self.view addConstraint:leftLc];

再看看咱們以前用系統API完成的例子,是否是格外熟悉?

那麼接下來,咱們就是要閱讀

make.left.equalTo(self).with.offset(20);
            make.right.equalTo(self).with.offset(-20);
            make.height.mas_equalTo(200);
            make.bottom.equalTo(self);

是如何變成firstLayoutItem, secondLayoutItem, firstLayoutAttribute, secondLayoutAttributelayoutRelation的。

約束條件的設置

回到前面的:

- (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *))block {
    self.translatesAutoresizingMaskIntoConstraints = NO;
    MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self];
    block(constraintMaker);
    return [constraintMaker install];
}

咱們接下來,就要看block的實現:

block實際上是一個函數指針。此處真正調用的方法是:

make.left.equalTo(self).with.offset(20);
            make.right.equalTo(self).with.offset(-20);
            make.height.mas_equalTo(200);
            make.bottom.equalTo(self);

咱們挑選其中一個,來看看源碼實現:

left
- (MASConstraint *)addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute {
    return [self constraint:nil addConstraintWithLayoutAttribute:layoutAttribute];
}

- (MASConstraint *)left {
    return [self addConstraintWithLayoutAttribute:NSLayoutAttributeLeft];
}

- (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]) {
        //replace with composite constraint
        NSArray *children = @[constraint, newConstraint];
        MASCompositeConstraint *compositeConstraint = [[MASCompositeConstraint alloc] initWithChildren:children];
        compositeConstraint.delegate = self;
        [self constraint:constraint shouldBeReplacedWithConstraint:compositeConstraint];
        return compositeConstraint;
    }
    if (!constraint) {
        newConstraint.delegate = self;
        [self.constraints addObject:newConstraint];
    }
    return newConstraint;
}

在對單個view添加約束時,constraint爲nil。咱們直接生成了一個新約束newConstraint。它的firstViewAttribute就是咱們傳入的NSLayoutAttributeLeft

equalTo
- (MASConstraint * (^)(id))equalTo {
    return ^id(id attribute) {
        return self.equalToWithRelation(attribute, NSLayoutRelationEqual);
    };
}

- (MASConstraint * (^)(id, NSLayoutRelation))equalToWithRelation {
    return ^id(id attribute, NSLayoutRelation relation) {
        if ([attribute isKindOfClass:NSArray.class]) {
            NSAssert(!self.hasLayoutRelation, @"Redefinition of constraint relation");
            NSMutableArray *children = NSMutableArray.new;
            for (id attr in attribute) {
                MASViewConstraint *viewConstraint = [self copy];
                viewConstraint.layoutRelation = relation;
                viewConstraint.secondViewAttribute = attr;
                [children addObject:viewConstraint];
            }
            MASCompositeConstraint *compositeConstraint = [[MASCompositeConstraint alloc] initWithChildren:children];
            compositeConstraint.delegate = self.delegate;
            [self.delegate constraint:self shouldBeReplacedWithConstraint:compositeConstraint];
            return compositeConstraint;
        } else {
            NSAssert(!self.hasLayoutRelation || self.layoutRelation == relation && [attribute isKindOfClass:NSValue.class], @"Redefinition of constraint relation");
            self.layoutRelation = relation;
            self.secondViewAttribute = attribute;
            return self;
        }
    };
}

此處,咱們依然先看attribute不是NSArray的狀況。這裏在單個屬性的約束中,就比較簡單了,將relationattribue傳入MASConstraint對應的成員。

在上面介紹install方法時,咱們就曾提到過:

MASLayoutConstraint *layoutConstraint
        = [MASLayoutConstraint constraintWithItem:firstLayoutItem
                                        attribute:firstLayoutAttribute
                                        relatedBy:self.layoutRelation
                                           toItem:secondLayoutItem
                                        attribute:secondLayoutAttribute
                                       multiplier:self.layoutMultiplier
                                         constant:self.layoutConstant];

firstLayoutItemsecondLayoutIteminstall方法中已收集完成,此時,通過leftequalTo咱們又收集到了:firstViewAttributesecondViewAttributelayoutRelation勝利即在眼前。

- (MASConstraint * (^)(CGFloat))offset {
    return ^id(CGFloat offset){
        self.offset = offset;
        return self;
    };
}

- (void)setOffset:(CGFloat)offset {
    self.layoutConstant = offset;
}

經過OC的set語法,Masonryoffset傳入layoutConstant。

至此,layoutConstraint就完成了所有的元素收集,可使用添加約束的方式,只須要解決最後一個問題,約束添加到哪裏呢?咱們彷佛在調用時,並不須要關心這件事情,那說明框架幫咱們完成了這個工做。

closestCommonSuperview

咱們在MASViewConstraint中,能夠找到這樣一段:

if (self.secondViewAttribute.view) {
        MAS_VIEW *closestCommonSuperview = [self.firstViewAttribute.view mas_closestCommonSuperview:self.secondViewAttribute.view];
        NSAssert(closestCommonSuperview,
                 @"couldn't find a common superview for %@ and %@",
                 self.firstViewAttribute.view, self.secondViewAttribute.view);
        self.installedView = closestCommonSuperview;
    } else if (self.firstViewAttribute.isSizeAttribute) {
        self.installedView = self.firstViewAttribute.view;
    } else {
        self.installedView = self.firstViewAttribute.view.superview;
    }

注意到,closetCommonSuperview就是Masonry爲咱們找到的最近公共父控件。

- (instancetype)mas_closestCommonSuperview:(MAS_VIEW *)view {
    MAS_VIEW *closestCommonSuperview = nil;

    MAS_VIEW *secondViewSuperview = view;
    while (!closestCommonSuperview && secondViewSuperview) {
        MAS_VIEW *firstViewSuperview = self;
        while (!closestCommonSuperview && firstViewSuperview) {
            if (secondViewSuperview == firstViewSuperview) {
                closestCommonSuperview = secondViewSuperview;
            }
            firstViewSuperview = firstViewSuperview.superview;
        }
        secondViewSuperview = secondViewSuperview.superview;
    }
    return closestCommonSuperview;
}

實現也比較簡單。

至此,咱們完成了全部準備,就能夠開始愉快的自動佈局啦。

以上就是Masonry對iOS自動佈局封裝的解讀。

若有問題,歡迎指正。

問答
iOS:如何使用自動佈局約束?
相關閱讀
走進 Masonry
iOS自動佈局框架之Masonry
iOS學習——佈局利器Masonry框架源碼深度剖析
【每日課程推薦】機器學習實戰!快速入門在線廣告業務及CTR相應知識

此文已由做者受權騰訊雲+社區發佈,更多原文請點擊

搜索關注公衆號「雲加社區」,第一時間獲取技術乾貨,關注後回覆1024 送你一份技術課程大禮包!

海量技術實踐經驗,盡在雲加社區

相關文章
相關標籤/搜索