1. 相關API
基于觸發約束的布局 Triggering Constraint-Based Layout
-- (BOOL)needsUpdateConstraints
constraint-based layout system使用此返回值去決定是否需要 調用updateConstraints
作為正常布局過程的一部分。
- (void)setNeedsUpdateConstraints
當一個自定義view的某個屬性發生改變,并且可能影響到constraint時,需要調用此方法去標記constraints需要在未來的某個點更新,系統然后調用updateConstraints
.
- (void)updateConstraints
更新約束,自定義view應該重寫此方法在其中建立constraints. 注意:要在實現在最后調用 [super updateConstraints]
- (void)updateConstraintsIfNeeded
立即觸發約束更新,自動更新布局。
鋪設子視圖 Laying out Subviews
-(void)layoutSubviews
如果你需要更精確控制子view,而不是使用限制或autoresizing行為,就需要實現該方法。
-(void)layoutIfNeeded
使用此方法強制立即進行layout,從當前view開始,此方法會遍歷整個view層次(包括superviews)請求layout。因此,調用此方法會強制整個view層次布局。
- (void)setNeedsLayout
此方法會將view當前的layout設置為無效的,并在下一個upadte cycle里去觸發layout更新。
繪畫和更新視圖
-(void)drawRect:(CGRect)rect
如果你的View畫自定義的內容,就要實現該方法,否則避免覆蓋該方法。
-void)setNeedsDisplay
標記整個視圖的邊界矩形需要重繪
-(void)setNeedsDisplayInRect:(CGRect)invalidRect
標記在指定區域內的視圖的邊界需要重繪
相關聯的改變 Observing View-Related Changes
-(void)didAddSubview:(UIView *)subview
通知視圖指定子視圖已經添加
-(void)willRemoveSubview:(UIView *)subview
通知視圖將要移除指定的子視圖
-(void)willMoveToSuperview:(UIView *)newSuperview
通知視圖將要移動到一個新的父視圖中
-(void)didMoveToSuperview
通知視圖已經移動到一個新的父視圖中
-(void)willMoveToWindow:(UIWindow *)newWindow
通知視圖將要移動到一個新的window中
-(void)didMoveToWindow
通知視圖已經移動到一個新的window中
配置自動調整大小狀態 Configuring the Resizing Behavior
-(void)sizeToFit
根據子視圖的大小位置,調整視圖,使其恰好圍繞子視圖, 也就是說自動適應子視圖的大小,只顯示子視圖
-(CGSize)sizeThatFits:(CGSize)size
讓視圖計算最適合子視圖的大小,即能把全部子視圖顯示出來所需要的最小的size
2. 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
觸發,setNeedsDisplay觸發系統會調用UIView 的 drawRect方法。因為每一步都依賴前一步,因此一個display可能會觸發layout,當有任何layout沒有被處理的時候,同理,layout可能會觸發updating constraints,當constraint system更新改變的時候。
需要注意的是,這三步不是單向的,constraint-based layout是一個迭代的過程,layout過程中,可能去改變constraints,又一次觸發updating constraints,進行一輪layout過程。
下面說說UIViewController的布局過程
VC的生命周期的部分過程viewDidLoad -> viewWillAppear -> updateViewConstraints -> viewWillLayoutSubviews -> viewDidLayoutSubviews -> viewDidAppear -> viewWillDisAppear -> updateViewConstraints -> viewDidDisAppear
對應updateConstraints -> layoutSubViews -> drawRect
當view修改約束(addConstraint, removeConstraint)會觸發setNeedsUpdateConstraints
,而這個在layoutSubViews之前會觸發updateConstraints,完成之后會調用layoutSubViews。
UIViewController在有個updateViewConstraints
方法,這個方法實際是self.view 被設置了setNeedsUpdateConstraints
(第一次展示的時候),必然會調用這個方法(與上面的解釋保持一致了,第一次可以理解為為self.view增加了各種約束)。而這個方法的默認實現是調用子view的updateConstraints
方法,這樣就自上而下的完成了布局。
此處需要注意的地方:
- 不要忘記調用父類的方法,避免有時候出現一些莫名的問題。
- 在view的layoutSubViews或者ViewController的
viewDidLayoutSubviews
方法里后可以拿到view的實際frame,所以當我們真的需要frame的時候需要在這個時間點以后才能拿到。 - 下面我們可以解釋是為什么
viewDidLoad
里通過setFrame的方式改過原先在storyboard里拖動的約束代碼無效了。因為updateViewConstraints
在viewDidLoad
后執行,會覆蓋掉之前的設置的frame,所以無效。
3. 補充總結
以下情況下會調用layoutSubviews
:
- init初始化不會觸發
layoutSubviews
,但是是用initWithFrame
進行初始化時,當rect的值不為CGRectZero時,也會觸發。 -
addSubview
會觸發layoutSubviews
- 設置view的Frame會觸發
layoutSubviews
,當然前提是frame的值設置前后發生了變化 - 滾動一個UIScrollView會觸發
layoutSubviews
- 旋轉Screen會觸發父UIView上的
layoutSubviews
事件 - 改變一個UIView大小的時候也會觸發父UIView上的layoutSubviews事件
刷新子對象布局
-layoutSubviews
方法:這個方法,默認沒有做任何事情,需要子類進行重寫
-setNeedsLayout
方法: 標記為需要重新布局,異步調用layoutIfNeeded刷新布局,不立即刷新,但layoutSubviews一定會被調用
-layoutIfNeeded
方法:如果,有需要刷新的標記,立即調用layoutSubviews進行布局(如果沒有標記,不會調用layoutSubviews)如果要立即刷新,要先調用[view setNeedsLayout]
,把標記設為需要布局,然后馬上調用[view layoutIfNeeded]
,實現布局在視圖第一次顯示之前,標記總是“需要刷新”的,可以直接調用[view layoutIfNeeded]
重繪
-drawRect:(CGRect)rect
方法:重寫此方法,執行重繪任務
-setNeedsDisplay
方法:標記為需要重繪,異步調用drawRect
-setNeedsDisplayInRect:(CGRect)invalidRect
方法:標記為需要局部重繪
sizeToFit
會自動調用sizeThatFits
方法;
sizeToFit
不應該在子類中被重寫,應該重寫sizeThatFits
sizeThatFits
傳入的參數是receiver當前的size,返回一個適合的sizesizeToFit
可以被手動直接調用sizeToFit
和sizeThatFits
方法都沒有遞歸,對subviews也不負責,只負責自己
layoutSubviews
對subviews重新布局
layoutSubviews
方法調用先于drawRectsetNeedsLayout
在receiver標上一個需要被重新布局的標記,在系統runloop的下一個周期自動調用layoutSubviews
layoutIfNeeded
方法如其名,UIKit會判斷該receiver是否需要layout,根據Apple官方文檔,layoutIfNeeded
方法應該是這樣的layoutIfNeeded
遍歷的不是superview鏈,應該是subviews鏈drawRect
是對receiver的重繪,能獲得context。
總之,理解view布局的過程,可以幫助你理解View顯示的相關問題,解決一些界面問題,合理使用以上方法對你自定義控件也有很大的幫助。