iOS 12 Auto Layout界面自動佈局系列3-使用原生NSLayoutConstraint添加布局約束

本系列的第一篇文章介紹了自動佈局的基本原理,第二篇文章通過一個簡單的例子演示瞭如何使用Xcode的Interface Builder(簡稱IB)以可視化方式添加約束。本篇爲該系列的第三篇文章,主要介紹如何通過寫代碼來添加布局約束。
說句題外話,不論是通過IB可視化加約束,還是寫代碼加約束,這兩種方式各有優劣。寫代碼加約束是最基礎最靈活的方式,但缺點是當界面較複雜時代碼量會很繁冗易錯。而通過IB可視化方式設置約束,操作簡單直觀,並支持在設計器中實時預覽佈局效果,但缺點是並非所有約束都能用IB來添加,而且不容易後期維護。所以掌握寫代碼添加自動佈局約束是非常必要的。原本這篇文章是本系列的第二篇,但爲了提高讀者理解和接受的程度,最終還是把本編放在第三的位置。OK閒言少敘,我們進入正題。

NSLayoutConstraint類的定義

第一篇文章講到,每一個佈局約束就是一個明確的線性變化規則,在數學上是以一次函數的形式表示,即:
y = m * x + c   (公式3.1)
例如,對於下圖計算器App,若想表達按鈕5的頂部與按鈕8的底部對齊,根據公式3.1,建立以下線性關係即可:
按鈕5.頂部 = 1.0 * 按鈕8.底部 + 0.0
這就是一個佈局約束。
計算器App
在UIKit中,每一個佈局約束是一個NSLayoutConstraint實例。NSLayoutConstraint類的定義如下:

NS_CLASS_AVAILABLE_IOS(6_0)
@interface NSLayoutConstraint : NSObject
...
@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;
@property CGFloat constant;
...
+(instancetype)constraintWithItem:(id)firstItem attribute:(NSLayoutAttribute)firstAttribute 
 relatedBy:(NSLayoutRelation)relation 
 toItem:(id)secondItem attribute:(NSLayoutAttribute)secondAttribute 
 multiplier:(CGFloat)multiplier constant:(CGFloat)constant;

其中firstItem與secondItem分別是界面中受約束的視圖與被參照的視圖。對於按鈕5頂部與按鈕8底部對齊的例子來說,則firstItem是按鈕5,secondItem是按鈕8。(需要注意的是,它們不一定非得是兄弟關係或者父子關係,只要它們有着共同的祖先視圖即可,這可是autoresizingMask無法做到的呦。)

firstAttribute與secondAttribute分別是firstItem與secondItem的某個佈局屬性(NSLayoutAttribute),它是一個枚舉:

typedef NS_ENUM(NSInteger, NSLayoutAttribute)
{
    NSLayoutAttributeLeft = 1,
    NSLayoutAttributeRight,
    NSLayoutAttributeTop,
    NSLayoutAttributeBottom,
    NSLayoutAttributeLeading,
    NSLayoutAttributeTrailing,
    NSLayoutAttributeWidth,
    NSLayoutAttributeHeight,
    NSLayoutAttributeCenterX,
    NSLayoutAttributeCenterY,
    NSLayoutAttributeBaseline,
    NSLayoutAttributeNotAnAttribute = 0,
    ......//省略剩餘
};

每一個枚舉值代表了一個佈局屬性,名字都很直觀,例如Left代表左側,Height代表高度等等。對於按鈕5頂部與按鈕8底部對齊的例子來說,firstAttribute是NSLayoutAttributeTop,secondItem是NSLayoutAttributeBottom。(注意,firstItem與secondItem不一定非得是同樣的值,允許定義諸如某視圖的高度等於另一個視圖的寬度這樣的約束,儘管很少這樣做。)
這裏額外解釋一下NSLayoutAttributeNotAnAttribute,當需要爲視圖的寬度或高度指定固定值時(例如希望某個視圖寬度固定爲100),這時候secondItem爲nil,secondAttribute爲NSLayoutAttributeNotAnAttribute。

relation定義了佈局關係(NSLayoutRelation):

typedef NS_ENUM(NSInteger, NSLayoutRelation)
{
    NSLayoutRelationLessThanOrEqual = -1,
    NSLayoutRelationEqual = 0,
    NSLayoutRelationGreaterThanOrEqual = 1,
};

佈局關係不僅限於相等,還可以是大於等於或者小於等於,這種不等關係在處理UILabel、UIImageView等具有自身內容尺寸的控件(自身內容尺寸參見本系列第五篇文章)時非常常用。舉個簡單的例子,UILabel的長度會隨文字的長度而變化,那麼我們可以向UILabel控件添加兩個約束,分別是“長度大於等於50”與“長度小於等於200”。這樣,當文字很少時,寬度也至少爲50;當文字非常多時,寬度也不會超過200。

multiplier即比例係數。constant即常量。

因此,每個約束就對應如下關係:
firstItem.firstAttribute {==,<=,>=} secondItem.secondAttribute * multiplier + constant   (公式3.2)
我們可以調用NSLayoutConstraint類的constraintWithItem:…方法,傳入所有需要的參數構造一個新的約束。
例如,要想表達按鈕5的頂部與按鈕8的底部對齊,根據公式3.2,代碼如下:

[NSLayoutConstraint constraintWithItem:button5 
    attribute:NSLayoutAttributeTop 
    relatedBy:NSLayoutRelationEqual 
    toItem:button8 
    attribute:NSLayoutAttributeBottom 
    multiplier:1.0f 
    constant:0.0f];

是不是很簡單?

使用NSLayoutConstraint添加布局約束

理論就到此爲止,下面我們還是以第二篇文章中的例子來講解如何使用代碼添加約束。Demo運行後的界面如下:
縱屏
橫屏
打開Xcode(10.1版),新建Single View App項目,項目命名爲AutoLayoutByConstraint,本文使用Objective-C講解,設備選擇Universal。下載蘋果Logo圖片apple.jpg,並將其拖入項目中。文件下載地址:
鏈接:https://pan.baidu.com/s/1b5AqDo 密碼:e4ff
首先,界面上方用來顯示蘋果Logo圖片的是一個UIImageView,ViewController類的viewDidLoad方法如下:

- (void)viewDidLoad
{
    [super viewDidLoad];
    
    UIImageView* logoImageView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"apple.jpg"]];
    logoImageView.contentMode = UIViewContentModeScaleAspectFit;
    [self.view addSubview:logoImageView];
}

我們需要爲logoImageView添加4個約束:

  • logoImageView左側與父視圖頭部對齊
  • logoImageView右側與父視圖尾部對齊
  • logoImageView頂部與父視圖頂部對齊
  • logoImageView高度爲父視圖高度一半
    根據公式3.2,在ViewController類的viewDidLoad方法末尾處構造上述4個約束,代碼如下:
    //logoImageView左側與父視圖頭部對齊
    NSLayoutConstraint* leftConstraint = [NSLayoutConstraint 
        constraintWithItem:logoImageView 
        attribute:NSLayoutAttributeLeading 
        relatedBy:NSLayoutRelationEqual 
        toItem:logoImageView.superview 
        attribute:NSLayoutAttributeLeading 
        multiplier:1.0f constant:0.0f];
    
    //logoImageView右側與父視圖尾部對齊
    NSLayoutConstraint* rightConstraint = [NSLayoutConstraint 
        constraintWithItem:logoImageView 
        attribute:NSLayoutAttributeTrailing 
        relatedBy:NSLayoutRelationEqual 
        toItem:logoImageView.superview 
        attribute:NSLayoutAttributeTrailing 
        multiplier:1.0f constant:0.0f];
    
    //logoImageView頂部與父視圖頂部對齊
    NSLayoutConstraint* topConstraint = [NSLayoutConstraint 
        constraintWithItem:logoImageView 
        attribute:NSLayoutAttributeTop 
        relatedBy:NSLayoutRelationEqual 
        toItem:logoImageView.superview 
        attribute:NSLayoutAttributeTop 
        multiplier:1.0f constant:0.0f];
    
    //logoImageView高度爲父視圖高度一半
    NSLayoutConstraint* heightConstraint = [NSLayoutConstraint 
        constraintWithItem:logoImageView 
        attribute:NSLayoutAttributeHeight 
        relatedBy:NSLayoutRelationEqual 
        toItem:logoImageView.superview 
        attribute:NSLayoutAttributeHeight 
        multiplier:0.5f constant:0.0f];
    
    //iOS 6.0或者7.0調用addConstraints
    //[self.view addConstraints:@[leftConstraint, rightConstraint, topConstraint, heightConstraint]];
    
    //iOS 8.0以後設置每個約束的active屬性值爲YES
    //leftConstraint.active = YES;
    //rightConstraint.active = YES;
    //topConstraint.active = YES;
    //heightConstraint.active = YES;
    
    ///iOS 8.0以後推薦使用activateConstraints:方法,效率比單獨設置active屬性要高
    [NSLayoutConstraint activateConstraints:@[leftConstraint, rightConstraint, topConstraint, heightConstraint]];

注意,約束在創建後默認是未激活的(不生效的),需要手動激活才能令約束生效。對於iOS 8及更新的版本,直接設置約束的active屬性(BOOL值)或者調用activateConstraints:與deactivateConstraints:類方法。(對於iOS 6或者iOS 7可以調用addConstraint(s):和removeConstraint(s):方法。)

就是這麼簡單!現在編譯並運行項目(模擬器iPhone 5s爲例)。
豎屏
橫屏
貌似logoImageView的尺寸不太對,而且豎屏時圖像未能居中。如果在viewDidLoad方法中將self.view的背景色設置爲紅色,看得會更清楚:
在這裏插入圖片描述
同時注意到Xcode控制檯打印出了一大段信息:

2019-01-30 14:36:07.945820+0800 AutoLayoutByConstraint[11003:993889] [LayoutConstraints] 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. 
	(Note: If you're seeing NSAutoresizingMaskLayoutConstraints that you don't understand, refer to the documentation for the UIView property translatesAutoresizingMaskIntoConstraints) 
(
    "<NSAutoresizingMaskLayoutConstraint:0x6000003cadf0 h=--& v=--& apple.jpg.midX == 120.5   (active, names: apple.jpg:0x7f9625f04530 )>",
    "<NSLayoutConstraint:0x6000003cba20 H:|-(0)-[apple.jpg]   (active, names: apple.jpg:0x7f9625f04530, '|':UIView:0x7f9625d27350 )>",
    "<NSLayoutConstraint:0x6000003cb890 apple.jpg.trailing == UIView:0x7f9625d27350.trailing   (active, names: apple.jpg:0x7f9625f04530 )>",
    "<NSLayoutConstraint:0x6000003c8280 'UIView-Encapsulated-Layout-Width' UIView:0x7f9625d27350.width == 568   (active)>"
)

Will attempt to recover by breaking constraint 
<NSLayoutConstraint:0x6000003cb890 apple.jpg.trailing == UIView:0x7f9625d27350.trailing   (active, names: apple.jpg:0x7f9625f04530 )>

Make a symbolic breakpoint at UIViewAlertForUnsatisfiableConstraints to catch this in the debugger.
The methods in the UIConstraintBasedLayoutDebugging category on UIView listed in <UIKitCore/UIView.h> may also be helpful.

//我簡要翻譯一下上述內容:
//不能同時滿足約束。或許下列約束中的至少其中一個並非如你所願。嘗試如下方法:
//(1) 檢查每個約束,試着找出並不期望的約束。
//(2) 找到添加該約束的代碼,並進行修正。
//(備註:如果你看到NSAutoresizingMaskLayoutConstraint卻並不理解,請查閱UIview文檔中的translatesAutoresizingMaskIntoConstraints屬性。)

看來是出錯了,爲什麼會這樣?這是由於自動佈局技術是蘋果在iOS 6當中新加入的,但在那時仍然有很多項目代碼使用autoresizingMask與setFrame:的方式構建界面。倘若將一個已經設置好frame並使用autoresizingMask的視圖v1添加到一個使用自動佈局的視圖v2中時,運行時會使用自動佈局來構建界面。爲了保持對舊代碼的兼容性,運行時會隱式地將v1的frame和autoresizingMask轉化爲自動佈局約束(這些隱式轉換的約束的類型爲NSAutoresizingMaskLayoutConstraint),這樣才能明確v1的位置與尺寸而不會導致約束缺失。這個隱式轉換的過程,是由UIView的translatesAutoresizingMaskIntoConstraints屬性的值決定的。默認情況下,爲保證兼容性,該值爲YES,表示需要自動進行隱式轉換。這對於兼容舊的代碼當然是好的(否則就需要對原有代碼做大量修改),然而當我們明確爲視圖添加了約束後,我們就不希望再進行autoresizingMask的隱式轉換了,否則就會引起約束的衝突(Confliction)。因此,需要特別注意的是,當我們使用代碼創建視圖時,需要將translatesAutoresizingMaskIntoConstraints屬性的值設置爲NO。在viewDidLoad方法中創建logoImageView的代碼之後,添加如下代碼:

    logoImageView.translatesAutoresizingMaskIntoConstraints = NO;

再次運行,這次就沒問題了。
在這裏插入圖片描述
到這裏,我想你應該可以把剩餘的視圖和約束的代碼添加上了,全部代碼如下:

- (void)viewDidLoad
{
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    
    // self.view.backgroundColor = [UIColor redColor];
    
    UIImageView* logoImageView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"apple.jpg"]];
    logoImageView.translatesAutoresizingMaskIntoConstraints = NO;
    logoImageView.contentMode = UIViewContentModeScaleAspectFit;
    [self.view addSubview:logoImageView];
    
    //logoImageView左側與父視圖左側對齊
    NSLayoutConstraint* leftConstraint = [NSLayoutConstraint constraintWithItem:logoImageView attribute:NSLayoutAttributeLeading relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeLeading multiplier:1.0f constant:0.0f];
    
    //logoImageView右側與父視圖右側對齊
    NSLayoutConstraint* rightConstraint = [NSLayoutConstraint constraintWithItem:logoImageView attribute:NSLayoutAttributeTrailing relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeTrailing multiplier:1.0f constant:0.0f];
    
    //logoImageView頂部與父視圖頂部對齊
    NSLayoutConstraint* topConstraint = [NSLayoutConstraint constraintWithItem:logoImageView attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeTop multiplier:1.0f constant:0.0f];
    
    //logoImageView高度爲父視圖高度一半
    NSLayoutConstraint* heightConstraint = [NSLayoutConstraint constraintWithItem:logoImageView attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeHeight multiplier:0.5f constant:0.0f];
    
    //iOS 6.0或者7.0調用addConstraints
    //    [self.view addConstraints:@[leftConstraint, rightConstraint, topConstraint, heightConstraint]];
    
    //iOS 8.0以後設置active屬性值
    //leftConstraint.active = YES;
    //rightConstraint.active = YES;
    //topConstraint.active = YES;
    //heightConstraint.active = YES;
    
    //iOS 8.0以後推薦使用activateConstraints:方法,效率比單獨設置active屬性要高
    [NSLayoutConstraint activateConstraints:@[leftConstraint, rightConstraint, topConstraint, heightConstraint]];
    
    UIScrollView* scrollView = [UIScrollView new];
    scrollView.translatesAutoresizingMaskIntoConstraints = NO;
    [self.view addSubview:scrollView];
    
    //scrollView左側與父視圖左側對齊
    NSLayoutConstraint* scrollLeftConstraint = [NSLayoutConstraint constraintWithItem:scrollView attribute:NSLayoutAttributeLeading relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeLeading multiplier:1.0f constant:0.0f];
    
    //scrollView右側與父視圖右側對齊
    NSLayoutConstraint* scrollRightConstraint = [NSLayoutConstraint constraintWithItem:scrollView attribute:NSLayoutAttributeTrailing relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeTrailing multiplier:1.0f constant:0.0f];
    
    //scrollView底部與父視圖底部對齊
    NSLayoutConstraint* scrollBottomConstraint = [NSLayoutConstraint constraintWithItem:scrollView attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeBottom multiplier:1.0f constant:0.0f];
    
    //scrollView頂部與logoImageView底部對齊
    NSLayoutConstraint* scrollTopConstraint = [NSLayoutConstraint constraintWithItem:scrollView attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:logoImageView attribute:NSLayoutAttributeBottom multiplier:1.0f constant:0.0f];
    
    [NSLayoutConstraint activateConstraints:@[scrollLeftConstraint, scrollRightConstraint, scrollBottomConstraint, scrollTopConstraint]];
    
    UILabel* nameLabel = [UILabel new];
    nameLabel.translatesAutoresizingMaskIntoConstraints = NO;
    nameLabel.text = @"蘋果公司";
    nameLabel.backgroundColor = [UIColor greenColor];
    [scrollView addSubview:nameLabel];
    
    UILabel* descriptionLabel = [UILabel new];
    descriptionLabel.translatesAutoresizingMaskIntoConstraints = NO;
    descriptionLabel.text = @"蘋果公司(Apple Inc. )是美國的一家高科技公司。由史蒂夫·喬布斯、斯蒂夫·沃茲尼亞克和羅·韋恩(Ron Wayne)等三人於1976年4月1日創立,並命名爲美國蘋果電腦公司(Apple Computer Inc. ), 2007年1月9日更名爲蘋果公司,總部位於加利福尼亞州的庫比蒂諾。\n蘋果公司創立之初主要開發和銷售的個人電腦,截至2014年致力於設計、開發和銷售消費電子、計算機軟件、在線服務和個人計算機。蘋果的Apple II於1970年代助長了個人電腦革命,其後的Macintosh接力於1980年代持續發展。該公司硬件產品主要是Mac電腦系列、iPod媒體播放器、iPhone智能手機和iPad平板電腦;在線服務包括iCloud、iTunes Store和App Store;消費軟件包括OS X和iOS操作系統、iTunes多媒體瀏覽器、Safari網絡瀏覽器,還有iLife和iWork創意和生產力套件。蘋果公司在高科技企業中以創新而聞名世界。\n蘋果公司1980年12月12日公開招股上市,2012年創下6235億美元的市值記錄,截至2014年6月,蘋果公司已經連續三年成爲全球市值最大公司。蘋果公司在2014年世界500強排行榜中排名第15名。2013年9月30日,在宏盟集團的“全球最佳品牌”報告中,蘋果公司超過可口可樂成爲世界最有價值品牌。2014年,蘋果品牌超越谷歌(Google),成爲世界最具價值品牌 。";
    descriptionLabel.numberOfLines = 0;
    descriptionLabel.backgroundColor = [UIColor yellowColor];
    [scrollView addSubview:descriptionLabel];
    
    //nameLabel左側與父視圖左側對齊
    NSLayoutConstraint* nameLabelLeftConstraint = [NSLayoutConstraint constraintWithItem:nameLabel attribute:NSLayoutAttributeLeading relatedBy:NSLayoutRelationEqual toItem:scrollView attribute:NSLayoutAttributeLeading multiplier:1.0f constant:0.0f];
    
    //nameLabel右側與父視圖右側對齊
    NSLayoutConstraint* nameLabelRightConstraint = [NSLayoutConstraint constraintWithItem:nameLabel attribute:NSLayoutAttributeTrailing relatedBy:NSLayoutRelationEqual toItem:scrollView attribute:NSLayoutAttributeTrailing multiplier:1.0f constant:0.0f];
    
    //nameLabel底部與descriptionLabel頂部對齊
    NSLayoutConstraint* nameLabelBottomConstraint = [NSLayoutConstraint constraintWithItem:nameLabel attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual toItem:descriptionLabel attribute:NSLayoutAttributeTop multiplier:1.0f constant:0.0f];
    
    //nameLabel頂部與父視圖頂部對齊
    NSLayoutConstraint* nameLabelTopConstraint = [NSLayoutConstraint constraintWithItem:nameLabel attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:scrollView attribute:NSLayoutAttributeTop multiplier:1.0f constant:0.0f];
    
    //nameLabel高度爲20
    NSLayoutConstraint* nameLabelHeightConstraint = [NSLayoutConstraint constraintWithItem:nameLabel attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1.0f constant:20.0f];
    
    [NSLayoutConstraint activateConstraints:@[nameLabelLeftConstraint, nameLabelRightConstraint, nameLabelBottomConstraint, nameLabelTopConstraint, nameLabelHeightConstraint]];
    
    //descriptionLabel左側與父視圖左側對齊
    NSLayoutConstraint* descriptionLabelLeftConstraint = [NSLayoutConstraint constraintWithItem:descriptionLabel attribute:NSLayoutAttributeLeading relatedBy:NSLayoutRelationEqual toItem:scrollView attribute:NSLayoutAttributeLeading multiplier:1.0f constant:0.0f];
    
    //descriptionLabel右側與父視圖右側對齊
    NSLayoutConstraint* descriptionLabelRightConstraint = [NSLayoutConstraint constraintWithItem:descriptionLabel attribute:NSLayoutAttributeTrailing relatedBy:NSLayoutRelationEqual toItem:scrollView attribute:NSLayoutAttributeTrailing multiplier:1.0f constant:0.0f];
    
    //descriptionLabel底部與父視圖底部對齊
    NSLayoutConstraint* descriptionLabelBottomConstraint = [NSLayoutConstraint constraintWithItem:descriptionLabel attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual toItem:scrollView attribute:NSLayoutAttributeBottom multiplier:1.0f constant:0.0f];
    
    [NSLayoutConstraint activateConstraints:@[descriptionLabelLeftConstraint, descriptionLabelRightConstraint, descriptionLabelBottomConstraint]];
    
    //nameLabel寬度與logoImageView寬度相等
    NSLayoutConstraint* nameLabelWidthConstraint = [NSLayoutConstraint constraintWithItem:nameLabel attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:logoImageView attribute:NSLayoutAttributeWidth multiplier:1.0f constant:0.0f];
    
    //nameLabel寬度與logoImageView寬度相等
    NSLayoutConstraint* descriptionLabelWidthConstraint = [NSLayoutConstraint constraintWithItem:descriptionLabel attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:logoImageView attribute:NSLayoutAttributeWidth multiplier:1.0f constant:0.0f];
    
    [NSLayoutConstraint activateConstraints:@[nameLabelWidthConstraint, descriptionLabelWidthConstraint]];
}

程序最終項目文件鏈接:https://github.com/puckerxp/AutoLayoutSeries
(Demo位於AutoLayoutByConstraint目錄下)。

自動佈局約束是通過描述視圖間的關係而非強加座標值來進行定位的,它更能滿足不同設備尺寸的界面佈局,並且更容易讓人理解。雖然上面的代碼很冗長,但每一句所描述的事實都十分清楚。在此省略自動佈局的好處10000字。。。

佈局錨點

可能你還是覺得,區區幾個簡單的視圖,就要寫這麼長的代碼。。。
。。。
每次生成NSLayoutConstraint實例時都需要傳入7個參數,每個視圖又至少需要創建4個NSLayoutConstraint實例,這確實是一件比較辛苦的事情。其實大多數情況下,我們只是關心對齊或者居中,幾乎不怎麼修改multiplier(默認爲1.0f)和constant的值(默認爲0.0f)。有沒有簡化方法呢?那你不妨試試佈局錨點表示法,這種方式犧牲了部分靈活性,但使得代碼簡化易讀。

iOS 9爲每個UIView對象添加了若干錨點屬性,表示該視圖的上下左右邊緣,以及水平中心、垂直中心、基準線等特殊位置,用來建立約束。

@interface UIView (UIViewLayoutConstraintCreation)
@property(nonatomic,readonly,strong) NSLayoutXAxisAnchor *leadingAnchor NS_AVAILABLE_IOS(9_0);
@property(nonatomic,readonly,strong) NSLayoutXAxisAnchor *trailingAnchor NS_AVAILABLE_IOS(9_0);
@property(nonatomic,readonly,strong) NSLayoutXAxisAnchor *leftAnchor NS_AVAILABLE_IOS(9_0);
@property(nonatomic,readonly,strong) NSLayoutXAxisAnchor *rightAnchor NS_AVAILABLE_IOS(9_0);
@property(nonatomic,readonly,strong) NSLayoutYAxisAnchor *topAnchor NS_AVAILABLE_IOS(9_0);
@property(nonatomic,readonly,strong) NSLayoutYAxisAnchor *bottomAnchor NS_AVAILABLE_IOS(9_0);
@property(nonatomic,readonly,strong) NSLayoutDimension *widthAnchor NS_AVAILABLE_IOS(9_0);
@property(nonatomic,readonly,strong) NSLayoutDimension *heightAnchor NS_AVAILABLE_IOS(9_0);
@property(nonatomic,readonly,strong) NSLayoutXAxisAnchor *centerXAnchor NS_AVAILABLE_IOS(9_0);
@property(nonatomic,readonly,strong) NSLayoutYAxisAnchor *centerYAnchor NS_AVAILABLE_IOS(9_0);
@property(nonatomic,readonly,strong) NSLayoutYAxisAnchor *firstBaselineAnchor NS_AVAILABLE_IOS(9_0);
@property(nonatomic,readonly,strong) NSLayoutYAxisAnchor *lastBaselineAnchor NS_AVAILABLE_IOS(9_0);
@end

通過這些佈局錨點就可以很方便地創建佈局,直接看代碼吧:

// headerImageView固定高度100pt
[headerImageView.heightAnchor 
    constraintEqualToConstant:100.0f].active = YES;

// logoImageView高度爲父視圖高度一半
[logoImageView.heightAnchor 
    constraintEqualToAnchor:logoImageView.superView.heightAnchor 
    multiplier:0.5f].active = YES;

// 按鈕5頂部與按鈕8底部對齊
[button5.topAnchor 
    constraintEqualToAnchor:button8.bottomAnchor].active = YES;

是不是簡單多了?
這些錨點屬性是隻讀的,能夠獲取到NSLayoutAnchor子類的實例,類圖關係如下:
在這裏插入圖片描述

這些類提供了創建NSLayoutConstraint實例的各種方法:
在這裏插入圖片描述

總結

通過編寫代碼來創建約束是必須要掌握的技巧,不過確實是稍微麻煩了一些,基本上都需要修改並檢查好幾次,又調試了好幾次才完全寫對。爲了能夠提高成功率,本人強烈建議將每個視圖的4個必要約束放在一起寫。這樣一旦出現問題,你也好調試和定位。
另外,很多人覺得蘋果提供的添加約束方法不夠簡潔優雅,畢竟之前一句setFrame搞定的事,現在要分成4句來寫,每句還那麼多參數。我認爲是他們沒有找到好的方法,如果他們能夠善用Code Snippet,那麼我想他們沒有任何理由再喜歡setFrame:CGRectMake…或者找第三方庫如Masonry之類的了(我之前的項目從未用過Masonry之類的第三方佈局庫,因爲當你熟悉之後,原生完全夠用)。(關於Code Snippet我會再寫一篇文章來詳細介紹)

在下一篇文章中,我將介紹另一種更簡潔的方式,即使用VFL來添加約束,敬請期待吧。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章