玩轉iOS開發:iOS 11 新特性《Layout的新特性》

CainLuo發表於2019-02-20

文章分享至我的個人技術部落格: https://cainluo.github.io/15101116434794.html


隨著蘋果爸爸越來越多尺寸的裝置釋出, 還有iOS設計的改變, 特別是在iOS 11之後, 比大更大的導航欄, 然後再滾動的時候可以改變大小等等操作.

但這些問題都不是什麼問題, 就如同在WWDC 2017一樣, 蘋果爸爸在跟我們開發者展示一樣東西, 也是他一直想我們去使用的東西, 那就是自動佈局.

隨著什麼10.5英寸, 5.8英寸, 12.9英寸這些裝置的釋出, 讓我們開發者在適配多個尺寸的時候也越來越麻煩了, 但隨著使用自動佈局的釋出可以讓我們開發者更加註重App的業務上的開發, 再也不用去計算這個差多少, 那個差多少, 旋轉一下又怎麼處理.

這裡我們就使用一個簡單的小專案來搗鼓就好了.

轉載宣告:如需要轉載該文章, 請聯絡作者, 並且註明出處, 以及不能擅自修改本文.

更大的標題

iOS 11裡, 最明顯的變化就是導航欄裡多了一個大標題:

1

這裡我們可以通過設定UINavigationBar的一個BOOL值屬性來決定是否顯示這個大標題:

@property (nonatomic, readwrite, assign) BOOL prefersLargeTitles;
複製程式碼

那如果你是在Storyboard上的話, 你也可以在UINavigationController上的UINavigationBar設定這個屬性, 勾上就是為顯示大標題, 不勾上就不顯示.

雖然prefersLargeTitles是開啟大標題的主開關, 但在每一個控制器裡, 我們都可以每個控制器裡的UINavigationItem來顯示是否顯示大標題, 或者是普通標題, 這裡共有三種顯示型別:

typedef NS_ENUM(NSInteger, UINavigationItemLargeTitleDisplayMode) {
    UINavigationItemLargeTitleDisplayModeAutomatic,
    UINavigationItemLargeTitleDisplayModeAlways,
    UINavigationItemLargeTitleDisplayModeNever,
} NS_SWIFT_NAME(UINavigationItem.LargeTitleDisplayMode);
複製程式碼

要使用這個屬性呢, 我們得提前把prefersLargeTitles大標題屬性設定為YES, 然後才能去搗鼓上面的三種顯示模式, 預設為Automatic, 我們可以通過這個來設定控制器是否需要顯示大標題啦~

當然如果你不想每個控制器都寫一遍, 那你可以自己用RunTime寫個Method Swizzling, 或者是自己封裝一個RootController, 一個ChildController, 這樣子區分也可以:

2

搜尋控制器

而第二個變化就是在UINavigationController裡整合了UISearchController, 雖然UISearchController並不是什麼新的東西, 但在iOS 11之後, 我們可以將UINavigationItemsearchController屬性設定為UISearchController.

    UISearchController *searchController = [[UISearchController alloc] initWithSearchResultsController:nil];
    
    self.navigationItem.searchController = searchController;
    self.navigationItem.hidesSearchBarWhenScrolling = YES;
複製程式碼

3

這裡還有一個屬性叫做hidesSearchBarWhenScrolling, 如果設定為YES的話, 那麼在操作的時候, 就會根據滑動來隱藏這個searchController, 如果設定為NO, 就一直顯示著啦, 這個屬性預設是為YES.

4

安全區域

相信iOS 11釋出的時候, 很多人都有一個黑人問號的表情, 安全區域(Safe area)是個什麼鬼.

其實早在iOS 7出來的時候就有兩個屬性topLayoutGuidebottomLayoutGuide, 這兩個屬性用於自動佈局, 由於在iOS 7的時候引入了半透明的概念, 如果我們用UITableView這類控制元件佈局的話, 那麼在UINavigationBarUITabBar的背後會顯示內容.

而我們設定了topLayoutGuidebottomLayoutGuide就不會有這個問題了, 但遺憾的是, 這兩個屬性是屬於Controller而不是屬於UIView, 為了解決這個問題, 蘋果爸爸搗鼓了一個新的東西, 就是現在的安全區域, 一個名為safeAreaLayoutGuide的東西, 是屬於UIVIew的.

    @property(nonatomic,readonly,strong) UILayoutGuide *safeAreaLayoutGuide;
複製程式碼

safeAreaLayoutGuide很適合我們在安全區域裡建立約束, 這樣子我們就可以非常簡單的適配各式各樣的機型, 比如強調安全區域的iPhone X.

如果我們只是想要測量一下這個安全區域, 那麼safeAreaInsets就會給我們返回一些值.

typedef struct UIEdgeInsets {
    CGFloat top, left, bottom, right;
} UIEdgeInsets;
複製程式碼

如果我們想知道這些值是在什麼時候改變的, 我們可以通過系統提供的兩個API:

// UIView的API
- (void)safeAreaInsetsDidChange;

// UIViewController
- (void)viewSafeAreaInsetsDidChange;
複製程式碼

如果我們不去修改這個安全區域的話, 我們從safeAreaInsets獲得的值都是為0, 如果我們手動去修改的話, 就可以給動一動additionalSafeAreaInsets這個屬性:

    self.additionalSafeAreaInsets = UIEdgeInsetsMake(100, 50, 0, 0);
複製程式碼

然後在看看效果:

玩轉iOS開發:iOS 11 新特性《Layout的新特性》

如果你是使用storyboard的話, 你可以隨便點選一個控制元件或者是控制器, 然後檢視是否勾上了Use Safe Area Layout Guides, 如果勾上了的話, 那麼Xcode就會將在之前在頂部和底部的佈局約束自動轉換到安全區域中.

6

但有一個情況是需要自己手動的搗鼓的, 這個時候我們就要取消Use Safe Area Layout Guides, 然後再手動佈局, 並且把之前相對於安全區域的約束雙擊, 然後設定為SuperView:

7

8

注意: 如果我們之前對topLayoutGuidebottomLayoutGuide設定了約束, 而這個時候我們把Use Safe Area Layout Guides勾掉, 那麼就會和剛剛說的那樣, UINavigationBarUITabBar的背後會顯示內容, 如果我們要限制一下檢視到頂部的話, 我們應該在檢視的topAnchortopLayoutGuide.bottomAnchor新增一個約束, 但是在iOS 11中, 我們可以在topAnchorsafeAreaLayoutGuide.topAnchor之間新增一個約束就哦了. PS: topLayoutGuide指示狀態列和導航欄覆蓋的區域. safeAreaLayoutGuide指示狀態列和導航欄未覆蓋的區域

9

10

邊距

Margins也有一些新的變化, 比如有些屬性被棄用了:

被代替:

@property (nonatomic) UIEdgeInsets layoutMargins;
複製程式碼

代替的屬性:

@property (nonatomic) NSDirectionalEdgeInsets directionalLayoutMargins;
複製程式碼

新的directionalLayoutMargins是允許改變閱讀方向, 用了leadingtrailing代替了leftright, 雖然仍然是用layoutMarginsGuide

當我們設定directionalLayoutMargins的時候, 它的值是會被新增到systemMinimumLayoutMargins中, 用來確定檢視的實際邊距, 如果我們不想要系統的最小邊距, 我們可以把viewRespectsSystemMinimumLayoutMargins設定為NO就可以了.

最後一點補充的一個屬性為:

    @property (nonatomic) BOOL insetsLayoutMarginsFromSafeArea;
複製程式碼

這個屬性如果為YES, 那麼我們需要佈局的檢視邊距就相對於安全區域, 如果設定為NO, 那我們需要佈局的檢視邊距就相對於其他檢視的邊距, 預設為YES.

滾動檢視們

iOS 11之前, 如果我們要對UIScrollView使用自動佈局, 我們需要寫一些邏輯來確定是約束UIScrollView的滾動檢視, 還是它的內容區域, 有時候在新增約束的時就會容易發生約束錯誤的情況, 比如在使用Storyboard佈局.

但在iOS 11時, 為了解決這個問題, 蘋果爸爸給UIScrollView新增了兩個約束屬性:

@property(nonatomic,readonly,strong) UILayoutGuide *contentLayoutGuide;
@property(nonatomic,readonly,strong) UILayoutGuide *frameLayoutGuide;
複製程式碼

這兩個屬性可以讓我們給UIScrollView新增約束時更加的精準, 但兩個屬性對於使用Storyboard的開發者來講應該不是一件好訊息, 因為這兩個東西只能在程式碼中使用.

除了上面兩個屬性歪, 還有另一個東西會影響到UIScrollView內容區域:

@property(nonatomic,assign) BOOL automaticallyAdjustsScrollViewInsets;
複製程式碼

這個屬性預設為YES, 有時候我們的檢視不會顯示在UINavigationBar的底部就是這個屬性搞的鬼, 把它設定為NO就好了.

但慶幸的是, 在iOS 11這個屬性被幹掉了, 系統也不再自動去設定UIScrollView的內容, 現在UIScrollView的內容插入調整是從安全區域和我們在contentInset設定的值計算得出, 由以下屬性控制, 暫時共有四種控制方式:

@property(nonatomic) UIScrollViewContentInsetAdjustmentBehavior contentInsetAdjustmentBehavior;

typedef NS_ENUM(NSInteger, UIScrollViewContentInsetAdjustmentBehavior) {
    UIScrollViewContentInsetAdjustmentAutomatic,
    UIScrollViewContentInsetAdjustmentScrollableAxes,
    UIScrollViewContentInsetAdjustmentNever,
    UIScrollViewContentInsetAdjustmentAlways,
} API_AVAILABLE(ios(11.0),tvos(11.0));
複製程式碼

Demo

為了更加好理解, 這裡展示一個Demo, 使用Storyboard加程式碼去實現.

ScrollView的佈局:

11

UIImageView的佈局:

12

TipsView的佈局:

13

14

PS: 如果你的約束感覺不對的話, 只要雙擊約束, 就可以進入到裡面去修改了.

15

16

這裡的程式碼也不難, 主要就是針對TipsViewScrollView的佈局:

    CGFloat scrollIndicatorMargin = 8;
    
    self.tipsView.layer.cornerRadius = 8;
    
    [self.tipsView.leadingAnchor constraintEqualToAnchor:self.scrollView.frameLayoutGuide.leadingAnchor
                                                constant:scrollIndicatorMargin].active = YES;
    
    [self.tipsView.trailingAnchor constraintEqualToAnchor:self.scrollView.frameLayoutGuide.trailingAnchor
                                                constant:-scrollIndicatorMargin].active = YES;
    
    [self.tipsView.bottomAnchor constraintEqualToAnchor:self.scrollView.frameLayoutGuide.bottomAnchor
                                                constant:-scrollIndicatorMargin].active = YES;
    
    self.additionalSafeAreaInsets = UIEdgeInsetsMake(0,
                                                     0,
                                                     self.tipsView.frame.size.height + scrollIndicatorMargin,
                                                     scrollIndicatorMargin);
    
    self.scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
複製程式碼

17

如果想更詳細的檢視約束, 可以自行開啟Demo慢慢研究哈~.

自適應的Cells

iOS 7推出的時候, UITableView就有一個屬性叫做:

    @property (nonatomic) CGFloat estimatedRowHeight;
複製程式碼

我們可以通過設定為UITableViewAutomaticDimension, 再給Cell的內部檢視做好適配的約束, 那麼Cell就可以自適應了.

但這些都是需要我們自己手動去適配, 在iOS 11時, 這個東西已經不需要我們去寫了, 預設就是UITableViewAutomaticDimension.

而且這個屬性不單單只是對普通的Cell有用, 包括SectionHeaderSectionFooter同樣都有效, 如果你不需要的話, 可以手動把estimatedRowHeight設定為0.

如果你的專案是使用比較老的Xcode, 並且是使用Storyboard搗鼓的話, 你可以開啟對應的Storyboard找到UITableView, 然後找到對應的屬性, 勾上Automatic:

18

但是大規模的使用自動佈局會造成一個效能上的問題, 這個之前也說過了, 那要怎麼做呢? 我們可以把estimatedRowHeight設定為大概要顯示多高的數值, 然後確定好Cell內的所有約束, 這樣子UITableView的效能就會得到改善, 當然最好的方式還是使用非同步載入, 或者是高度快取之類的, 這些資料的話, 大家可以自行去百度搜搜.

重新整理控制器

如果我們給對應的UITableViewController新增UIRefreshControl的話, 那麼它會自動載入到UINavigationBar中:

19

程式碼也是很簡單:

    self.tableView.refreshControl = [[UIRefreshControl alloc] init];
    
    [self.tableView.refreshControl addTarget:self
                                      action:@selector(refreshControllerAction)
                            forControlEvents:UIControlEventValueChanged];
複製程式碼
- (void)refreshControllerAction {
    
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        
        [self.tableView.refreshControl endRefreshing];
    });
}
複製程式碼

UITableVIew的分割線

我們都知道在iOS 7的時候, 蘋果爸爸就給UITableView的分割線新增了一些偏移, 但那時候只能通過separatorInset去設定, 但這種設定太過死板缺少靈活性.

iOS 11之後, 蘋果爸爸又新增了一個屬性:

@property (nonatomic) UITableViewSeparatorInsetReference separatorInsetReference;

typedef NS_ENUM(NSInteger, UITableViewSeparatorInsetReference) {
    UITableViewSeparatorInsetFromCellEdges,    
    UITableViewSeparatorInsetFromAutomaticInsets
} API_AVAILABLE(ios(11.0), tvos(11.0));
複製程式碼
  • UITableViewSeparatorInsetFromCellEdges: 預設值, 如果使用該屬性的話, 分割線的起始座標為Cell的邊緣值, 也就是0.
  • UITableViewSeparatorInsetFromAutomaticInsets: 如果使用該屬性的話, 分割線的起始座標會帶上預設值, 比如左邊的偏移為15.

20

堆疊檢視Stack Views

我們都知道了在iOS 9的時候釋出了一個靈活佈局的堆疊檢視UIStackViews, 有了它我們就不需要管理大量的約束.

但有一些場景還是沒有適應到了, 現在就可以解決這個問題了, 在iOS 11時, 蘋果爸爸給它增加了更多的特性, 比如我們在多個檢視裡, 有一個檢視比較特殊, 要離別的地方比較遠, 之前是不可實現的, 現在可以了, 讓我們來看看吧:

21

詳細的約束佈局就麻煩大家去工程裡看看吧, 這裡就不多說了, 直接看程式碼:

    [UIViewPropertyAnimator runningPropertyAnimatorWithDuration:0.5
                                                          delay:0
                                                        options:UIViewAnimationOptionCurveEaseInOut
                                                     animations:^{
        
                                                         if (self.action) {
                                                             
                                                             [self.stackView setCustomSpacing:self.customSpacing
                                                                                    afterView:self.sunImageView];
                                                         } else {
                                                             
                                                             [self.stackView setCustomSpacing:0
                                                                                    afterView:self.sunImageView];
                                                         }
                                                         
                                                         self.action = !self.action;
                                                         
                                                     } completion:nil];
複製程式碼

向量圖

在以前的Xcode版本里, 如果我們要使用向量圖, 那麼XcodeiOS系統會將這個向量圖在編譯的時候自動生成不同大小的圖片.

而在Xcode 9中, 我們可以勾選一個東西, 告訴系統保留向量資料:

22

這樣子的話, 當我們或者是其他使用者在輔助功能裡開啟的大字號字型時, 我們就可以通過設定UIImageView的一個屬性來保證可以正常顯示:

@property (nonatomic) BOOL adjustsImageSizeForAccessibilityContentSizeCategory;
複製程式碼

Demo裡, 我只設定了一個圖示的屬性, 所以這個圖示是正常顯示, 而另外兩個是不正常的:

23

這個屬性可以在Storyboard找到, 也可以在程式碼裡實現, 只要是UIImageViewok了.

自定義導航欄檢視

最後就是補充一下給UINavigationBar或者是UIToolbar新增自定義檢視了.

我們還是拿剛剛的那個太陽, 星星和月亮的來舉例子, 這裡只要一個UIImageView和一個UILabel就好了.

這個佈局和我們去自定義UITableViewCell差不多, 直接用約束搗鼓就完事, 而且還特別的簡單:

24

執行的話, 我們可以看到是正常顯示的, 但發現會有一個白色的背景色, 我們只要找到對應的UIView, 然後把顏色清理掉就ok了.

總結

iOS 11有一些會影響到自動佈局的變化, 還有些新增的屬性來幫助我們開發者更便捷的開發, 如果你覺得光看文章還不夠的話, 可以去看看原汁原味的WWDC 2017的介紹:

工程

https://github.com/CainRun/iOS-11-Characteristic/tree/master/3.Layout

最後

碼字很費腦, 看官賞點飯錢可好

微信

支付寶

相關文章