原文位址:http://www.cocoachina.com/ios/20141026/10045.html
細數AutoLayout以來UIView和UIViewController新增的相關API – UIViewController篇
UILayoutSupport
- @property(nonatomic,readonly,retain) id topLayoutGuide NS_AVAILABLE_IOS(7_0);
- @property(nonatomic,readonly,retain) id bottomLayoutGuide NS_AVAILABLE_IOS(7_0);
- @protocol UILayoutSupport
- @property(nonatomic,readonly) CGFloat length;
- @end
從iOS 7以來,當我們的視圖控制器結構中有NavigationBar,TabBar或者ToolBar的時候,它們的translucent屬性的預設值改為了YES,并且目前的ViewController的高度會是整個螢幕的高度。(比如一個場景:拖動TableView的時候,上面的NavigationBar能夠透過去看到TableView的内容。)
為了確定我們的視圖不被這些Bar覆寫,我們可以在我們AutoLayout布局中使用topLayoutGuide和bottomLayoutGuide這兩個屬性。像這樣:
- NSDictionary *views = @{"topLayoutGuide" : self.topLayoutGuide, @"myLabel" : myLabel};
- [NSLayoutConstraint constraintsWithVisualFormat:@"V:[topLayoutGuide]-[myView]" options:0 metrics:nil views:views]
這個時候我們的視圖就不會被Bar所覆寫,顯示在了Bar下方:

并且使用這個屬性布局時,在traitCollection改變時(旋轉螢幕),它的值也會動态變化。上述代碼,在橫屏情況下,navigationbar高度變了之後,仍然能夠正确顯示。
這兩個guides的計算方式如下:
topLayoutGuide是通過計算 View Controller->View->Top 到 覆寫這個View最下層的那個Bar(像Navigation Bar) -> Bottom 的距離
bottomLayoutGuide是通過計算 View Controller->View->Bottom 到 覆寫這個View上層那個Bar(像Tab bar) -> Top 的距離
如果我們不使用AutoLayout布局,我們也可以通過Guide的length屬性獲得相應的距離。我們應該在-viewDidLayoutSubviews或者-layoutSubviews調用super之後,再去獲得length這個值,以確定正确。
UIConstraintBasedLayoutCoreMethods
- - (void)updateViewConstraints NS_AVAILABLE_IOS(6_0);
UIViewController中也新增了一個更新布局限制的方法,在AutoLayout UIView相關API的筆記中,詳細講述了UIView的一組更新布局限制的方法。
這個方法預設的實作是調用對應View的 -updateConstraints 。ViewController的View在更新視圖布局時,會先調用ViewController的updateViewConstraints 方法。我們可以通過重寫這個方法去更新目前View的内部布局,而不用再繼承這個View去重寫-updateConstraints方法。我們在重寫這個方法時,務必要調用 super 或者 調用目前View的 -updateConstraints 方法。
UITraitEnvironment
又一次看到了UITraitEnvironment協定,在UIKit Framework中,有四個類支援這個協定,分别是UIScreen, UIViewController,UIView 和 UIPresentationController。是以當視圖的traitCollection改變時,UIViewController能夠捕獲到這個消息,并做對應處理的。 更多解釋可以參考上一篇文章詳解UICoordinateSpace和UIScreen在iOS 8上的坐标問題。
關于Size Class和UITraitCollection的概念可參考如下連結:
WWDC 2014 Session筆記 - iOS界面開發的大一統 From: onecat’s Blog
iOS8 Size Classes的了解與使用 From: Joywii’s Blog
另外,UIViewController還另外提供了以下兩個方法:
- - (void)setOverrideTraitCollection:(UITraitCollection *)collection forChildViewController:(UIViewController *)childViewController NS_AVAILABLE_IOS(8_0);
- - (UITraitCollection *)overrideTraitCollectionForChildViewController:(UIViewController *)childViewController NS_AVAILABLE_IOS(8_0);
我們可以通過調用ViewController的setOverrideTraitCollection方法為它的ChildViewController重新設定traitCollection的值。一般情況下traitCollection值從父controller傳到子controller是不做修改的。當我們自己實作一個容器Controller的時候,我們可以使用這個方法進行調整。
相對的,我們可以通過overrideTraitCollectionForChildViewController方法獲得ChildViewController的traitCollection值。
UIContentContainer
iOS 8上随着Size Class概念的提出,UIViewController支援了UIContentContainer這樣一組新的協定:
- - (void)systemLayoutFittingSizeDidChangeForChildContentContainer:(id )container NS_AVAILABLE_IOS(8_0);
- - (CGSize)sizeForChildContentContainer:(id )container withParentContainerSize:(CGSize)parentSize NS_AVAILABLE_IOS(8_0);
- - (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id )coordinator NS_AVAILABLE_IOS(8_0);
- - (void)willTransitionToTraitCollection:(UITraitCollection *)newCollection withTransitionCoordinator:(id )coordinator NS_AVAILABLE_IOS(8_0);
UIViewController對這組協定提供了預設的實作。我們自定義ViewController的時候可以重寫這些方法來調整視圖布局,比如我們可以在這些方法裡調整ChildViewControler的位置。當我們重寫這些協定方法時,我們通常都去調用 super。
viewWillTransitionToSize: ViewController的View的size被他的Parent Controller改變時,會觸發這個方法。(比如rootViewController在它的window旋轉的時候)。我們在重寫這個方法時,確定要調用super,來保證size改變的這條消息能夠正常傳遞給它的Views或者ChildViewControllers。
willTransitionToTraitCollection: 當ViewController的traitCollection的值将要改變時會調用這個方法。這個方法是在 UITraitEnvironment協定方法 traitCollectionDidChange:之前被調用。我們在重寫這個方法時,也要確定要調用super來保證消息的傳遞。比如,我們可以像這樣在traitCollection值改變時,對視圖做對應的動畫進行調整:
- - (void)willTransitionToTraitCollection:(UITraitCollection *)newCollection
- withTransitionCoordinator:(id )coordinator
- {
- [super willTransitionToTraitCollection:newCollection
- withTransitionCoordinator:coordinator];
- [coordinator animateAlongsideTransition:^(id context) {
- if (newCollection.verticalSizeClass == UIUserInterfaceSizeClassCompact) {
- } else {
- }
- [self.view setNeedsLayout];
- } completion:nil];
- }
sizeForChildContentContainer:一個容器ViewController可以使用這個方法設定ChildViewController的size。當容器ViewControllerviewWillTransitionToSize:withTransitionCoordinator:被調用時(我們重寫這個方法時要調用Super),sizeForChildContentContainer方法将會被調用。然後我們可以把需要設定的size發送給ChildViewController。當我們設定的這個size和目前ChildViewController的size一樣,那麼ChildViewController的viewWillTransitionToSize方法将不會被調用。
sizeForChildContentContainer預設的實作是傳回 parentSize。
systemLayoutFittingSizeDidChangeForChildContentContainer:當滿足如下情況,這個方法會被調用:
目前ViewController沒有使用AutoLayout布局
ChildrenViewController的View使用了AutoLayout布局
ChildrenViewController View -systemLayoutSizeFittingSize:方法傳回的值改變(View由于内容的變化,size也出現了變化)
preferredContentSize
- // From UIContentContainer Protocol
- @property (nonatomic, readonly) CGSize preferredContentSize NS_AVAILABLE_IOS(8_0);
- - (void)preferredContentSizeDidChangeForChildContentContainer:(id )container NS_AVAILABLE_IOS(8_0);
- // From UIViewController
- @property (nonatomic) CGSize preferredContentSize NS_AVAILABLE_IOS(7_0);
preferredContentSize在UIContentContainer協定中是隻讀的,對應的UIViewController有可寫的版本。我們可以使用preferredContentSize來設定我們期望的ChildViewController的界面大小。舉個例子,如果應用中使用的popOver大小會發生變化,iOS7之前我們可以用contentSizeForViewInPopover來調整。iOS7開始這個API被廢棄,我們可以使用preferredContentSize來設定。
當一個容器ViewController的ChildViewController的這個值改變時,UIKit會調用preferredContentSizeDidChangeForChildContentContainer這個方法告訴目前容器ViewController。我們可以在這個方法裡根據新的Size對界面進行調整。
總結
UIViewController到目前為止(iOS 8.1), 關于布局的API最大的變化是iOS8中新增支援的兩組協定:UITraitEnvironment 和 UIContentContainer。我們可以在學習中通過Demo實作這些協定,來觀察ViewController中這些方法最終被調用的時機。
細數AutoLayout以來UIView和UIViewController新增的相關API--UIView篇
iOS8上關于UIView的Margin新增了3個APIs:
- @property (nonatomic) UIEdgeInsets layoutMargins NS_AVAILABLE_IOS(8_0);
- @property (nonatomic) BOOL preservesSuperviewLayoutMargins NS_AVAILABLE_IOS(8_0);
- - (void)layoutMarginsDidChange NS_AVAILABLE_IOS(8_0);
在iOS 8中,可以使用layoutMargins去定義view之間的間距,該屬性隻對AutoLayout布局生效。
是以AutoLayout中NSLayoutAttribute的枚舉值有了相應的更新:
- NSLayoutAttributeLeftMargin NS_ENUM_AVAILABLE_IOS(8_0),
- NSLayoutAttributeRightMargin NS_ENUM_AVAILABLE_IOS(8_0),
- NSLayoutAttributeTopMargin NS_ENUM_AVAILABLE_IOS(8_0),
- NSLayoutAttributeBottomMargin NS_ENUM_AVAILABLE_IOS(8_0),
- NSLayoutAttributeLeadingMargin NS_ENUM_AVAILABLE_IOS(8_0),
- NSLayoutAttributeTrailingMargin NS_ENUM_AVAILABLE_IOS(8_0),
- NSLayoutAttributeCenterXWithinMargins NS_ENUM_AVAILABLE_IOS(8_0),
- NSLayoutAttributeCenterYWithinMargins NS_ENUM_AVAILABLE_IOS(8_0),
通過在Xcode中測試列印,發現UIView預設的layoutMargins的值為 {8, 8, 8, 8},我們可以通過修改這個值來改變View之間的距離。
在我們改變View的layoutMargins這個屬性時,會觸發- (void)layoutMarginsDidChange這個方法。我們在自己的View裡面可以重寫這個方法來捕獲layoutMargins的變化。在大多數情況下,我們可以在這個方法裡觸發drawing和layout的Update。
preservesSuperviewLayoutMargins這個屬性預設是NO。如果把它設為YES,layoutMargins會根據螢幕中相關View的布局而改變。舉個例子:
如上圖,有三個View,其中藍色View的layoutMargins設為 UIEdgeInsetsMake(50, 50, 50, 50),黃色View的layoutMargins設為 UIEdgeInsetsMake(10, 10, 10, 10)。對黃色View的布局限制代碼如下:
- [NSLayoutConstraint constraintWithItem:yellowView attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:blueView attribute:NSLayoutAttributeWidth multiplier:1.0 constant:0.0];
- [NSLayoutConstraint constraintWithItem:yellowView attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:blueView attribute:NSLayoutAttributeHeight multiplier:0.5 constant:0.0];
- [NSLayoutConstraint constraintWithItem:yellowView attribute:NSLayoutAttributeCenterY relatedBy:NSLayoutRelationEqual toItem:blueView attribute:NSLayoutAttributeCenterY multiplier:1.0 constant:0.0];
- [NSLayoutConstraint constraintWithItem:yellowView attribute:NSLayoutAttributeCenterX relatedBy:NSLayoutRelationEqual toItem:blueView attribute:NSLayoutAttributeCenterX multiplier:1.0 constant:0.0];
對黑色View的布局代碼如下:
- [NSLayoutConstraint constraintWithItem:blackView attribute:NSLayoutAttributeTrailing relatedBy:NSLayoutRelationEqual toItem:yellowView attribute:NSLayoutAttributeTrailingMargin multiplier:1.0 constant:0.0];
- [NSLayoutConstraint constraintWithItem:blackView attribute:NSLayoutAttributeLeading relatedBy:NSLayoutRelationEqual toItem:yellowView attribute:NSLayoutAttributeLeadingMargin multiplier:1.0 constant:0.0];
- [NSLayoutConstraint constraintWithItem:blackView attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:yellowView attribute:NSLayoutAttributeTopMargin multiplier:1.0 constant:0.0];
- [NSLayoutConstraint constraintWithItem:blackView attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual toItem:yellowView attribute:NSLayoutAttributeBottomMargin multiplier:1.0 constant:0.0];
在preservesSuperviewLayoutMargins預設為NO的情況下,顯示效果就和上圖一樣(間距為10)。當設定黃色View的preservesSuperviewLayoutMargins為YES時,将會獲得如下效果(間距為50):
UIConstraintBasedLayoutInstallingConstraints
- @interface UIView (UIConstraintBasedLayoutInstallingConstraints)
- - (NSArray *)constraints NS_AVAILABLE_IOS(6_0);
- - (void)addConstraint:(NSLayoutConstraint *)constraint NS_AVAILABLE_IOS(6_0);
- - (void)addConstraints:(NSArray *)constraints NS_AVAILABLE_IOS(6_0);
- - (void)removeConstraint:(NSLayoutConstraint *)constraint NS_AVAILABLE_IOS(6_0);
- - (void)removeConstraints:(NSArray *)constraints NS_AVAILABLE_IOS(6_0);
- @end
以上這五個API中,第一個是傳回目前View中所有的constraints。後面四個方法即将被廢棄,應該使用NSLayoutConstraint類中activateConstraint相關方法替代。
UIConstraintBasedLayoutCoreMethods
- @interface UIView (UIConstraintBasedLayoutCoreMethods)
- - (void)updateConstraintsIfNeeded NS_AVAILABLE_IOS(6_0);
- - (void)updateConstraints NS_AVAILABLE_IOS(6_0);
- - (BOOL)needsUpdateConstraints NS_AVAILABLE_IOS(6_0);
- - (void)setNeedsUpdateConstraints NS_AVAILABLE_IOS(6_0);
- @end
setNeedsUpdateConstraints : 當一個自定義的View某一個屬性的改變可能影響到界面布局,我們應該調用這個方法來告訴布局系統在未來某個時刻需要更新。系統會調用updateConstraints去更新布局。
updateConstraints :自定義View時,我們應該重寫這個方法來設定目前view局部的布局限制。重寫這個方法時,一定要調用[super updateConstraints]。
needsUpdateConstraints :布局系統使用這個傳回值來确定是否調用updateConstraints
updateConstraintsIfNeeded :我們可以調用這個方法觸發update Constraints的操作。在needsUpdateConstraints傳回YES時,才能成功觸發update Constraints的操作。我們不應該重寫這個方法。
Auto Layout的布局過程是 update constraints(updateConstraints)-> layout Subviews(layoutSubViews)-> display(drawRect) 這三步不是單向的,如果layout的過程中改變了constrait, 就會觸發update constraints,進行新的一輪疊代。我們在實際代碼中,應該避免在此造成死循環。
UIConstraintBasedCompatibility
- @interface UIView (UIConstraintBasedCompatibility)
- - (BOOL)translatesAutoresizingMaskIntoConstraints NS_AVAILABLE_IOS(6_0);
- - (void)setTranslatesAutoresizingMaskIntoConstraints:(BOOL)flag NS_AVAILABLE_IOS(6_0);
- + (BOOL)requiresConstraintBasedLayout NS_AVAILABLE_IOS(6_0);
- @end
預設情況下,View的autoresizing工作會根據目前位置自動設定限制。我們在使用代碼寫自己的限制布局代碼時,必須設定目前View的translatesAutoresizingMaskIntoConstraints為NO,否則無法正常運作。IB預設是NO。
requiresConstraintBasedLayout :我們應該在自定義View中重寫這個方法。如果我們要使用Auto Layout布局目前視圖,應該設定為傳回YES。
UIConstraintBasedLayoutLayering
- - (CGRect)alignmentRectForFrame:(CGRect)frame NS_AVAILABLE_IOS(6_0);
- - (CGRect)frameForAlignmentRect:(CGRect)alignmentRect NS_AVAILABLE_IOS(6_0);
- - (UIEdgeInsets)alignmentRectInsets NS_AVAILABLE_IOS(6_0);
AutoLayout并不會直接操作View的Frame,但是視圖的alignment rect是起作用的。視圖的預設alignmentRectInsets值就是(0,0,0,0)。
我們可以簡單的對目前View設定用來布局的矩形,比如:
我們有一個自定義icon類型的Button,但是icon的大小比我們期望點選的Button區域要小。這個時候我們可以重寫alignmentRectInsets,把icon放在适當的位置。
大多數情況下重寫alignmentRectInsets這個方法可以滿足我們的工作。如果需要更加個性化的修改,我們可以重寫alignmentRectForFrame和frameForAlignmentRect這兩個方法。比如我們不想減去視圖固定的Insets,而是需要基于目前frame修改alignment rect。在重寫這兩個方法時,我們應該確定是互為可逆的。
Base line
- - (UIView *)viewForBaselineLayout NS_AVAILABLE_IOS(6_0);
當我們在使用布局限制中NSLayoutAttributeBaseline屬性時,系統會預設傳回目前視圖的底部作為baseline。我們可以重寫上述方法,但必須傳回的是目前視圖中的子視圖。
Intrinsic Content Size
- UIKIT_EXTERN const CGFloat UIViewNoIntrinsicMetric NS_AVAILABLE_IOS(6_0);
- - (CGSize)intrinsicContentSize NS_AVAILABLE_IOS(6_0);
- - (void)invalidateIntrinsicContentSize NS_AVAILABLE_IOS(6_0);
- - (UILayoutPriority)contentHuggingPriorityForAxis:(UILayoutConstraintAxis)axis NS_AVAILABLE_IOS(6_0);
- - (void)setContentHuggingPriority:(UILayoutPriority)priority forAxis:(UILayoutConstraintAxis)axis NS_AVAILABLE_IOS(6_0);
- - (UILayoutPriority)contentCompressionResistancePriorityForAxis:(UILayoutConstraintAxis)axis NS_AVAILABLE_IOS(6_0);
- - (void)setContentCompressionResistancePriority:(UILayoutPriority)priority forAxis:(UILayoutConstraintAxis)axis NS_AVAILABLE_IOS(6_0);
通過重寫intrinsicContentSize可以設定目前視圖顯示特定内容時的大小。比如我們設定一個自定義View,View裡面包含一個Label顯示文字,為了設定目前View在不同Size Class下内容的大小,我們可以這樣:
- - (CGSize)intrinsicContentSize
- {
- CGSize size = [label intrinsicContentSize];
- if (self.traitCollection.horizontalSizeClass == UIUserInterfaceSizeClassCompact) {
- size.width += 4.0f;
- } else {
- size.width += 40.0f;
- }
- if (self.traitCollection.verticalSizeClass == UIUserInterfaceSizeClassCompact) {
- size.height += 4.0;
- } else {
- size.height += 40.0;
- }
- return size;
- }
當有任何會影響這個Label内容大小的事件發生時,我們應該調用invalidateIntrinsicContentSize:
- label.text = @"content update"
- [self invalidateIntrinsicContentSize];
- // 或者比如目前視圖Size Class改變的時候
- - (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection
- {
- [super traitCollectionDidChange:previousTraitCollection];
- if ((self.traitCollection.verticalSizeClass != previousTraitCollection.verticalSizeClass)
- || (self.traitCollection.horizontalSizeClass != previousTraitCollection.horizontalSizeClass)) {
- [self invalidateIntrinsicContentSize];
- }
- }
不是所有的視圖都有 intrinsicContentSize, UIView預設情況下就傳回的是 UIViewNoIntrinsicMetric。隻有當視圖中需要根據内部内容進行調整大小時,我們才需要用到 intrinsicContentSize。
當視圖大小在變化時,我們可以使用上面最後四個API來設定視圖的壓縮或者放大的方式。
- typedef NS_ENUM(NSInteger, UILayoutConstraintAxis) {
- UILayoutConstraintAxisHorizontal = 0,
- UILayoutConstraintAxisVertical = 1
- };
上面最後四個API主要是通過修改水準或者垂直方向的優先級來實作視圖是基于水準縮小(放大)還是垂直縮小(放大)。當我們的視圖需要根據内部内容進行調整大小時,我們應該使用上述方法為目前視圖設定初始值。而不應該重寫這幾個方法。
UIConstraintBasedLayoutFittingSize
- UIKIT_EXTERN const CGSize UILayoutFittingCompressedSize NS_AVAILABLE_IOS(6_0);
- UIKIT_EXTERN const CGSize UILayoutFittingExpandedSize NS_AVAILABLE_IOS(6_0);
- @interface UIView (UIConstraintBasedLayoutFittingSize)
- - (CGSize)systemLayoutSizeFittingSize:(CGSize)targetSize NS_AVAILABLE_IOS(6_0);
- - (CGSize)systemLayoutSizeFittingSize:(CGSize)targetSize withHorizontalFittingPriority:(UILayoutPriority)horizontalFittingPriority verticalFittingPriority:(UILayoutPriority)verticalFittingPriority NS_AVAILABLE_IOS(8_0);
- @end
上面兩個API可以獲得目前使用AutoLayout視圖的size。其中targetSize可以傳入UILayoutFittingCompressedSize或者UILayoutFittingExpandedSize,分别對應的是最小情況下可能的Size和最大情況下可能的Size。
UIConstraintBasedLayoutDebugging
- - (NSArray *)constraintsAffectingLayoutForAxis:(UILayoutConstraintAxis)axis NS_AVAILABLE_IOS(6_0);
- - (BOOL)hasAmbiguousLayout NS_AVAILABLE_IOS(6_0);
- - (void)exerciseAmbiguityInLayout NS_AVAILABLE_IOS(6_0);
第一個API可以獲得視圖在不同方向上所有的布局限制。
hasAmbiguousLayout :可以知道目前視圖的布局是否會有歧義。這裡有一個私有API _autolayoutTrace可以獲得整個視圖樹的字元串。
- #ifdef DEBUG
- NSLog(@"%@", [self performSelector:@selector(_autolayoutTrace)]);
- #endif
exerciseAmbiguityInLayout :這個方法會随機改變視圖的layout到另外一個有效的layout。這樣我們就可以很清楚的看到哪一個layout導緻了整體的布局限制出現了錯誤,或者我們應該增加更多的布局限制。
我們應該讓上面的四個方法隻在DEBUG環境下被調用。
新增支援 UITraitEnvironment Protocol
- @protocol UITraitEnvironment
- @property (nonatomic, readonly) UITraitCollection *traitCollection;
- - (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection;
- @end
iOS 8 上新增了Size Class的概念,其中UITraitCollection類用來描述不同Size大小。關于Size Class和UITraitCollection的概念可參考如下連結:http://joywii.github.io/blog/2014/09/24/ios8-size-classesde-li-jie-yu-shi-yong/
UIView實作了這個協定,我們可以獲得目前View的traitCollection,進而得知目前View處于什麼樣的Size Class下。并且當traitCollection有變化時,我們可以通過重寫traitCollectionDidChange知道該事件的觸發。預設情況下,這個方法什麼都不執行。
traitCollection的變化是從UIScreen開始被觸發,并且逐層往下傳遞的,具體如下:
UIScreen -> UIWindow -> UIViewController -> ChildViewControllers -> View -> Subviews
關于這一點,我在詳解UICoordinateSpace和UIScreen在iOS 8上的坐标問題一文中有做詳細解釋。
總結
UIView到目前為止(iOS 8.1),所有增加的關于AutoLayout的API請參考上述文章。進一步對這些API了解可以讓我們寫出更健壯的布局代碼。