你可能需要為你的APP適配iOS11
前言
前幾天發現現在在做的APP在iOS11系統上動畫有異常,在其他系統的裝置上都是正常的,動畫的操作是觀察tableView
的contentOffset
變化後執行的,異常動畫發生在tableView reloadData
之後,也就是說tableView reloadData之後,tableView的contentOffset
發生了幾次變化。查了下資料發現原因是iOS11中預設開啟了Self-Sizing
,在WWDC
2017 session204 Updating Your App for iOS 11 中有介紹,因此研究了下這個session,本文作為一個總結,下文的第三部分會有對上述的動畫異常的原因分析及解決方式。
本文內容包括:集成了搜尋的大標題欄、橫向選項卡欄、Margins 和 Insets以及 UIScrollView
和UITableView
的更新和功能更強大的滑動操作。
一. 在UIKit’s Bars中加入的新功能
WWDC通過iOS新增的檔案管理App:Files開始介紹,在Files這個APP中能夠看到iOS11中UIKit’s Bars的一些新特性:在瀏覽功能上的大標題檢視(向上滑動後標題會回到原來的UI效果)、橫屏狀態下tab上的文字和icon會變為左右排列。我用iOS11的模擬器體驗了一下Files這個APP,如下圖所示:
image.png
image.png
(command+向左的箭頭讓模擬器橫屏)
在iPhone上,tab上的圖示較小,tab bar較小,這樣垂直空間可多放置內容。如果有人看不清楚tab bar上的圖示或文字,可以通過長按tab bar上的任意item,會將該item顯示在HUD上,這樣可以清楚的看清icon和text。對tool bar 和 navigation bar同理,長按item也會放大顯示。如下圖顯示:
image.png
UIBarItem
UIBarItem是UI tab bar item和UI bar button item的父類,要想實現上面介紹的效果,只需要為UIBarItem 設定landscapeImagePhone
屬性,在storyboard中也支援這個設定,對於HUD的image需要設定另一個iOS11新增的屬性:largeContentSizeImage
,關於這部分更詳細的討論,可以參考 WWDC2017 Session 215:What's New in
Accessibility
控制大標題的顯示
在UI navigation bar中新增了一個BOOL屬性prefersLargeTitles
navigation bar
就會在整個APP中顯示大標題,如果想要在控制不同頁面大標題的顯示,可以通過設定當前頁面的navigationItem
的largeTitleDisplayMode
屬性;
navigationItem.largeTitleDisplayMode
typedef NS_ENUM(NSInteger, UINavigationItemLargeTitleDisplayMode) {
/// 自動模式依賴上一個 item 的特性
UINavigationItemLargeTitleDisplayModeAutomatic,
/// 針對當前 item 總是啟用大標題特性
UINavigationItemLargeTitleDisplayModeAlways,
/// Never
UINavigationItemLargeTitleDisplayModeNever,
}
Navigation 整合 UISearchController
把你的UISearchController
賦值給navigationItem
,就可以實現將UISearchController
整合到Navigation
。
navigationItem.searchController //iOS 11 新增屬性
navigationItem.hidesSearchBarWhenScrolling //決定滑動的時候是否隱藏搜尋框;iOS 11 新增屬性
UINavigationController和滾動互動
滾動的時候,以下互動操作都是由UINavigationController
負責調動的:
UIsearchController搜尋框效果更新
大標題效果的控制
Rubber banding效果 //當你開始往下拉,大標題會變大來回應那個滾輪
所以,如果你使用navigation bar
,組裝一些整個push和pop體驗,你不會得到searchController
的整合、大標題的控制更新和Rubber banding
效果,因為這些都是由UINavigationController
控制的。
UIToolbar and UINavigationBar— Layout
在 iOS 11 中,當蘋果進行所有這些新特性時,也進行了其他的優化,針對 UIToolbar 和 UINavigaBar 做了新的自動佈局擴充套件支援,自定義的bar button items、自定義的title都可以通過layout來表示尺寸。
需要注意的是,你的constraints
需要在view內部設定,所以如果你有一個自定義的標題檢視,你需要確保任何約束只依賴於標題檢視及其任何子檢視。當你使用自動佈局,系統假設你知道你在做什麼。
Avoiding Zero-Sized Custom Views
自定義檢視的size為0是因為你有一些模糊的約束佈局。要避免檢視尺寸為0,可以從以下方面做:
-
UINavigationBar 和 UIToolbar 提供位置
-
開發者則必須提供檢視的size,有三種方式:
-
對寬度和高度的約束;
-
實現 intrinsicContentSize;
-
通過約束關聯你的子檢視;
二. 管理margins 和 insets
layout margins
基於約束的Auto Layout,使我們搭建能夠動態響應內部和外部變化的使用者介面。Auto Layout為每一個view都定義了margin
。margin
指的是控制元件顯示內容部分的邊緣和控制元件邊緣的距離。
可以用layoutMargins
或者layoutMarginsGuide
屬性獲得view的margin
,margin
是檢視內部的一部分。layoutMargins
允許獲取或者設定UIEdgeInsets
結構的margin
。layoutMarginsGuide
則獲取到只讀的UILayoutGuide
物件。
在iOS11新增了一個屬性:directional layout margins
,該屬性是NSDirectionalEdgeInsets
結構體型別的屬性:
typedef struct NSDirectionalEdgeInsets {
CGFloat top, leading, bottom, trailing;
} NSDirectionalEdgeInsets API_AVAILABLE(ios(11.0),tvos(11.0),watchos(4.0));
layoutMargins
是UIEdgeInsets
結構體型別的屬性:
typedef struct UIEdgeInsets {
CGFloat top, left, bottom, right;
} UIEdgeInsets;
從上面兩種結構體的對比可以看出,NSDirectionalEdgeInsets
屬性用leading 和 trailing 取代了之前的 left 和 right。
directional layout margins屬性的說明如下:
directionalLayoutMargins.leading is used on the left when the user interface direction is LTR and on the right for RTL.
Vice versa for directionalLayoutMargins.trailing.
例子:當你設定了trailing = 30;當在一個right to left 語言下trailing的值會被設定在view的左邊,可以通過layoutMargin的left屬性讀出該值。如下圖所示:
image.png
還有其他一些更新。自從引入
layout margins
,當將一個view新增到viewController
時,viewController
會修復view的的layoutMargins
為UIKit定義的一個值,這些調整對外是封閉的。從iOS11開始,這些不再是一個固定的值,它們實際是最小值,你可以改變view的layoutMargins
為任意一個更大的值。而且,viewController
新增了一個屬性:viewRespectsSystemMinimumLayoutMargins
,如果你設定該屬性為"false",你就可以改變你的layoutMargins
為任意你想設定的值,包括0,如下圖所示:image.png
安全區域(Safe Area)
如下圖:照片應用程式
image.png
從iOS 7以來,我們在整個作業系統中都有這些半透明的bars,蘋果鼓勵我們通過這些bars繪製內容,我們是通過viewController 的edgesForExtendedLayout屬性來做這些的。
iOS 7 開始,在
UIViewController
中引入的 topLayoutGuide
和 bottomLayoutGuide
在 iOS 11 中被廢棄了!取而代之的就是safeArea
的概念,safeArea
是描述你的檢視部分不被任何內容遮擋的方法。
它提供兩種方式:safeAreaInsets
或safeAreaLayoutGuide
來提供給你safeArea的參照值,即 insets 或者 layout guide。 safeArea區域如圖所示:image.png
如果有一個自定義的
viewController
,你可能要新增你自己的bars,增加safeAreaInsets
的值,可以通過一個新的屬性:addtionalSafeAreaInsets
來改變safeAreaInsets
的值,當你的viewController
改變了它的safeAreaInsets
值時,有兩種方式獲取到回撥:
UIView.safeAreaInsetsDidChange()
UIViewController.viewSafeAreaInsetsDidChange()
三. UIScrollView and UITableView的新特性
Scroll Views
如果有一些文字位於UI滾動檢視的內部,幷包含在導航控制器中,現在一般navigationContollers
會傳入一個contentInset
給其最頂層的viewController
的scrollView,在iOS11中進行了一個很大的改變,不再通過scrollView的contentInset
屬性了,而是新增了一個屬性:adjustedContentInset
,通過下面兩種圖的對比,能夠表示adjustContentInset
表示的區域:
image.png
image.png
新增的contentInsetAdjustmentBehavior
屬性用來配置adjustedContentInset
的行為,該結構體有以下幾種型別:
typedef NS_ENUM(NSInteger, UIScrollViewContentInsetAdjustmentBehavior) {
UIScrollViewContentInsetAdjustmentAutomatic,
UIScrollViewContentInsetAdjustmentScrollableAxes,
UIScrollViewContentInsetAdjustmentNever,
UIScrollViewContentInsetAdjustmentAlways,
}
@property(nonatomic) UIScrollViewContentInsetAdjustmentBehavior contentInsetAdjustmentBehavior;
@property(nonatomic, readonly) UIEdgeInsets adjustedContentInset;
//adjustedContentInset值被改變的delegate
- (void)adjustedContentInsetDidChange;
- (void)scrollViewDidChangeAdjustedContentInset:(UIScrollView *)scrollView;
Table Views :在iOS 11中預設啟用Self-Sizing
這個應該是UITableView最大的改變。我們知道在iOS8引入Self-Sizing 之後,我們可以通過實現estimatedRowHeight
相關的屬性來展示動態的內容,實現了estimatedRowHeight
屬性後,得到的初始contenSize是個估算值,是通過estimatedRowHeight
x cell
的個數得到的,並不是最終的contenSize
,tableView
不會一次性計算所有的cell
的高度了,只會計算當前螢幕能夠顯示的cell個數再加上幾個,滑動時,tableView
不停地得到新的cell,更新自己的contenSize,在滑到最後的時候,會得到正確的contenSize。建立tableView到顯示出來的過程中,contentSize的計算過程如下圖:
image.png
Self-Sizing
在iOS11下是預設開啟的,Headers, footers, and cells都預設開啟Self-Sizing
,所有estimated 高度預設值從iOS11之前的 0 改變為UITableViewAutomaticDimension
:
@property (nonatomic) CGFloat estimatedRowHeight NS_AVAILABLE_IOS(7_0); // default is UITableViewAutomaticDimension, set to 0 to disable
如果目前專案中沒有使用estimateRowHeight屬性,在iOS11的環境下就要注意了,因為開啟Self-Sizing
之後,tableView是使用estimateRowHeight
屬性的,這樣就會造成contentSize和contentOffset值的變化,如果是有動畫是觀察這兩個屬性的變化進行的,就會造成動畫的異常,因為在估算行高機制下,contentSize的值是一點點地變化更新的,所有cell顯示完後才是最終的contentSize值。因為不會快取正確的行高,tableView
reloadData的時候,會重新計算contentSize,就有可能會引起contentOffset的變化。iOS11下不想使用Self-Sizing
的話,可以通過以下方式關閉:
self.tableView.estimatedRowHeight = 0;
self.tableView.estimatedSectionHeaderHeight = 0;
self.tableView.estimatedSectionFooterHeight = 0;
iOS11下,如果沒有設定estimateRowHeight
的值,也沒有設定rowHeight的值,那contentSize計算初始值是 44 * cell的個數,如下圖:
contentSize.png
Table Views:separatorInset 擴充套件
iOS 7 引入separatorInset
屬性,用以設定 cell 的分割線邊距,在 iOS 11 中對其進行了擴充套件。可以通過新增的UITableViewSeparatorInsetReference
列舉型別的separatorInsetReference
屬性來設定separatorInset
屬性的參照值。
typedef NS_ENUM(NSInteger, UITableViewSeparatorInsetReference) {
UITableViewSeparatorInsetFromCellEdges, //預設值,表示separatorInset是從cell的邊緣的偏移量
UITableViewSeparatorInsetFromAutomaticInsets //表示separatorInset屬性值是從一個insets的偏移量
}
下圖清晰的展示了這兩種參照值的區別:
separatorInsetReference.png
Table Views 和 Safe Area
有以下幾點需要注意:
separatorInset
被自動地關聯到safe area insets
,因此,預設情況下,表檢視的整個內容避免了其根檢視控制器的安全區域的插入。UITableviewCell
和UITableViewHeaderFooterView
的 content view 在安全區域內;因此你應該始終在 content view 中使用add-subviews
操作。- 所有的 headers 和 footers 都應該使用
UITableViewHeaderFooterView
,包括 table headers 和 footers、section headers 和 footers。
滑動操作(Swipe Actions)
在iOS8之後,蘋果官方增加了UITableVIew的右滑操作介面,即新增了一個代理方法(tableView: editActionsForRowAtIndexPath:)和一個類(UITableViewRowAction),代理方法返回的是一個數組,我們可以在這個代理方法中定義所需要的操作按鈕(刪除、置頂等),這些按鈕的類就是UITableViewRowAction
。這個類只能定義按鈕的顯示文字、背景色、和按鈕事件。並且返回陣列的第一個元素在UITableViewCell的最右側顯示,最後一個元素在最左側顯示。從iOS
11開始有了一些改變,首先是可以給這些按鈕新增圖片了,然後是如果實現了以下兩個iOS 11新增的代理方法,將會取代(tableView: editActionsForRowAtIndexPath:)代理方法:
// Swipe actions
// These methods supersede -editActionsForRowAtIndexPath: if implemented
- (nullable UISwipeActionsConfiguration *)tableView:(UITableView *)tableView leadingSwipeActionsConfigurationForRowAtIndexPath:(NSIndexPath *)indexPath
- (nullable UISwipeActionsConfiguration *)tableView:(UITableView *)tableView trailingSwipeActionsConfigurationForRowAtIndexPath:(NSIndexPath *)indexPath
這兩個代理方法返回的是UISwipeActionsConfiguration
型別的物件,建立該物件及賦值可看下面的程式碼片段:
- ( UISwipeActionsConfiguration *)tableView:(UITableView *)tableView trailingSwipeActionsConfigurationForRowAtIndexPath:(NSIndexPath *)indexPath {
//刪除
UIContextualAction *deleteRowAction = [UIContextualAction contextualActionWithStyle:UIContextualActionStyleDestructive title:@"delete" handler:^(UIContextualAction * _Nonnull action, __kindof UIView * _Nonnull sourceView, void (^ _Nonnull completionHandler)(BOOL)) {
[self.titleArr removeObjectAtIndex:indexPath.row];
completionHandler (YES);
}];
deleteRowAction.image = [UIImage imageNamed:@"icon_del"];
deleteRowAction.backgroundColor = [UIColor blueColor];
UISwipeActionsConfiguration *config = [UISwipeActionsConfiguration configurationWithActions:@[deleteRowAction]];
return config;
}
建立UIContextualAction
物件時,UIContextualActionStyle
有兩種型別,如果是置頂、已讀等按鈕就使用UIContextualActionStyleNormal
型別,delete操作按鈕可使用UIContextualActionStyleDestructive
型別,當使用該型別時,如果是右滑操作,一直向右滑動某個cell,會直接執行刪除操作,不用再點選刪除按鈕,這也是一個好玩的更新。
typedef NS_ENUM(NSInteger, UIContextualActionStyle) {
UIContextualActionStyleNormal,
UIContextualActionStyleDestructive
} NS_SWIFT_NAME(UIContextualAction.Style)
滑動操作這裡還有一個需要注意的是,當cell高度較小時,會只顯示image,不顯示title,當cell高度夠大時,會同時顯示image和title。我寫demo測試的時候,因為每個cell的高度都較小,所以只顯示image,然後我增加cell的高度後,就可以同時顯示image和title了。見下圖對比:
image.png
總結
大概介紹了iOS 11的UI方面的一些更新,大部分內容自己程式碼測試過了,有些更新確實是很實用,可以適配下iOS 11,有的更新可能會給現有APP造成bug,所以學習下這些內容還是很有必要的。