天天看點

iOS 絕對布局、autoresizingMask 布局、AutoLayout 布局詳解絕對布局autoresizingMask 布局AutoLayout 布局

絕對布局

絕對布局使用寫死的 frame 定位各個視圖,優點是 app 性能消耗小,缺點是開發成本高(需要寫代碼時适配機型計算好視圖起點寬高)。

autoresizingMask 布局

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

};

UIViewAutoresizingNone就是不自動調整。

UIViewAutoresizingFlexibleLeftMargin ,如果不使用,左側margin不會改變,也就是左頂點的x坐标不會變化。如果使用,左側margin會變化,具體變化值在不同情況下會有所不同(成比例放大?系統會自行按一定算法調整吧。)。

UIViewAutoresizingFlexibleRightMargin ,原理同上

UIViewAutoresizingFlexibleTopMargin 原理同上

UIViewAutoresizingFlexibleBottomMargin 原理同上

UIViewAutoresizingFlexibleWidth 自動調整自己的寬度,如果不使用,寬度不會變化,如果使用,寬度跟父view等比縮放。

UIViewAutoresizingFlexibleHeight  原理同上

另外上面這裡的枚舉個數和 xib 中的設定相仿,但是 xib 的設定并不和這裡的變量一一對應。

比如下圖,autoresizingMask 是 42,就是 101010,就是沒有使用UIViewAutoresizingFlexibleLeftMargin,UIViewAutoresizingFlexibleRightMargin 和UIViewAutoresizingFlexibleHeight 其他的都用了。

iOS 絕對布局、autoresizingMask 布局、AutoLayout 布局詳解絕對布局autoresizingMask 布局AutoLayout 布局

再看一個例子,如下圖:

iOS 絕對布局、autoresizingMask 布局、AutoLayout 布局詳解絕對布局autoresizingMask 布局AutoLayout 布局

列印出的 view 的 autoresizingMask 屬性值是 46,也就是 二進制的 101110,其實,僅僅是沒有使用UIViewAutoresizingFlexibleLeftMargin,UIViewAutoresizingFlexibleHeight!剩下的都用了。

再看一個

iOS 絕對布局、autoresizingMask 布局、AutoLayout 布局詳解絕對布局autoresizingMask 布局AutoLayout 布局

這個,屬性值是45,101101,就是缺少UIViewAutoresizingFlexibleHeight和UIViewAutoresizingFlexibleWidth。

再看一個:

iOS 絕對布局、autoresizingMask 布局、AutoLayout 布局詳解絕對布局autoresizingMask 布局AutoLayout 布局

屬性值是18 ,010010,就是隻有  UIViewAutoresizingFlexibleHeight和UIViewAutoresizingFlexibleWidth。

這個也是讓我困擾很久的問題!是以不要根據xib中的設定,想當然地寫出錯誤的代碼限制!

看完上面這些例子,我們可以總結一下,如果你希望view的長寬需要等比放大,就需要使用UIViewAutoresizingFlexibleHeight 和UIViewAutoresizingFlexibleWidth,而如果需要把某個方向的margin固定,就不要加入對應的方向的FlexibleMargin mask ,而不需要固定的方向,就要加入對應的mask。

另外,需要注意,再xib中的某些設定是沖突的,系統會自動去掉沖突的設定,比如,下面這種情況:

iOS 絕對布局、autoresizingMask 布局、AutoLayout 布局詳解絕對布局autoresizingMask 布局AutoLayout 布局

同時限定左右距離不變,而且長度不變,這是不可能達到的要求。系統會無視右邊距離不變的限制。

一般控件預設有的 autoresizingMask 的值就是18 ,高、寬會随着父控件的變化而變化,高、寬擁有自動伸縮功能。

從 xib 裡建立出來的預設的 view( Xcode 預設建立出來的 view,代碼中使用 xib 建立的 view 和 xib 面闆設定中保持一緻),其 autoresizingMask 的值即為 18。

AutoLayout 布局

1. translatesAutoresizingMaskIntoConstraints 屬性

  1. 把 autoresizingMask 轉換為 Constraints,即:可以把 frame ,bouds,center 方式布局的視圖自動轉化為限制形式。(此時該視圖上限制已經足夠 不需要手動去添加别的限制)
  • 用代碼建立的所有 view(即使是代碼中使用打開了 autolayout 的 xib 建立的 view), translatesAutoresizingMaskIntoConstraints 預設是 YES
  • 用 IB 建立的所有 view ,translatesAutoresizingMaskIntoConstraints 預設是(autoresize 布局:YES , autolayout布局 :NO)

         如何設定 translatesAutoresizingMaskIntoConstraints ?

  • 視圖 使用代碼建立,frame 布局 ,不用去管 translatesAutoresizingMaskIntoConstraints
  • 視圖 使用代碼建立,autolayout 布局,translatesAutoresizingMaskIntoConstraints 設定為 NO
  • 視圖 IB 建立,frame 布局 , translatesAutoresizingMaskIntoConstraints 不用管 (IB 幫我們設定好了:YES)
  • 視圖 IB 建立,autolayout 布局,translatesAutoresizingMaskIntoConstraints 不用管 (IB 幫我們設定好了,NO)

       為什麼 translatesAutoresizingMaskIntoConstraints 使用AutoLayout布局時候,就要設定為 NO ?

  • translatesAutoresizingMaskIntoConstraints 的本意是将 frame 布局自動轉化為限制布局(這種規律難以捕捉,即使設定的frame是正确的,也很容易和自己添加的限制沖突,進而引起布局出錯),轉化的結果是為這個視圖自動添加所有需要的限制,如果我們這時給視圖添加自己建立的限制就一定會限制沖突。為了避免上面說的限制沖突,我們在代碼建立限制布局的控件時直接指定這個視圖不要根據 frame 布局轉化自動生成限制布局(即translatesAutoresizingMaskIntoConstraints=NO),可以放心的去建立自己想要的限制了。
  • 例如:v1 是一個使用 autolayout 的view,v2 是一個不使用 autolayout 的 view,但 v2 成為 v1 的 subview 時,v2 需要四條隐含的 constraint 來确定 v2 的位置,這些限制都是從 v2 的 frame 轉化而來(注意可在規律難以捕捉情況下,将 autoresizingMake 設定為 None,轉換後的限制相當于絕對布局)。
  • setTranslatesAutoresizingMaskIntoConstraints 和 setFrame 組合使用導緻異常,如下代碼:
[_programHeadView setTranslatesAutoresizingMaskIntoConstraints:YES];
[_programHeadView setFrame:headRect];
           

        在調用 setTranslatesAutoresizingMaskIntoConstraints 和 setFrame 之前先把 view 從父 view 中移除,調用之後再添加回去這樣就能避免目前 view 的 constraints 和預設的 constraints 産生沖突。

2. 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中,會讓代碼的可讀性非常差。

關于性能,我也做了一個簡單的測試:

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 為 false 後會讓視圖失去限制,走投無路。例如我自定義轉場時就遇到了這樣的問題,轉場後的視圖并不在視圖的正中間。

是以,這個屬性,基本上我們也不用設定它。

3. AutoLayout 與 Frame

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

那麼你可能會遇到這種情況,想讓layer的尺寸是由其它視圖尺寸設定的,而這個視圖又是由限制控制布局的。如果将layer的初始化與view的初始化放在一個方法中,類似于viewDidLoad的方法中

layer.bounds = CGRectMake(0,0,view.bounds.size.width * 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。在這之後就可以拿到設定限制的視圖的尺寸了。

4. AutoLayout 動畫

如果我們的一個視圖是通過設定frame來布局的,那麼我們在位移動畫時直接改變frame就可以了。很簡單。

可是在限制布局的視圖中,設定frame這個辦法就無效了。那我們怎麼辦?

網上有很多人的辦法就是:拿到想要做動畫的限制,在動畫之前對限制進行修改,在動畫的block中調用setNeedsLayout方法。

這個方法我覺得非常的麻煩,為了友善地拿到限制,我們通常還需要把限制設定成屬性,動畫一多那豈不就是完蛋了?

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

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

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

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

5. AutoLayout 比例設定

如果我們用autoLayout想把一個視圖的中心設定到螢幕橫向和縱向的1/4處:

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

這就相當于

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

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

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倍
           

6. Local Constraints

If we want to compose a custom view out of several subviews, we have to lay out these subviews somehow. In an Auto Layout environment it is most natural to add local constraints for these views. However, note that this makes your custom view dependent on Auto Layout, and it cannot be used anymore in windows without Auto Layout enabled. It’s best to make this dependency explicit by implementing 

requiresConstraintBasedLayout

 to return 

YES

.

The place to add local constraints is 

updateConstraints

. Make sure to invoke 

[super updateConstraints]

 in your implementation after you’ve added whatever constraints you need to lay out the subviews. In this method, you’re not allowed to invalidate any constraints, because you are already in the first step of the layout process described above. Trying to do so will generate a friendly error message informing you that you’ve made a “programming error.”

If something changes later on that invalidates one of your constraints, you should remove the constraint immediately and call 

setNeedsUpdateConstraints

. In fact, that’s the only case where you should have to trigger a constraint update pass.

When 

layoutSubviews

 is called, it also calls 

updateConstraintsIfNeeded

, so calling it manually is rarely needed in my experience. In fact, I have never called it except when debugging layouts.

Updating constraints using 

setNeedsUpdateConstraints

 is pretty rare too,as above。

In addition, in my experience, I have never had to invalidate constraints, and not set the 

setNeedsLayout

 in the next line of the code, because new constraints pretty much are asking for a new layout. 

The rules of thumb are:

  • If you manipulated constraints directly, call 

    setNeedsLayout

    .
  • If you changed some conditions (like offsets or smth) which would change constraints in your overridden 

    updateConstraints

     method (a recommended way to change constraints, btw), call 

    setNeedsUpdateConstraints

    , and most of the time, 

    setNeedsLayout

     after that.
  • If you need any of the actions above to have immediate effect—e.g. when your need to learn new frame height after a layout pass—append it with a 

    layoutIfNeeded

    .

Also, in the below animation code, I believe 

setNeedsUpdateConstraints

 is unneeded, since constraints are updated before the animation manually, and the animation only re-lays-out the view based on differences between the old and new ones.

[UIView animateWithDuration:1.0f delay:0.0f usingSpringWithDamping:0.5f initialSpringVelocity:1 options:UIViewAnimationOptionCurveEaseInOut animations:^{
        [self.modifConstrView setNeedsUpdateConstraints];   // 這一行可以省略
        [self.modifConstrView layoutIfNeeded];
    } completion:NULL];
           

7. 相關方法

1、layoutSubviews

在iOS5.1和之前的版本,此方法的預設實作不會做任何事情(實作為空),iOS5.1之後(iOS6開始)的版本,此方法的預設實作是使用你設定在此view上面的constraints(Autolayout)去決定subviews的position和size。 UIView的子類如果需要對其subviews進行更精确的布局,則可以重寫此方法。隻有在

autoresizing

constraint-based behaviors of subviews

不能提供我們想要的布局結果的時候,我們才應該重寫此方法。可以在此方法中直接設定subviews的frame。 我們不應該直接調用此方法,而應當用下面兩個方法。

2、setNeedsLayout

此方法會将view目前的layout設定為無效的,并在下一個upadte cycle裡去觸發layout更新。

3、layoutIfNeeded

使用此方法強制立即進行layout,從目前view開始,此方法會周遊整個view層次(包括superviews)請求layout。是以,調用此方法會強制整個view層次布局。

4、基于限制的 AutoLayout 的方法

4.1、setNeedsUpdateConstraints

當一個自定義view的某個屬性發生改變,并且可能影響到constraint時,需要調用此方法去标記constraints需要在未來的某個點更新,系統然後調用

updateConstraints

.

4.2、needsUpdateConstraints

constraint-based layout system使用此傳回值去決定是否需要調用

updateConstraints

作為正常布局過程的一部分。

4.3、updateConstraintsIfNeeded

立即觸發限制更新,自動更新布局。

4.4、updateConstraints

自定義view應該重寫此方法在其中建立constraints. 注意:要在實作在最後調用

[super updateConstraints]

8. Auto Layout Process 自動布局過程

與使用springs and struts(autoresizingMask)比較,Auto layout 在 view 顯示之前,多引入了兩個步驟:updating constraints 和laying out views。每一個步驟都依賴于上一個。display 依賴 layout,而 layout 依賴 updating constraints。 

updating constraints->layout->display

第一步:updating constraints,被稱為測量階段,其從下向上(from subview to super view),為下一步 layout 準備資訊。可以通過調用方法 

setNeedUpdateConstraints 

去觸發此步。constraints 的改變也會自動的觸發此步。但是,當你自定義view的時候,如果一些改變可能會影響到布局的時候,通常需要自己去通知 Auto layout,updateConstraintsIfNeeded。

自定義 view 的話,通常可以重寫 updateConstraints 方法,在其中可以添加 view 需要的局部的 contraints。

第二步:layout,其從上向下(from super view to subview),此步主要應用上一步的資訊去設定 view 的 center 和 bounds。可以通過調用 setNeedsLayout 去觸發此步驟,此方法不會立即應用 layout。如果想要系統立即的更新 layout,可以調用layoutIfNeeded。另外,自定義 view 可以重寫方法 layoutSubViews 來在 layout 的工程中得到更多的定制化效果。

第三步:display,此步時把 view 渲染到螢幕上,它與你是否使用 Auto layout 無關,其操作是從上向下(from super view to subview),通過調用 setNeedsDisplay 觸發,

因為每一步都依賴前一步,是以一個 display 可能會觸發 layout,當有任何 layout 沒有被處理的時候,同理,layout 可能會觸發updating constraints,當 constraint system 更新改變的時候。

需要注意的是,這三步不是單向的,constraint-based layout 是一個疊代的過程,layout 過程中,可能去改變 constraints,又一次觸發 updating constraints,進行一輪 layout 過程。

注意:如果你每一次調用自定義 layoutSubviews 都會導緻另一個布局傳遞,那麼你将會陷入一個無限循環中。 

如下圖:

iOS 絕對布局、autoresizingMask 布局、AutoLayout 布局詳解絕對布局autoresizingMask 布局AutoLayout 布局