文章分享至我的個人技術部落格: https://cainluo.github.io/15101116434794.html
隨著蘋果爸爸越來越多尺寸的裝置釋出, 還有iOS
設計的改變, 特別是在iOS 11
之後, 比大更大的導航欄, 然後再滾動的時候可以改變大小等等操作.
但這些問題都不是什麼問題, 就如同在WWDC 2017
一樣, 蘋果爸爸在跟我們開發者展示一樣東西, 也是他一直想我們去使用的東西, 那就是自動佈局.
隨著什麼10.5英寸, 5.8英寸, 12.9英寸這些裝置的釋出, 讓我們開發者在適配多個尺寸的時候也越來越麻煩了, 但隨著使用自動佈局的釋出可以讓我們開發者更加註重App
的業務上的開發, 再也不用去計算這個差多少, 那個差多少, 旋轉一下又怎麼處理.
這裡我們就使用一個簡單的小專案來搗鼓就好了.
轉載宣告:如需要轉載該文章, 請聯絡作者, 並且註明出處, 以及不能擅自修改本文.
更大的標題
在iOS 11
裡, 最明顯的變化就是導航欄裡多了一個大標題:
這裡我們可以通過設定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
, 這樣子區分也可以:
搜尋控制器
而第二個變化就是在UINavigationController
裡整合了UISearchController
, 雖然UISearchController
並不是什麼新的東西, 但在iOS 11
之後, 我們可以將UINavigationItem
的searchController
屬性設定為UISearchController
.
UISearchController *searchController = [[UISearchController alloc] initWithSearchResultsController:nil];
self.navigationItem.searchController = searchController;
self.navigationItem.hidesSearchBarWhenScrolling = YES;
複製程式碼
這裡還有一個屬性叫做hidesSearchBarWhenScrolling
, 如果設定為YES
的話, 那麼在操作的時候, 就會根據滑動來隱藏這個searchController
, 如果設定為NO
, 就一直顯示著啦, 這個屬性預設是為YES
.
安全區域
相信iOS 11
釋出的時候, 很多人都有一個黑人問號的表情, 安全區域(Safe area)是個什麼鬼.
其實早在iOS 7
出來的時候就有兩個屬性topLayoutGuide
和bottomLayoutGuide
, 這兩個屬性用於自動佈局, 由於在iOS 7
的時候引入了半透明的概念, 如果我們用UITableView
這類控制元件佈局的話, 那麼在UINavigationBar
和UITabBar
的背後會顯示內容.
而我們設定了topLayoutGuide
和bottomLayoutGuide
就不會有這個問題了, 但遺憾的是, 這兩個屬性是屬於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);
複製程式碼
然後在看看效果:
如果你是使用storyboard
的話, 你可以隨便點選一個控制元件或者是控制器, 然後檢視是否勾上了Use Safe Area Layout Guides
, 如果勾上了的話, 那麼Xcode
就會將在之前在頂部和底部的佈局約束自動轉換到安全區域中.
但有一個情況是需要自己手動的搗鼓的, 這個時候我們就要取消Use Safe Area Layout Guides
, 然後再手動佈局, 並且把之前相對於安全區域的約束雙擊, 然後設定為SuperView
:
注意: 如果我們之前對
topLayoutGuide
和bottomLayoutGuide
設定了約束, 而這個時候我們把Use Safe Area Layout Guides
勾掉, 那麼就會和剛剛說的那樣,UINavigationBar
和UITabBar
的背後會顯示內容, 如果我們要限制一下檢視到頂部的話, 我們應該在檢視的topAnchor
和topLayoutGuide.bottomAnchor
新增一個約束, 但是在iOS 11
中, 我們可以在topAnchor
和safeAreaLayoutGuide.topAnchor
之間新增一個約束就哦了. PS:topLayoutGuide
指示狀態列和導航欄覆蓋的區域.safeAreaLayoutGuide
指示狀態列和導航欄未覆蓋的區域
邊距
Margins
也有一些新的變化, 比如有些屬性被棄用了:
被代替:
@property (nonatomic) UIEdgeInsets layoutMargins;
複製程式碼
代替的屬性:
@property (nonatomic) NSDirectionalEdgeInsets directionalLayoutMargins;
複製程式碼
新的directionalLayoutMargins
是允許改變閱讀方向, 用了leading
和trailing
代替了left
和right
, 雖然仍然是用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
的佈局:
UIImageView
的佈局:
TipsView
的佈局:
PS: 如果你的約束感覺不對的話, 只要雙擊約束, 就可以進入到裡面去修改了.
這裡的程式碼也不難, 主要就是針對TipsView
和ScrollView
的佈局:
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;
複製程式碼
如果想更詳細的檢視約束, 可以自行開啟Demo
慢慢研究哈~.
自適應的Cells
在iOS 7
推出的時候, UITableView
就有一個屬性叫做:
@property (nonatomic) CGFloat estimatedRowHeight;
複製程式碼
我們可以通過設定為UITableViewAutomaticDimension
, 再給Cell
的內部檢視做好適配的約束, 那麼Cell
就可以自適應了.
但這些都是需要我們自己手動去適配, 在iOS 11
時, 這個東西已經不需要我們去寫了, 預設就是UITableViewAutomaticDimension
.
而且這個屬性不單單只是對普通的Cell
有用, 包括SectionHeader
和SectionFooter
同樣都有效, 如果你不需要的話, 可以手動把estimatedRowHeight
設定為0.
如果你的專案是使用比較老的Xcode
, 並且是使用Storyboard
搗鼓的話, 你可以開啟對應的Storyboard
找到UITableView
, 然後找到對應的屬性, 勾上Automatic
:
但是大規模的使用自動佈局會造成一個效能上的問題, 這個之前也說過了, 那要怎麼做呢? 我們可以把estimatedRowHeight
設定為大概要顯示多高的數值, 然後確定好Cell
內的所有約束, 這樣子UITableView
的效能就會得到改善, 當然最好的方式還是使用非同步載入, 或者是高度快取之類的, 這些資料的話, 大家可以自行去百度搜搜.
重新整理控制器
如果我們給對應的UITableViewController
新增UIRefreshControl
的話, 那麼它會自動載入到UINavigationBar
中:
程式碼也是很簡單:
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.
堆疊檢視Stack Views
我們都知道了在iOS 9
的時候釋出了一個靈活佈局的堆疊檢視UIStackViews
, 有了它我們就不需要管理大量的約束.
但有一些場景還是沒有適應到了, 現在就可以解決這個問題了, 在iOS 11
時, 蘋果爸爸給它增加了更多的特性, 比如我們在多個檢視裡, 有一個檢視比較特殊, 要離別的地方比較遠, 之前是不可實現的, 現在可以了, 讓我們來看看吧:
詳細的約束佈局就麻煩大家去工程裡看看吧, 這裡就不多說了, 直接看程式碼:
[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
版本里, 如果我們要使用向量圖, 那麼Xcode
和iOS
系統會將這個向量圖在編譯的時候自動生成不同大小的圖片.
而在Xcode 9
中, 我們可以勾選一個東西, 告訴系統保留向量資料:
這樣子的話, 當我們或者是其他使用者在輔助功能裡開啟的大字號字型時, 我們就可以通過設定UIImageView
的一個屬性來保證可以正常顯示:
@property (nonatomic) BOOL adjustsImageSizeForAccessibilityContentSizeCategory;
複製程式碼
在Demo
裡, 我只設定了一個圖示的屬性, 所以這個圖示是正常顯示, 而另外兩個是不正常的:
這個屬性可以在Storyboard
找到, 也可以在程式碼裡實現, 只要是UIImageView
就ok
了.
自定義導航欄檢視
最後就是補充一下給UINavigationBar
或者是UIToolbar
新增自定義檢視了.
我們還是拿剛剛的那個太陽, 星星和月亮的來舉例子, 這裡只要一個UIImageView
和一個UILabel
就好了.
這個佈局和我們去自定義UITableViewCell
差不多, 直接用約束搗鼓就完事, 而且還特別的簡單:
執行的話, 我們可以看到是正常顯示的, 但發現會有一個白色的背景色, 我們只要找到對應的UIView
, 然後把顏色清理掉就ok了.
總結
在iOS 11
有一些會影響到自動佈局的變化, 還有些新增的屬性來幫助我們開發者更便捷的開發, 如果你覺得光看文章還不夠的話, 可以去看看原汁原味的WWDC 2017
的介紹:
- WWDC17 Session 204 - Updating Your App for iOS 11 apple.co/2syu3Tt
工程
https://github.com/CainRun/iOS-11-Characteristic/tree/master/3.Layout