AutoLayout的坑

本文投稿文章,做者:MangoMade(簡書api

AutoLayout很是強大也很是易用,可讀性也很強,加上各類第三方AutoLayout庫,讓你布起局來猶如繃掉鏈子的狗!根本停不下來!之前的ide

1
label.frame.origin.y + label.frame.size.height + 10

現在只用:佈局

1
2
3
button.snp_makeConstraints{
     $0.top.equalTo(label.snp_bottom).offset(10)
}

真是好用得不要不要。性能

但是,我在使用AutoLayout卻遇到很多坑,翻閱了很多博客網站才找到了我認爲的比較不錯的解決方案。我把這些內容貼出來,若是其中有誤,能夠在下方留言指出,但願你們可以多多交流共同進步。學習

本文主要分四部分:測試

  • updateViewConstraints與updateConstraints篇字體

  • AutoLayout與Frame篇動畫

  • AutoLayout動畫篇網站

  • AutoLayout比例設置ui

其中‘篇’字體現了本文做者對逼格的追求。

updateViewConstraints與updateConstraints篇

基本用法

updateViewConstraints與updateConstraints是AutoLayout出現後新增的api,updateConstraints主要功能是更新view的約束,並會調用其全部子視圖的該方法去更新約束。

而updateViewConstraints的出現方便了viewController,不用專門去重寫controller的view,當view的updateConstraints被調用時,該view如有controller,該controller的updateViewConstraints便會被調用。

兩個方法都須要在方法實現的最後調用父類的該方法。而且這兩個方法不建議直接調用。

在使用過程當中我發現這兩個方法有時候不會被系統調用。後來我看到public class func requiresConstraintBasedLayout() -> Bool方法的描述:

constraint-based layout engages lazily when someone tries to use it (e.g., adds a constraint to a view). If you do all of your constraint set up in -updateConstraints, you might never even receive updateConstraints if no one makes a constraint. To fix this chicken and egg problem, override this method to return YES if your view needs the window to use constraint-based layout.

大意是說,視圖並非主動採用constraint-based的。在非constraint-based的狀況下-updateConstraints,可能一次都不會被調用,解決這個問題須要重寫該類方法並返回true。

這裏要注意,若是一個view或controller是由interface builder初始化的,那麼這個實例的updateViewConstraints或updateConstraints方法便會被系統自動調用,起緣由應該就是對應的requiresConstraintBasedLayout方法返回true。而純代碼初始化的視圖requiresConstraintBasedLayout方法默認返回false。

因此在純代碼自定義一個view時,想把約束寫在updateConstraints方法中,就必定要重寫requiresConstraintBasedLayout方法,返回true。

至於純代碼寫的viewController如何讓其updateViewConstraints方法被調用。我本身的解決辦法是手動調用其view的setNeedsUpdateConstraints方法。

How to use updateConstraints?

文檔中對於這兩個方法提的最多的就是,重寫這兩個方法,在裏面設置約束。因此一開始我認爲這兩個方法是蘋果提供給咱們專門寫約束的。因而便開始嘗試使用。

直到後來在UIView中看到這樣一句話:

You should only override this method when changing constraints in place is too slow, or when a view is producing a number of redundant changes.

「你只因該在添加約束過於慢的時候,或者一次要修改大量約束的狀況下重寫次方法。」

簡直是讓人以爲又迷茫又坑爹。updateConstraints方法到底應該什麼時候使用

後來看到how to use updateConstraints這篇文章。給出了一個合理的解釋:

  • 儘可能將約束的添加寫到相似於viewDidLoad的方法中。

  • updateConstraints並不該該用來給視圖添加約束,它更適合用於週期性地更新視圖的約束,或者在添加約束過於消耗性能的狀況下將約束寫到該方法中。

  • 當咱們在響應事件時(例如點擊按鈕時)對約束的修改若是寫到updateConstraints中,會讓代碼的可讀性很是差。

關於性能,我也作了一個簡單的測試:

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
38
39
40
41
42
43
44
45
class MMView: UIView {
     override init(frame: CGRect) {
         super .init(frame: frame)
         self.backgroundColor = UIColor.grayColor()
         initManyButton()
         //初始化時添加約束
         test()  //每次只有一個test()不被註釋就好
     }
     override func touchesBegan(touches: Set, withEvent event: UIEvent?) {
         //響應事件時添加約束
         //test()
     }
     override func updateConstraints() {
         //updateConstraints中添加約束
         //test()
         super .updateConstraints()
     }
     func test(){
         let then = CFAbsoluteTimeGetCurrent()
         addConstraintsToButton()
         let now = CFAbsoluteTimeGetCurrent()
         print(now - then)
     }
     required init?(coder aDecoder: NSCoder) {
         fatalError( "init(coder:) has not been implemented" )
     }
     let buttonTag = 200
     func initManyButton(){
         for  index  in  0...1000{
             let button = UIButton(type: .System)
             button.tag = buttonTag + index
             self.addSubview(button)
         }
     }
     func addConstraintsToButton(){
         for  index  in  0...1000{
             if  let button = self.viewWithTag(index+buttonTag){
                 button.snp_makeConstraints{ make  in
                     make.center.equalTo(self)
                     make.size.equalTo(self)
                 }
             }
         }
     }
}

分別將設置約束寫在init中、寫在updateConstraints中、寫在事件響應方法中 的時間消耗進行測試,對1000個button添加約束,每一個添加4個約束。

  • init中,時間消耗約爲0.37秒

  • 寫在updateconstraints中,時間消耗約爲0.52秒

  • 寫在事件響應方法中,時間消耗約爲0.77秒

因此,結論,仍是將約束的設置寫在viewDidLoad中或者init中。沒事兒儘可能不去碰updateConstraints。除非對性能有要求。

關於UIView的translatesAutoresizingMaskIntoConstraints屬性

最近在對AutoLayout的學習中發現,不少人彷佛對translatesAutoresizingMaskIntoConstraints的誤解很是大,不少時候遇到問題總有人會在下面回答到:把translatesAutoresizingMaskIntoConstraints設置成false就能夠解決問題。。。實際上並無什麼用。

那麼這個屬性究竟是作什麼的呢?

其實這個屬性的命名已經把這個屬性的功能解釋的很是清楚了。

除了AutoLayout,AutoresizingMask也是一種佈局方式。這個想必你們都有了解。默認狀況下,translatesAutoresizingMaskIntoConstraints = true , 此時視圖的AutoresizingMask會被轉換成對應效果的約束。這樣極可能就會和咱們手動添加的其它約束有衝突。此屬性設置成false時,AutoresizingMask就不會變成約束。也就是說 當前 視圖的 AutoresizingMask失效了。

那咱們何時須要設置這個屬性呢?

當咱們用代碼添加視圖時,視圖的translatesAutoresizingMaskIntoConstraints屬性默認爲true,但是AutoresizingMask屬性默認會被設置成.None。也就是說若是咱們不去動AutoresizingMask,那麼AutoresizingMask就不會對約束產生影響。

當咱們使用interface builder添加視圖時,AutoresizingMask雖然會被設置成非.None,可是translatesAutoresizingMaskIntoConstraints默認被設置成了false。因此也不會有衝突。

反而有的視圖是靠AutoresizingMask佈局的,當咱們修改了translatesAutoresizingMaskIntoConstraints後會讓視圖失去約束,走投無路。例如我自定義轉場時就遇到了這樣的問題,轉場後的視圖並不在視圖的正中間。

因此,這個屬性,基本上咱們也不用設置它。

AutoLayout與Frame篇

在使用AutoLayout的時候你可能也會同時也會用到frame,好比須要用到layer的時候。

那麼你可能會遇到這種狀況,想讓layer的尺寸是由其它視圖尺寸設定的,而這個視圖又是由約束控制佈局的。若是將layer的初始化與view的初始化放在一個方法中,相似於viewDidLoad的方法中

1
layer.bounds = CGRectMake(0,0,view.bounds.size.widith * 0.5,50)

那麼極可能最終layer的寬度是0。

這是由於約束被設置以後它並不會當即對view做出改變,而是要等到layout時,纔會對視圖的尺寸進行修改。而layout一般是在視圖已經加載到父視上時。

因此咱們若是在viewDidLoad中設置了約束,要等到viewDidAppear時view的尺寸纔會真正改變。

那麼,若是須要既用約束佈局,又用frame佈局,若是能讓它們很好的協做呢?

一個很好的解決辦法是:吧frame設置寫到layoutSubviews中或者寫到viewDidLayoutSubviews中便可。由於約束生效時view的center或者bounds就會被修改,center或者bounds被修改時layoutSubview,就會被調用,隨後viewDidLayoutSubviews就回被調用。這個時候,設置約束的視圖frame就再也不是(0,0,0,0)了

若是咱們必需要將約束和frame寫在同一方法中,寫完約束就設置frame,而不是想把frame的設置寫到layoutSubview中(好比咱們設置好約束後立刻就想根據約束的結果計算高度),那麼咱們還能夠在設置完約束以後手動調用layoutIfNeeded方法,讓視圖當即layout,更新frame。在這以後就能夠拿到設置約束的視圖的尺寸了。

AutoLayout動畫篇

這篇的內容很是簡單,就是介紹約束佈局的視圖如何進行位移動畫。

若是咱們的一個視圖是經過設置frame來佈局的,那麼咱們在位移動畫時直接改變frame就能夠了。很簡單。

但是在約束佈局的視圖中,設置frame這個辦法就無效了。那咱們怎麼辦?

網上有不少人的辦法就是:拿到想要作動畫的約束,在動畫以前對約束進行修改,在動畫的block中調用setNeedsLayout方法。

這個方法我以爲很是的麻煩,爲了方便地拿到約束,咱們一般還須要把約束設置成屬性,動畫一多那豈不就是完蛋了?

一種更好的方法就是設置視圖的transform屬性。

好比我想要讓視圖作一個x軸+50的位移,

1
self.view.transform = CGAffineTransformMakeTranslation(50, 0)

這樣設置便可。CGAffineTransformMakeTranslation這個方法就是設置位置。

AutoLayout比例設置

若是咱們用autoLayout想把一個視圖的中心設置到屏幕橫向和縱向的1/4處:

1
2
3
4
button.snp_makeConstraints{ make  in
     make.centerX.equalTo(self.view).multipliedBy(0.25)
     make.centerY.equalTo(self.view).multipliedBy(0.25)
}

這就至關於

1
button.center = CGPointMake(self.view.bounds.size.width * 0.25 ,self.view.bounds.size.height * 0.25)

那麼AutoLayout中的倍數,具體表示什麼呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
let view = UIView()
self.view.addSubview(view)
var  bottomConstraint : Constraint!
view.snp_makeConstraints { (make)  in
     make.height.equalTo(50)
     make.width.equalTo(50)
     make.centerX.equalTo(self.view.snp_centerX)
     bottomConstraint = make.bottom.equalTo(self.view.snp_centerY).constraint
}
self.view.layoutIfNeeded()
print(view.frame)
//打印結果 y:318 height:50 和爲368
bottomConstraint.uninstall()
view.snp_makeConstraints { (make)  in
     make.bottom.equalTo(self.view.snp_centerY).multipliedBy(1.5)
}
self.view.layoutIfNeeded()
print(view.frame)
//打印結果 y:318 height:50 和爲552,恰好是368的1.5倍
//因此咱們能夠得出結論:某條邊的約束的倍數表明着這條邊到相對邊的距離的倍數
//上面代碼中的1.5倍讓bottom邊到y = 0邊的距離變成了1.5倍
相關文章
相關標籤/搜索