天天看點

iOS 中 Auto Layout(自動布局)

Auto Layout 是什麼?

我的了解:Auto Layout 是一種基于限制的布局系統,它可以根據你在元素(對象)上設定的限制自動調整元素(對象)的位置和大小。

官方的說明:

Auto Layout 是一個系統,可以讓你通過建立元素之間關系的數學描述來布局應用程式的使用者界面。——《Auto Layout Guide》

Auto Layout 是一種基于限制的,描述性的布局系統。——《Taking Control of Auto Layout in Xcode 5 - WWDC 2013》

這裡有幾個關鍵字:

  • 元素
  • 關系
  • 限制
  • 描述

元素(Element)

低頭看看你電腦的鍵盤,你可以把每一個按鍵當做一個元素;對于 iOS 系統來說,你可以把桌面上每一個應用圖示當做一個元素;對于某一款 iOS 應用來說,你可以把視圖中的每一個子視圖當做一個元素。

事實上,你也可以把整個鍵盤、桌面或者視圖當做一個元素。

關系(Relation)

元素之間可以有關系。例如在鍵盤上 

Q

 鍵和 

W

 鍵之間有關系。是什麼關系呢?有很多,例如 

Q

 鍵在 

W

 鍵的左邊,

W

 鍵在 

Q

 鍵的右邊,

Q

 鍵和 

W

 鍵之間相距 0.5 厘米等等。

不了解?試着把鍵盤想象成 

View

,把按鍵想象成 

Button

,再思考一遍。

限制(Constraint)

元素之間關系的限制。限制是 Auto Layout 系統中最重要的概念。我們上面提到的 

左邊

右邊

 以及

相距 0.5 厘米

 等這些都是限制,它們限制了元素之間的關系。

描述(Description)

定義限制來限制元素之間的關系。描述定義了元素之間的關系及限制。

繼續用鍵盤舉例,

Q

 鍵的長寬均為 1 厘米,左邊距離鍵盤的左邊緣 10 厘米,上邊距離鍵盤的頂部 5 厘米。這句話就可以定位 

Q

 鍵在鍵盤中的位置,很輕松就可以計算出 

Q

 鍵的 

frame

 為

{{10.0, 5.0}, {1.0, 1.0}}

現在 

Q

 鍵的坐标已經确定,那麼 

W

 鍵的坐标可以這樣描述:頂部和 

Q

 鍵對齊,大小和 

Q

 鍵相等,位于 

Q

 鍵右側 0.5 厘米處。仔細想想,這句話中包含了元素間的關系,關系間的限制,可以直接計算出 

W

 鍵的 

frame

忘掉傳統的 Springs & Struts 布局方式

事實上如果你用傳統的設定 frame 的布局方式的思維來了解上面的 

Q

 鍵和 

W

 鍵的布局也說的通。

因為在 Auto Layout 中,當你描述完之後, Auto Layout 會自動幫你計算出 frame。換句話說,你的描述告訴了 Auto Layout 如何幫你計算出 frame。是以,你也可以了解為你間接的設定了 frame。為什麼要這麼做呢?為什麼不直接設定 frame?這是因為使用 Auto Layout 有很多好處:

  • 多數情況下旋轉螢幕不用再做額外的處理
  • 更容易适配不同尺寸的螢幕
  • 上手後布局非常簡單容易,布局邏輯更清晰

Auto Layout 和傳統布局很大的不同之處在于它是一種相對的布局方式。怎麼了解這句話?上面提到

W

 鍵位于 

Q

 鍵右側 0.5 厘米處。

傳統的布局無法直接表示,你必須把這種布局手動轉換為傳統布局代碼。例如上面的 

Q

 鍵和 

W

 鍵的傳統布局代碼看起來可能是這樣:

q.frame = CGRectMake(CGRectGetMinX(keyBoard.frame) + 10.f, CGRectGetMinY(keyBoard.frame) + 5.f, 1.f, 1.f);
w.frame = CGRectMake(CGRectGetMaxX(q.frame) + 0.5f, CGRectGetMinY(q.frame), CGRectGetWidth(q.frame), CGRectGetHeight(q.frame));
           

使用 Auto Layout 的布局代碼看起來像這樣:

// 僞代碼
q.width = 1.f;
q.height = 1.f;
q.left = keyboard.left + 10.f;
q.top = keyboard.top + 5.f;

w.top = q.top;
w.width = q.width;
w.height = q.height;
w.left = q.right + .5f;
           

Auto Layout 不僅能輕松表示這種布局,而且相對于傳統的布局更清晰簡潔易懂,還免費附贈很多優點,有什麼理由不使用 Auto Layout 呢?

實踐中我發現對于很多新手來說,Auto Layout 這種布局方式比較容易了解接受,相反很多對傳統布局很熟練的人卻不太容易了解,總是用傳統布局的思維來思考,是以如果可能的話,我建議你暫時忘掉傳統的布局方式。

Autoresizing Mask

事實上我不打算講這個東西,以及它和 Auto Layout 的差別和聯系。如果你不知道,對學習 Auto Layout 不會有什麼影響。

你唯一需要注意的是在使用 Auto Layout 時,首先需要将視圖的

translatesAutoresizingMaskIntoConstraints

 屬性設定為 

NO

。這個屬性預設為

YES

,如果你是使用 Xib 的話,這個屬性會自動幫你設定為 

NO

。當它為 

YES

 時,運作時系統會自動将 Autoresizing Mask 轉換為 Auto Layout 的限制,這些限制很有可能會和我們自己添加的産生沖突。

Auto Layout 基礎知識

無論是在 Xib 中還是代碼中使用 Auto Layout,你都需要了解 Auto Layout 的一些必要知識。這些你現在不了解沒有關系,後面我們會詳細講述。

限制 (Constraint)

Auto Layout 中限制對應的類為 

NSLayoutConstraint

,一個 

NSLayoutConstraint

 執行個體代表一條限制。

NSLayoutConstraint

 有兩個方法,第一個是

+ (id)constraintWithItem:(id)view1
               attribute:(NSLayoutAttribute)attribute1
               relatedBy:(NSLayoutRelation)relation
                  toItem:(id)view2
               attribute:(NSLayoutAttribute)attribute2
              multiplier:(CGFloat)multiplier
                constant:(CGFloat)constant;
           

不要被這個方法的參數吓到,實際上它隻做一件事,就是讓 

view1

 的某個 

attribute

 等于

view2

 的某個 

attribute

 的 

multiplier

 倍加上 

constant

這裡的 

attribute

可以是上下左右寬高等等。

精簡後就是下面這個公式:

view1.attribute1 = view2.attribute2 × multiplier + constant
           

還有一個參數是 

relation

,這是一個關系參數,它标明了上面這個公式兩邊的關系,它可以是

小于等于 (≤)

等于 (=)

大于等于 (≥)

。上面的公式假定了這個參數傳入的是 

=

,根據參數的不同,公式中的關系符号也不同。

需要注意的是,

 或 

 優先會使用 

=

 關系,如果 

=

 不能滿足,才會使用 

<

 或 

>

。例如設定一個 

≥ 100

 的關系,預設會是 100,當視圖被拉伸時,100 無法被滿足,尺寸才會變得更大。

例子:

1、我們要實作一個如下圖的布局。

iOS 中 Auto Layout(自動布局)

布局代碼如下:

UIView *view = [UIView new];
[view setBackgroundColor:[UIColor redColor]];
[self.view addSubview:view];

CGRect viewFrame = CGRectMake(50.f, 100.f, 150.f, 150.f);

// 使用 Auto Layout 布局
[view setTranslatesAutoresizingMaskIntoConstraints:NO];

// `view` 的左邊距離 `self.view` 的左邊 50 點.
NSLayoutConstraint *viewLeft = [NSLayoutConstraint constraintWithItem:view
                                                            attribute:NSLayoutAttributeLeading
                                                            relatedBy:NSLayoutRelationEqual
                                                               toItem:self.view
                                                            attribute:NSLayoutAttributeLeading
                                                           multiplier:1
                                                             constant:CGRectGetMinX(viewFrame)];
// `view` 的頂部距離 `self.view` 的頂部 100 點.
NSLayoutConstraint *viewTop = [NSLayoutConstraint constraintWithItem:view
                                                           attribute:NSLayoutAttributeTop
                                                           relatedBy:NSLayoutRelationEqual
                                                              toItem:self.view
                                                           attribute:NSLayoutAttributeTop
                                                          multiplier:1
                                                            constant:CGRectGetMinY(viewFrame)];
// `view` 的寬度 是 60 點.
NSLayoutConstraint *viewWidth = [NSLayoutConstraint constraintWithItem:view
                                                             attribute:NSLayoutAttributeWidth
                                                             relatedBy:NSLayoutRelationGreaterThanOrEqual
                                                                toItem:nil
                                                             attribute:NSLayoutAttributeNotAnAttribute
                                                            multiplier:1
                                                              constant:CGRectGetWidth(viewFrame)];
// `view` 的高度是 60 點.
NSLayoutConstraint *viewHeight = [NSLayoutConstraint constraintWithItem:view
                                                              attribute:NSLayoutAttributeHeight
                                                              relatedBy:NSLayoutRelationGreaterThanOrEqual
                                                                 toItem:nil
                                                              attribute:NSLayoutAttributeNotAnAttribute
                                                             multiplier:1
                                                               constant:CGRectGetHeight(viewFrame)];
// 把限制添加到父視圖上.
[self.view addConstraints:@[viewLeft, viewTop, viewWidth, viewHeight]];
           

實作一個如此簡單的布局竟然要寫這麼多的代碼,這顯然難于推廣使用。于是 UIKit 團隊發明了另外一種更簡便的表達方式進行布局,這個我們後面再講,現在先看看這段代碼。

首先我把 

view

 的 

translatesAutoresizingMaskIntoConstraints

 設為了 

NO

,禁止将 Autoresizing Mask 轉換為限制。

然後在設定 

viewLeft

 這個限制時,

attribute

 參數使用了 

NSLayoutAttributeLeading

而不是 

NSLayoutAttributeLeft

,這兩個參數值都表示左邊,但它們之間的差別在于

NSLayoutAttributeLeft

 永遠表示左邊,但 

NSLayoutAttributeLeading

 是根據習慣區分的,例如在某些文字從右向左閱讀的地區,例如阿拉伯,

NSLayoutAttributeLeading

 表示右邊。換句話說,

NSLayoutAttributeLeading

 是表示文字開始的方向。在英文、中文這種從左往右閱讀的文字中它表示左邊,在像阿拉伯語、希伯來語這種從右往左閱讀的文字中它表示右邊。通常情況下,除非你明确要限制在左邊,否則你都應該使用 

NSLayoutAttributeLeading

 表示左邊。相對的,表示右邊也類似這樣。這對于我們的本地化工作有很大的幫助。

然後在設定 

viewWidth

 和 

viewHeight

 這兩個限制時,

relatedBy

 參數使用的是

NSLayoutRelationGreaterThanOrEqual

 而不是 

NSLayoutRelationEqual

因為 Auto Layout 是相對布局,是以通常你不應該直接設定寬度和高度這種固定不變的值,除非你很确定視圖的寬度或高度需要保持不變。

如果一定要設定高度或寬度,特别是寬度,在沒有顯式地設定内容壓縮優先級(Content Hugging Priority,後面會講到)和内容抗壓縮優先級(Content Compression Resistance Priority,後面會講到)的情況下,盡量不要使用 

NSLayoutRelationEqual

 這種絕對的關系,這會帶來許多潛在的問題:

  • 根據内容決定寬度的視圖,當内容改變時,外觀尺寸無法做出正确的改變
  • 在本地化時過長的文字無法顯示,造成文字切斷,或文字過短,寬度顯得過寬,影響美觀
  • 添加了多餘的限制時,限制之間沖突,無法顯示正确的布局

所帶來的問題不僅僅局限與這幾條,這裡隻是簡單列出幾條。

如何正确的設定寬度或高度?給出一些 Tips:

  • 如果寬度和高度布局可以改變,使用固有内容尺寸(Intrinsic Content Size,後面會講到)設定限制(即 size to fit size)。
  • 如果寬度和高度布局不可以改變,改變限制的關系為 

  • 調整壓縮優先級和内容抗壓縮優先級

最後我把所有限制都添加到了 

view

 的父視圖 

self.view

 上。

view

 的限制為什麼不添加到自身而添加到别的視圖上去呢?這是由于限制是根據視圖層級自下而上更新的,也就是從子視圖到父視圖。是以 Auto Layout 添加限制有一套自己的規則,如下:

  • 兩個同層級間視圖的限制,添加到它們共同的父視圖上
iOS 中 Auto Layout(自動布局)
  • 兩個不同層級間視圖的限制,添加到它們最近的共同的父視圖上
iOS 中 Auto Layout(自動布局)
  • 兩個有層級關系的視圖的限制,添加到層次較高的視圖上(父視圖)上
iOS 中 Auto Layout(自動布局)

因為我們屬于最後一種情況,是以子視圖 

view

 的限制添加到了父視圖 

self.view

 上。

接下來是第二個方法

+ (NSArray *)constraintsWithVisualFormat:(NSString *)format 
                                 options:(NSLayoutFormatOptions)opts 
                                 metrics:(NSDictionary *)metrics 
                                   views:(NSDictionary *)views;
           

這個方法是我們實際程式設計中最常用的方法。它會根據我們指定的參數傳回一組限制。 這個方法很重要,是以我會詳細解釋每個參數的用途。

format

這個參數存放的是布局邏輯,布局邏輯是使用 可視化格式語言 (VFL) 編寫的。實際程式設計中我們也是使用

VFL

 編寫布局邏輯,因為第一個方法明顯參數過多,一個簡單的布局要寫很多代碼。

上一個布局使用 

VFL

 來重構的話,代碼如下:

....
[view setTranslatesAutoresizingMaskIntoConstraints:NO];
NSDictionary *views = NSDictionaryOfVariableBindings(self.view, view);
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-50-[view(>=150)]" options:0 metrics:nil views:views]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-100-[view(>=150)]" options:0 metrics:nil views:views]];
           

嘩,代碼量減少了很多。首先我們使用 

NSDictionaryOfVariableBindings(...)

 宏建立了一個字典 

views

,這個宏會自動把傳入的對象的鍵路徑作為字典的鍵,把對象作為字典的值。是以

views

 字典的内容就像這樣:

{@"self.view": self.view, @"view", view}
           

VFL

 就是這兩句:

H:|-50-[view(>=150)]

V:|-100-[view(>=150)]

第一句是在水準方向布局,表示 

view

 左邊距離父視圖左邊 50 點,寬度至少 150 點。(水準方向是寬度)

第二句是在垂直方向上布局,表示 

view

 頂部距離父視圖頂部 100 點,寬度至少 150 點。(垂直方向是高度)

分解說明如下:

H

 / 

V

 表示布局方向。

H

 表示水準方向(Horizontal),

V

 表示垂直方向(Vertical),方向後要緊跟一個 

:

,不能有空格。

|

 表示父視圖。通常出現在語句的首尾。

-

 有兩個用途,單獨一個表示标準距離。這個值通常是 8 ;兩個中間夾着數值,表示使用中間的數值代替标準距離,如第一句的 

-50-

,就是使用 50 來代替标準距離。

[]

 表示對象,括号中間需要填上對象名,對象名必須是我們傳入的 

views

 字典中的鍵。對象名後可以跟小括号 

()

,小括号中是對此對象的尺寸和優先級限制。水準布局中尺寸是寬度,垂直布局中尺寸是高度。如第一句中的 

(>=150)

 就是對 

view

 尺寸的限制,因為是水準方向布局,是以它表示寬度大于或等于 150 點。而 150 前面的 

>=

 就是我們上面第一個方法中提到的關系參數。至于為什麼這裡使用

>=

,上面已經解釋過了。括号中可以包含多條限制,如果我們想再加一條限制,保證 

view

 的寬度最大不超過 200 點,我們可以這樣寫:

H:|-50-[view(>=150,<=200)]

。還可以添加優先級限制,這個我們後面再講。

VFL

 文法有幾點需要注意:

  • 布局語句中不能包含空格
  • 和關系一樣,沒有 

    >

    <

     這種限制

然後下面是一些例子,增加你對 

VFL

 文法的了解。

例一:

我們在 

view

 右側添加另一個視圖 

view2

,效果如圖:

iOS 中 Auto Layout(自動布局)

代碼如下:

UIView *view = [UIView new];
[view setBackgroundColor:[UIColor redColor]];
[self.view addSubview:view];

UIView *view2 = [UIView new];
[view2 setBackgroundColor:[UIColor blueColor]];
[self.view addSubview:view2];

[view setTranslatesAutoresizingMaskIntoConstraints:NO];
[view2 setTranslatesAutoresizingMaskIntoConstraints:NO];

NSDictionary *views = NSDictionaryOfVariableBindings(self.view, view, view2);
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-50-[view(>=150)]" options:0 metrics:nil views:views]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-100-[view(>=150)]" options:0 metrics:nil views:views]];

[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:[view]-[view2(>=50)]" options:0 metrics:nil views:views]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-100-[view2(>=50)]" options:0 metrics:nil views:views]];
           

我們講講最後的兩條新的 

VFL

 語句:

H:[view]-[view2(>=50)]

從開始的 

H:

 我們可以判斷出這是水準方向的布局,換句話說就是設定視圖的 

x

 和 

width

。接着的 

[view]

,說明後面的所有視圖都是在 

view

 的右側;接着是 

-

,說明後一個視圖和 

view

之間有一個标準距離的間距;也就是說 x 等于 

view

 的右側再加上标準距離,即

CGRectGetMaxX(view) + 标準距離

。最後是 

[view2(>=50)]

,這裡可以看出後一個視圖是

view2

,并且它的寬度不小于 50 點。整一句翻譯成白話就是說:在水準方向上,

view2

 在

view

 右側的标準距離位置處,并且它的寬度不小于 50 點。

V:|-100-[view2(>=50)]

從開始的 

V:

 我們可以判斷出這是垂直方向的布局,換句話說就是設定視圖的 

y

 和 

height

。接着的 

|

 說明是後一個視圖是相對于父視圖進行布局;接着是 

-100-

,說明垂直方向和父視圖(頂部)相距 100 點,也就是說 y 等于 100 點。最後是 

[view2(>=50)]

,這和上一句相同,隻是因為是垂直方向,是以 50 是設定高度而不是寬度。整一句翻譯成白話就是說:在垂直方向上,

view2

 在相對于父視圖(頂部) 100 點的位置處,并且它的高度不小于 50 點。

實際上我們的代碼還可以簡化:

......
NSDictionary *views = NSDictionaryOfVariableBindings(self.view, view, view2);
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-50-[view(>=150)]-[view2(>=50)]" options:0 metrics:nil views:views]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-100-[view(>=150)]" options:0 metrics:nil views:views]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-100-[view2(>=50)]" options:0 metrics:nil views:views]];
           

因為兩個視圖水準方向上是并排(從左到右)的,是以我們可以将水準方向布局的代碼合并到一起。而垂直方向我們并非并排的,是以垂直方向的布局代碼我們不能合并。這裡所講的并排的意思是後一個在前一個的後面,水準方向上明顯是這樣,但垂直方向上兩個視圖的 

y

 是相同的,是以無法合并在一起布局。

例二:我們繼續添加一個視圖 

view3

 填補 

view

 右下方的空缺,效果如圖:

iOS 中 Auto Layout(自動布局)

代碼如下:

UIView *view = [UIView new];
[view setBackgroundColor:[UIColor redColor]];
[self.view addSubview:view];

UIView *view2 = [UIView new];
[view2 setBackgroundColor:[UIColor blueColor]];
[self.view addSubview:view2];

UIView *view3 = [UIView new];
[view3 setBackgroundColor:[UIColor orangeColor]];
[self.view addSubview:view3];

[view setTranslatesAutoresizingMaskIntoConstraints:NO];
[view2 setTranslatesAutoresizingMaskIntoConstraints:NO];
[view3 setTranslatesAutoresizingMaskIntoConstraints:NO];

NSDictionary *views = NSDictionaryOfVariableBindings(self.view, view, view2, view3);
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-(50)-[view(>=150)]-[view2(>=50)]" options:0 metrics:nil views:views]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-(100)-[view(>=150)]" options:0 metrics:nil views:views]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:[view]-[view3(>=50)]" options:0 metrics:nil views:views]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-(100)-[view2(>=50)][view3(>=100)]" options:0 metrics:nil views:views]];
           

你可能注意到我把每個間距都使用小括号闊了起來,這是可選的,你完全可以直接寫間距,這麼寫隻是告訴你還有這種文法。實際上沒什麼必要這麼寫,因為 

VFL

 文法并不支援運算,例如把 

(50)

 切分為

(10+40)

 或 

(5*10)

 都是不合法的。

最後兩行是 

view3

 的布局代碼,簡單解釋一下:

H:[view]-[view3(>=50)]

水準方向布局,

view3

 在 

view

 右側标準距離處,并且寬度不小于 50 點。

V:|-(100)-[view2(>=50)][view3(>=100)]

垂直方向布局,

view2

 距離父視圖(頂部)100 點,并且高度不小于 50 點;

view3

 緊挨着

view2

 底部(沒有 

-

),并且高度不小于 100 點。

options

這個參數的值是位掩碼,使用頻率并不高,但非常有用。它可以操作在 

VFL

 語句中的所有對象的某一個屬性或方向。例如上面的例一,水準方向有兩個視圖,它們的垂直方向到頂部的距離相同,或者說頂部對齊,我們就可以給這個參數傳入 

NSLayoutFormatAlignAllTop

 讓它們頂部對齊,這樣以來隻需要指定兩個視圖的其中一個的垂直方向到頂部的距離就可以了。代碼:

......
NSDictionary *views = NSDictionaryOfVariableBindings(self.view, view, view
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-50-[view(>=150)]-[view2(>=50)]" options:NSLayoutFormatAlignAllTop metrics:nil views:views]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-100-[view(>=150)]" options:0 metrics:nil views:views]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:[view2(>=50)]" options:0 metrics:nil views:views]];
           

它的預設值是 

NSLayoutFormatDirectionLeadingToTrailing

,根據目前使用者的語言環境進行設定,比如英文中就是從左到右,希伯來語中就是從右到左。

這個值符合我們常用的選項。

NSLayoutFormatDirectionLeadingToTrailing

 的值是 

0 << 16

,是以我們可以直接傳入 

 使用此值。

因為是位掩碼,是以我們可以使用 

|

 進行多選,例如例一,我們希望在現有限制的基礎上讓兩個視圖的高度相等,那代碼可以這樣寫:

......
NSDictionary *views = NSDictionaryOfVariableBindings(self.view, view, view2);
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-50-[view(>=150)]-[view2(>=50)]" options:NSLayoutFormatAlignAllTop | NSLayoutFormatAlignAllBottom metrics:nil views:views]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-100-[view(>=150)]" options:0 metrics:nil views:views]];
           

指定兩個視圖的頂部和底部限制相同,然後隻設定其中一個視圖的相關限制即可。

靈活使用此參數可以節省不少時間,但這個參數内容太多,如果你有興趣了解,可以看看我的另一篇博文:《Auto Layout 中的排列選項》

metrics

這是一個字典,字典的鍵必須是出現在 

VFL

 語句中的字元串,值必須是 

NSNumber

 類型,作用是将在 

VFL

 語句中出現的鍵替換為相應的值。例如本文中的第一個布局的例子,使用了這個參數後代碼就變成了這樣:

UIView *view = [UIView new];
[view setBackgroundColor:[UIColor redColor]];
[self.view addSubview:view];

[view setTranslatesAutoresizingMaskIntoConstraints:NO];

CGRect viewFrame = CGRectMake(50.f, 100.f, 150.f, 150.f);

NSDictionary *views = NSDictionaryOfVariableBindings(self.view, view);

NSDictionary *metrics = @{@"left": @(CGRectGetMinX(viewFrame)),
                          @"top": @(CGRectGetMinY(viewFrame)),
                          @"width": @(CGRectGetWidth(viewFrame)),
                          @"height": @(CGRectGetHeight(viewFrame))};

[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-left-[view(>=width)]" options:0 metrics:metrics views:views]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-top-[view(>=height)]" options:0 metrics:metrics views:views]];
           

聰明的你看了這段代碼後肯定已經明白這個參數的用途了,雖然使用頻率不高,但依然很有用,特别是要動态計算限制值的時候非常有用。

實際上這個參數也可以使用 

NSDictionaryOfVariableBindings(...)

 宏來快速建立,代碼如下:

......
[view setTranslatesAutoresizingMaskIntoConstraints:NO];

NSNumber *left = @50.f;
NSNumber *top = @100.f;
NSNumber *width = @150.f;
NSNumber *height = @150.f;

NSDictionary *views = NSDictionaryOfVariableBindings(self.view, view);
NSDictionary *metrics = NSDictionaryOfVariableBindings(left, top, width, height);

[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-left-[view(>=width)]" options:0 metrics:metrics views:views]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-top-[view(>=height)]" options:0 metrics:metrics views:views]];
           

views

又是一個字典,包含了 

VFL

 語句中用到的視圖。字典的鍵必須是出現在 

VFL

 語句中的視圖名稱,值必須視圖的執行個體。這個字典我們在講 

format

 時已經講過,也用過很多次,相信你早已明白是怎麼回事了。

講了這麼多,可能你也發現了,隻要學會了 

VFL

 文法,就可以友善地使用 Auto Layout 了,其他的知識都屬于輔助選項,會的話,布局更輕松一些,不會也沒關系,實踐多了,自然就會了。

優先級 (Priority level)

限制條件有優先級,高優先級限制會比低優先級限制優先得到滿足,系統内置了 4 個優先級:

enum {
    UILayoutPriorityRequired = 1000,
    UILayoutPriorityDefaultHigh = 750,
    UILayoutPriorityDefaultLow = 250,
    UILayoutPriorityFittingSizeLevel = 50,
};
typedef float UILayoutPriority;
           
  • UILayoutPriorityRequired 這是預設值,這意味着這個限制條件必須被精确地滿足。
  • UILayoutPriorityDefaultHigh
  • UILayoutPriorityDefaultLow
  • UILayoutPriorityFittingSizeLevel 這是内置的最低優先級。

相信你已經看到每個等級的數值了,優先級的取值在 

0 ~ 1000

 之間,取值越大,優先級越高,越會被優先滿足。

每個限制的預設優先級就是 

UILayoutPriorityRequired

,這意味着你給出的所有限制都必須得到滿足,一旦限制間發生沖突,你的應用就會 Crash。這也是在使用 Auto Layout 時經常會犯的錯誤:沒有給限制設定适當的優先級。

舉個例子說明優先級設定不當的情況,給我們首次使用 Auto Layout 時的例子再添加一個限制:

......

// `view` 的高度是 60 點.
NSLayoutConstraint *viewHeight = [NSLayoutConstraint constraintWithItem:view
                                                              attribute:NSLayoutAttributeHeight
                                                              relatedBy:NSLayoutRelationGreaterThanOrEqual
                                                                 toItem:nil
                                                              attribute:NSLayoutAttributeNotAnAttribute
                                                             multiplier:1
                                                               constant:CGRectGetHeight(viewFrame)];
// `view` 緊貼着 `self.view` 的左邊.
NSLayoutConstraint *marginLeft = [NSLayoutConstraint constraintWithItem:view
                                                              attribute:NSLayoutAttributeLeading
                                                              relatedBy:NSLayoutRelationEqual
                                                                 toItem:self.view
                                                              attribute:NSLayoutAttributeLeading
                                                             multiplier:1
                                                               constant:0];

// 把限制添加到父視圖上.
[self.view addConstraints:@[viewLeft, viewTop, viewWidth, viewHeight, marginLeft]];
           

運作看看效果,程式 Crash 了!控制台 Log 中有這麼一段資訊:

"<NSLayoutConstraint:0xXXXXXXX H:|-(50)-[UIView:0xXXXXXX]   (Names: '|':UIView:0xXXXXXX )>",
"<NSLayoutConstraint:0xXXXXXXX H:|-(0)-[UIView:0xXXXXXX]   (Names: '|':UIView:0xXXXXXX )>"
           

可以看到第一條是 

viewLeft

 這個限制,它限制了 

view

 的左邊距離父視圖的左邊 50 點。

第二條是新添加的 

marginLeft

 這個限制,它限制了 

view

 的左邊距離父視圖的左邊 0 點,也就是緊貼着父視圖的左邊。

很明顯這兩個限制是沖突的,當系統嘗試根據優先級進行布局時,發現它們的優先級也相同,無法滿足兩個沖突的限制,是以抛出了異常。

我們隻需要給兩個限制設定不同的優先級即可解決。添加下面一行代碼:

[viewLeft setPriority:UILayoutPriorityDefaultHigh];
           

因為預設所有限制的優先級都是 

UILayoutPriorityRequired

,是以我們隻需要将 

viewLeft

的優先級設定得比預設的低即可。

效果:

iOS 中 Auto Layout(自動布局)

需要注意的一點是,限制的優先級必須在它添加到視圖上之前設定,如果限制已經添加到視圖上後去嘗試改變它的優先級,将會得到一個異常。

提高效率

Auto Layout 雖然很好,但無論是直接使用 

NSLayoutConstraint

 還是使用 

VFL

 來編寫布局的代碼都比較麻煩。

好消息是有大量的開源庫幫助我們提高編寫布局代碼的效率。比較流行的有:

  • Masonry
  • PureLayout(前 UIView-AutoLayout)
  • FLKAutoLayout
  • KeepLayout

我最初使用 

UIView-AutoLayout

,但因為它不支援 OSX,是以後來使用過一段時間的

Masonry

,當 

UIView-AutoLayout

 的原作者釋出 

PureLayout

 後,我就轉向了

PureLayout

 并使用至今。

在我看來,

Masonry

 和 

PureLayout

 差别并不大,PureLayout 的文法更偏向Objective-C。

2015.11.11 更新, 由于 

Masonry

 強大的特性,建議大家優先考慮使用它。

下面是一個 Instagram 頁面截圖,我們使用 

PureLayout

 來實作這個布局。

iOS 中 Auto Layout(自動布局)

我把它分為頭像、昵稱、時間辨別、時間、贊辨別、贊的數量、贊按鈕、評論按鈕、更多按鈕以及中間的圖檔視圖。

聲明以下屬性:

@property (nonatomic, strong) UIImageView *avatarImageView;
@property (nonatomic, strong) UILabel     *nicknameLabel;
@property (nonatomic, strong) UIView      *timestampIndicator;
@property (nonatomic, strong) UILabel     *timestampLabel;
@property (nonatomic, strong) UIImageView *contentImageView;
@property (nonatomic, strong) UIView      *likeIndicator;
@property (nonatomic, strong) UILabel     *likesLabel;
@property (nonatomic, strong) UIButton    *likeButton;
@property (nonatomic, strong) UIButton    *commentButton;
@property (nonatomic, strong) UIButton    *moreButton;
           

布局代碼如下:

// 頭像左邊距離父視圖左邊 10 點.
[self.avatarImageView autoPinEdgeToSuperviewEdge:ALEdgeLeading withInset:10.f];

// 頭像頂邊距離父視圖頂部 10 點.
[self.avatarImageView autoPinEdgeToSuperviewEdge:ALEdgeTop withInset:10.f];

// 設定頭像尺寸
[self.avatarImageView autoSetDimensionsToSize:kAvatarSize];

// 昵稱的左邊位于頭像的右邊 10 點的地方.
[self.nicknameLabel autoPinEdge:ALEdgeLeading toEdge:ALEdgeTrailing ofView:self.avatarImageView withOffset:10.f];

// 根據昵稱的固有内容尺寸設定它的尺寸
[self.nicknameLabel autoSetDimensionsToSize:[self.nicknameLabel intrinsicContentSize]];

// 時間辨別的右邊位于時間視圖左邊 -10 點的地方, 從右往左、從下往上布局時數值都是負的。
[self.timestampIndicator autoPinEdge:ALEdgeTrailing toEdge:ALEdgeLeading ofView:self.timestampLabel withOffset:-10.f];

// 根據時間辨別的固有内容尺寸設定它的尺寸
[self.timestampIndicator autoSetDimensionsToSize:CGSizeMake(10.f, 10.f)];

// 時間視圖的右邊距離父視圖的右邊 10 點.
[self.timestampLabel autoPinEdgeToSuperviewEdge:ALEdgeTrailing withInset:10.f];

// 根據時間視圖的固有内容尺寸設定它的尺寸
[self.timestampLabel autoSetDimensionsToSize:[self.timestampLabel intrinsicContentSize]];

// 頭像、昵稱、時間辨別、時間視圖水準對齊。(意思就是說隻需要設定其中一個的垂直限制(y)即可)
[@[self.avatarImageView, self.nicknameLabel, self.timestampIndicator, self.timestampLabel] autoAlignViewsToAxis:ALAxisHorizontal];

// 内容圖檔視圖頂部距離頭像的底部 10 點.
[self.contentImageView autoPinEdge:ALEdgeTop toEdge:ALEdgeBottom ofView:self.avatarImageView withOffset:10.f];

// 内容圖檔視圖左邊緊貼父視圖左邊
[self.contentImageView autoPinEdgeToSuperviewEdge:ALEdgeLeading];

// 内容圖檔視圖的寬度等于父視圖的寬度
[self.contentImageView autoMatchDimension:ALDimensionWidth toDimension:ALDimensionWidth ofView:self];

// 内容圖檔視圖的高度等于父視圖的寬度
[self.contentImageView autoMatchDimension:ALDimensionHeight toDimension:ALDimensionWidth ofView:self];

// 贊辨別與頭像左對齊
[self.likeIndicator autoPinEdge:ALEdgeLeading toEdge:ALEdgeLeading ofView:self.avatarImageView];

// 贊辨別的頂部距離内容圖檔視圖底部 10 點.
[self.likeIndicator autoPinEdge:ALEdgeTop toEdge:ALEdgeBottom ofView:self.contentImageView withOffset:10.f];

// 設定贊辨別的尺寸
[self.likeIndicator autoSetDimensionsToSize:CGSizeMake(10.f, 10.f)];

// 贊數量視圖與贊辨別水準對齊
[self.likesLabel autoAlignAxis:ALAxisHorizontal toSameAxisOfView:self.likeIndicator];

// 贊數量視圖的左邊距離贊辨別的右邊 10 點.
[self.likesLabel autoPinEdge:ALEdgeLeading toEdge:ALEdgeTrailing ofView:self.likeIndicator withOffset:10.f];

// 以下請自行腦補...
[self.likesLabel autoSetDimensionsToSize:[self.likesLabel intrinsicContentSize]];

NSArray *buttons = @[self.likeButton, self.commentButton, self.moreButton];
[buttons autoMatchViewsDimension:ALDimensionHeight];
[buttons autoAlignViewsToEdge:ALEdgeBottom];
[self.likeButton autoPinEdge:ALEdgeLeading toEdge:ALEdgeLeading ofView:self.avatarImageView];
[self.likeButton autoPinEdgeToSuperviewEdge:ALEdgeBottom withInset:10.f];
[self.likeButton autoSetDimensionsToSize:CGSizeMake(50.f, 25.f)];

[self.commentButton autoPinEdge:ALEdgeLeading toEdge:ALEdgeTrailing ofView:self.likeButton withOffset:5.f];
[self.commentButton autoSetDimension:ALDimensionWidth toSize:65.f];

[self.moreButton autoPinEdgeToSuperviewEdge:ALEdgeTrailing withInset:10.f];
[self.moreButton autoSetDimension:ALDimensionWidth toSize:40.f];
           

效果完成: 

iOS 中 Auto Layout(自動布局)

繼續閱讀