iOS 11 為整個生態(tài)系統(tǒng)的 UI 元素帶來了一種更加大膽、動態(tài)的新風(fēng)格。 本文介紹iOS11中在UI方面做了哪些更新,有些更新可以為用戶提供更加完美的體驗(yàn),但也有的可能會給目前的APP帶來異常bug。
WWDC2017大會視頻:
https://developer.apple.com/videos/play/wwdc2017/204/
前言
前幾天發(fā)現(xiàn)現(xiàn)在在做的APP在iOS11系統(tǒng)上動畫有異常,在其他系統(tǒng)的設(shè)備上都是正常的,動畫的操作是觀察tableView的contentOffset變化后執(zhí)行的,異常動畫發(fā)生在tableView reloadData之后,也就是說tableView reloadData之后,tableView的contentOffset發(fā)生了幾次變化。查了下資料發(fā)現(xiàn)原因是iOS11中默認(rèn)開啟了Self-Sizing,在WWDC 2017 session204 Updating Your App for iOS 11 中有介紹,因此研究了下這個session,本文作為一個總結(jié),下文的第三部分會有對上述的動畫異常的原因分析及解決方式。
本文內(nèi)容包括:集成了搜索的大標(biāo)題欄、橫向選項(xiàng)卡欄、Margins 和 Insets以及 UIScrollView和UITableView 的更新和功能更強(qiáng)大的滑動操作。
一. 在UIKit’s Bars中加入的新功能
WWDC通過iOS新增的文件管理App:Files開始介紹,在Files這個APP中能夠看到iOS11中UIKit’s Bars的一些新特性:在瀏覽功能上的大標(biāo)題視圖(向上滑動后標(biāo)題會回到原來的UI效果)、橫屏狀態(tài)下tab上的文字和icon會變?yōu)樽笥遗帕小N矣胕OS11的模擬器體驗(yàn)了一下Files這個APP,如下圖所示:
(command+向左的箭頭讓模擬器橫屏)
在iPhone上,tab上的圖標(biāo)較小,tab bar較小,這樣垂直空間可多放置內(nèi)容。如果有人看不清楚tab bar上的圖標(biāo)或文字,可以通過長按tab bar上的任意item,會將該item顯示在HUD上,這樣可以清楚的看清icon和text。對tool bar 和 navigation bar同理,長按item也會放大顯示。如下圖顯示:
UIBarItem
UIBarItem是UI tab bar item和UI bar button item的父類,要想實(shí)現(xiàn)上面介紹的效果,只需要為UIBarItem 設(shè)置landscapeImagePhone屬性,在storyboard中也支持這個設(shè)置,對于HUD的image需要設(shè)置另一個iOS11新增的屬性:largeContentSizeImage,關(guān)于這部分更詳細(xì)的討論,可以參考 WWDC2017 Session 215:What's New in Accessibility
控制大標(biāo)題的顯示
在UI navigation bar中新增了一個BOOL屬性prefersLargeTitles,將該屬性設(shè)置為ture,navigation bar就會在整個APP中顯示大標(biāo)題,如果想要在控制不同頁面大標(biāo)題的顯示,可以通過設(shè)置當(dāng)前頁面的navigationItem的largeTitleDisplayMode屬性;
navigationItem.largeTitleDisplayMode
typedef NS_ENUM(NSInteger, UINavigationItemLargeTitleDisplayMode) {
/// 自動模式依賴上一個 item 的特性
UINavigationItemLargeTitleDisplayModeAutomatic,
/// 針對當(dāng)前 item 總是啟用大標(biāo)題特性
UINavigationItemLargeTitleDisplayModeAlways,
/// Never
UINavigationItemLargeTitleDisplayModeNever,
}
Navigation 集成 UISearchController
把你的UISearchController賦值給navigationItem,就可以實(shí)現(xiàn)將UISearchController集成到Navigation。
navigationItem.searchController //iOS 11 新增屬性
navigationItem.hidesSearchBarWhenScrolling //決定滑動的時候是否隱藏搜索框;iOS 11 新增屬性
UINavigationController和滾動交互
滾動的時候,以下交互操作都是由UINavigationController負(fù)責(zé)調(diào)動的:
UISearchController搜索框效果更新
大標(biāo)題效果的控制
Rubber banding效果 //當(dāng)你開始往下拉,大標(biāo)題會變大來回應(yīng)那個滾輪
所以,如果你使用navigation bar,組裝一些整個push和pop體驗(yàn),你不會得到searchController的集成、大標(biāo)題的控制更新和Rubber banding效果,因?yàn)檫@些都是由UINavigationController控制的。
UIToolbar and UINavigationBar— Layout
在 iOS 11 中,當(dāng)蘋果進(jìn)行所有這些新特性時,也進(jìn)行了其他的優(yōu)化,針對 UIToolbar 和 UINavigaBar 做了新的自動布局?jǐn)U展支持,自定義的bar button items、自定義的title都可以通過layout來表示尺寸。
需要注意的是,你的constraints需要在view內(nèi)部設(shè)置,所以如果你有一個自定義的標(biāo)題視圖,你需要確保任何約束只依賴于標(biāo)題視圖及其任何子視圖。當(dāng)你使用自動布局,系統(tǒng)假設(shè)你知道你在做什么。
Avoiding Zero-Sized Custom Views
自定義視圖的size為0是因?yàn)槟阌幸恍┠:募s束布局。要避免視圖尺寸為0,可以從以下方面做:
UINavigationBar 和 UIToolbar 提供位置
開發(fā)者則必須提供視圖的size,有三種方式:
對寬度和高度的約束;
實(shí)現(xiàn) intrinsicContentSize;
通過約束關(guān)聯(lián)你的子視圖;
二. 管理margins 和 insets
layout margins
基于約束的Auto Layout,使我們搭建能夠動態(tài)響應(yīng)內(nèi)部和外部變化的用戶界面。Auto Layout為每一個view都定義了margin。margin指的是控件顯示內(nèi)容部分的邊緣和控件邊緣的距離。
可以用layoutMargins或者layoutMarginsGuide屬性獲得view的margin,margin是視圖內(nèi)部的一部分。layoutMargins允許獲取或者設(shè)置UIEdgeInsets結(jié)構(gòu)的margin。layoutMarginsGuide則獲取到只讀的UILayoutGuide對象。
在iOS11新增了一個屬性:directional layout margins,該屬性是NSDirectionalEdgeInsets結(jié)構(gòu)體類型的屬性:
typedef struct NSDirectionalEdgeInsets {
CGFloat top, leading, bottom, trailing;
} NSDirectionalEdgeInsets API_AVAILABLE(ios(11.0),tvos(11.0),watchos(4.0));
layoutMargins是UIEdgeInsets結(jié)構(gòu)體類型的屬性:
typedef struct UIEdgeInsets {
CGFloat top, left, bottom, right;
} UIEdgeInsets;
從上面兩種結(jié)構(gòu)體的對比可以看出,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.
例子:當(dāng)你設(shè)置了trailing = 30;當(dāng)在一個right to left 語言下trailing的值會被設(shè)置在view的左邊,可以通過layoutMargin的left屬性讀出該值。如下圖所示:
還有其他一些更新。自從引入layout margins,當(dāng)將一個view添加到viewController時,viewController會修復(fù)view的的layoutMargins為UIKit定義的一個值,這些調(diào)整對外是封閉的。從iOS11開始,這些不再是一個固定的值,它們實(shí)際是最小值,你可以改變view的layoutMargins為任意一個更大的值。而且,viewController新增了一個屬性:viewRespectsSystemMinimumLayoutMargins,如果你設(shè)置該屬性為"false",你就可以改變你的layoutMargins為任意你想設(shè)置的值,包括0,如下圖所示:
安全區(qū)域(Safe Area)
如下圖:照片應(yīng)用程序
從iOS 7以來,我們在整個操作系統(tǒng)中都有這些半透明的bars,蘋果鼓勵我們通過這些bars繪制內(nèi)容,我們是通過viewController 的edgesForExtendedLayout屬性來做這些的。
iOS 7 開始,在 UIViewController中引入的 topLayoutGuide 和 bottomLayoutGuide 在 iOS 11 中被廢棄了!取而代之的就是safeArea的概念,safeArea是描述你的視圖部分不被任何內(nèi)容遮擋的方法。 它提供兩種方式:safeAreaInsets或safeAreaLayoutGuide來提供給你safeArea的參照值,即 insets 或者 layout guide。 safeArea區(qū)域如圖所示:
如果有一個自定義的viewController,你可能要添加你自己的bars,增加safeAreaInsets的值,可以通過一個新的屬性:addtionalSafeAreaInsets來改變safeAreaInsets的值,當(dāng)你的viewController改變了它的safeAreaInsets值時,有兩種方式獲取到回調(diào):
UIView.safeAreaInsetsDidChange()
UIViewController.viewSafeAreaInsetsDidChange()
三. UIScrollView and UITableView的新特性
Scroll Views
如果有一些文本位于UI滾動視圖的內(nèi)部,并包含在導(dǎo)航控制器中,現(xiàn)在一般navigationContollers會傳入一個contentInset給其最頂層的viewController的scrollView,在iOS11中進(jìn)行了一個很大的改變,不再通過scrollView的contentInset屬性了,而是新增了一個屬性:adjustedContentInset,通過下面兩種圖的對比,能夠表示adjustContentInset表示的區(qū)域:
新增的contentInsetAdjustmentBehavior屬性用來配置adjustedContentInset的行為,該結(jié)構(gòu)體有以下幾種類型:
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中默認(rèn)啟用Self-Sizing
這個應(yīng)該是UITableView最大的改變。我們知道在iOS8引入Self-Sizing 之后,我們可以通過實(shí)現(xiàn)estimatedRowHeight相關(guān)的屬性來展示動態(tài)的內(nèi)容,實(shí)現(xiàn)了estimatedRowHeight屬性后,得到的初始contenSize是個估算值,是通過estimatedRowHeight x cell的個數(shù)得到的,并不是最終的contenSize,tableView不會一次性計(jì)算所有的cell的高度了,只會計(jì)算當(dāng)前屏幕能夠顯示的cell個數(shù)再加上幾個,滑動時,tableView不停地得到新的cell,更新自己的contenSize,在滑到最后的時候,會得到正確的contenSize。創(chuàng)建tableView到顯示出來的過程中,contentSize的計(jì)算過程如下圖:
Self-Sizing在iOS11下是默認(rèn)開啟的,Headers, footers, and cells都默認(rèn)開啟Self-Sizing,所有estimated 高度默認(rèn)值從iOS11之前的 0 改變?yōu)閁ITableViewAutomaticDimension:
@property (nonatomic) CGFloat estimatedRowHeight NS_AVAILABLE_IOS(7_0); // default is UITableViewAutomaticDimension, set to 0 to disable
如果目前項(xiàng)目中沒有使用estimateRowHeight屬性,在iOS11的環(huán)境下就要注意了,因?yàn)殚_啟Self-Sizing之后,tableView是使用estimateRowHeight屬性的,這樣就會造成contentSize和contentOffset值的變化,如果是有動畫是觀察這兩個屬性的變化進(jìn)行的,就會造成動畫的異常,因?yàn)樵诠浪阈懈邫C(jī)制下,contentSize的值是一點(diǎn)點(diǎn)地變化更新的,所有cell顯示完后才是最終的contentSize值。因?yàn)椴粫彺嬲_的行高,tableView reloadData的時候,會重新計(jì)算contentSize,就有可能會引起contentOffset的變化。iOS11下不想使用Self-Sizing的話,可以通過以下方式關(guān)閉:
self.tableView.estimatedRowHeight = 0;
self.tableView.estimatedSectionHeaderHeight = 0;
self.tableView.estimatedSectionFooterHeight = 0;
iOS11下,如果沒有設(shè)置estimateRowHeight的值,也沒有設(shè)置rowHeight的值,那contentSize計(jì)算初始值是 44 * cell的個數(shù),如下圖:
Table Views:separatorInset 擴(kuò)展
iOS 7 引入separatorInset屬性,用以設(shè)置 cell 的分割線邊距,在 iOS 11 中對其進(jìn)行了擴(kuò)展。可以通過新增的UITableViewSeparatorInsetReference枚舉類型的separatorInsetReference屬性來設(shè)置separatorInset屬性的參照值。
typedef NS_ENUM(NSInteger, UITableViewSeparatorInsetReference) {
UITableViewSeparatorInsetFromCellEdges, //默認(rèn)值,表示separatorInset是從cell的邊緣的偏移量
UITableViewSeparatorInsetFromAutomaticInsets //表示separatorInset屬性值是從一個insets的偏移量
}
下圖清晰的展示了這兩種參照值的區(qū)別:
Table Views 和 Safe Area
有以下幾點(diǎn)需要注意:
separatorInset 被自動地關(guān)聯(lián)到 safe area insets,因此,默認(rèn)情況下,表視圖的整個內(nèi)容避免了其根視圖控制器的安全區(qū)域的插入。
UITableviewCell 和 UITableViewHeaderFooterView的 content view 在安全區(qū)域內(nèi);因此你應(yīng)該始終在 content view 中使用add-subviews操作。
所有的 headers 和 footers 都應(yīng)該使用UITableViewHeaderFooterView,包括 table headers 和 footers、section headers 和 footers。
滑動操作(Swipe Actions)
在iOS8之后,蘋果官方增加了UITableVIew的右滑操作接口,即新增了一個代理方法(tableView: editActionsForRowAtIndexPath:)和一個類(UITableViewRowAction),代理方法返回的是一個數(shù)組,我們可以在這個代理方法中定義所需要的操作按鈕(刪除、置頂?shù)?,這些按鈕的類就是UITableViewRowAction。這個類只能定義按鈕的顯示文字、背景色、和按鈕事件。并且返回?cái)?shù)組的第一個元素在UITableViewCell的最右側(cè)顯示,最后一個元素在最左側(cè)顯示。從iOS 11開始有了一些改變,首先是可以給這些按鈕添加圖片了,然后是如果實(shí)現(xiàn)了以下兩個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類型的對象,創(chuàng)建該對象及賦值可看下面的代碼片段:
- ( 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;
}
創(chuàng)建UIContextualAction對象時,UIContextualActionStyle有兩種類型,如果是置頂、已讀等按鈕就使用UIContextualActionStyleNormal類型,delete操作按鈕可使用UIContextualActionStyleDestructive類型,當(dāng)使用該類型時,如果是右滑操作,一直向右滑動某個cell,會直接執(zhí)行刪除操作,不用再點(diǎn)擊刪除按鈕,這也是一個好玩的更新。
typedef NS_ENUM(NSInteger, UIContextualActionStyle) {
UIContextualActionStyleNormal,
UIContextualActionStyleDestructive
} NS_SWIFT_NAME(UIContextualAction.Style)
滑動操作這里還有一個需要注意的是,當(dāng)cell高度較小時,會只顯示image,不顯示title,當(dāng)cell高度夠大時,會同時顯示image和title。我寫demo測試的時候,因?yàn)槊總€cell的高度都較小,所以只顯示image,然后我增加cell的高度后,就可以同時顯示image和title了。見下圖對比:
總結(jié)
大概介紹了iOS 11的UI方面的一些更新,大部分內(nèi)容自己代碼測試過了,有些更新確實(shí)是很實(shí)用,可以適配下iOS 11,有的更新可能會給現(xiàn)有APP造成bug,所以學(xué)習(xí)下這些內(nèi)容還是很有必要的。
作者:sonialiu
鏈接:http://www.lxweimin.com/p/370d82ba3939
來源:簡書
著作權(quán)歸作者所有。商業(yè)轉(zhuǎn)載請聯(lián)系作者獲得授權(quán),非商業(yè)轉(zhuǎn)載請注明出處。