一、Safe Area
從 iOS 7 開始,我們就在操作系統(tǒng)里提供這樣的半透明的欄,并且鼓勵(lì)你把要顯示的內(nèi)容布局延伸過(guò)這些欄,就像下圖中照片 App 中做的那樣。(注意頂部和底部都帶有 Bar,且內(nèi)容都被 Bar 所覆蓋,產(chǎn)生出模糊效果)

之所以又這樣的效果是利用了 UIViewController 的屬性 edgesForExtendedLayout,它可以讓作為 Container 的ViewControllers 定義這些(translucent bars)欄下View 的大小
| edgesForExtendedLayout控制 View 的大小 | 讓 translucent bars 覆蓋其上之后帶有模糊效果 |
|---|---|
![]() |
![]() |
默認(rèn)情況下edgesForExtendedLayout 適用于所有的邊緣,你可以通過(guò)topLayoutGuide 和 bottomLayoutGuide 兩個(gè)屬性來(lái)定義懸浮欄的大小。

從 iOS 11開始,系統(tǒng)將取消topLayoutGuide 和 bottomLayoutGuide屬性,引入新的布局結(jié)構(gòu)概念,SafeArea。
| 取消topLayoutGuide 和 bottomLayoutGuide屬性 | 新的布局結(jié)構(gòu)概念,SafeArea。 |
|---|---|
![]() |
![]() |
safeArea是描述你的視圖部分不被任何內(nèi)容遮擋的方法。 它提供兩種方式:safeAreaInsets 或 safeAreaLayoutGuide 來(lái)提供給你 safeArea 的參照值,這兩個(gè)屬性定義在 UIView 中,它們分別對(duì)應(yīng) insets 或者 layout guide類型。
例如在你自定義的 ViewController 中添加一些自定義的欄樣式 View,此時(shí)就需要改變 safeAreaInsets 的值。要想增加或減少safeAreaInsets的值,你可以通過(guò)調(diào)用 UIViewController 的新屬性 additionalSafeAreaInsets (UIEdgeInsets 類型)在對(duì)應(yīng)的位置增加 inset 值進(jìn)而改變 safeAreaInsets。當(dāng)你的viewController改變了它的safeAreaInsets值時(shí),有兩種方式獲取到回調(diào):
UIView.safeAreaInsetsDidChange()
UIViewController.viewSafeAreaInsetsDidChange()
每個(gè) view 都可以改變 safeAreaInsets 的值,包括 UIViewController。

二、Scroll Views
下面例子中的結(jié)構(gòu)是 UIVIewController + UIScrollView 包在
UINavigationController 里面。

以前如果一個(gè) VIewController 中含有 ScrollView的話, 被
NavigationController 包住的這個(gè) ViewController 會(huì)自動(dòng)地調(diào)整 ScrollView 的 contentInset 值(增加64)如下

iOS 11之后這個(gè)行為已取消,取而代之的是,使用一個(gè)新的屬性adjustedContentInset代替。而 contentInset 這個(gè)屬性代表的概念簡(jiǎn)單明了,單單是內(nèi)容的區(qū)域的 inset,不再與外界布局有關(guān)。

UIScrollView 支持自動(dòng)布局,讓scrollView可以根據(jù)所添加的sub-view的大小自動(dòng)處理其可滾動(dòng)區(qū)域的大小。iOS 11下更是添加了一些新的屬性來(lái)協(xié)助開發(fā)中更快速的布局,其中包括 frameLayoutGuide 和 contentLayoutGuide 以及 contentInsetAdjustmentBehavior。
- frameLayoutGuide 負(fù)責(zé)scrollView在屏幕中的大小和位置,也就是你可以約束 scrollView 中的 sub-view 如下圖中的 Page 1 labelView。當(dāng)你滾動(dòng)時(shí),該 page 1 labelview 是固定不動(dòng)的。
| 約束 scrollView 中的 sub-view Page 1 labelView | 當(dāng)內(nèi)容滾動(dòng)后,Page 1 位置不變 |
|---|---|
![]() |
![]() |
- contentLayoutGuide,你可以約束 sub-view 來(lái)控制器 scrollView 中可滾動(dòng)區(qū)域的大小或者讓內(nèi)容隨著滾動(dòng)而移動(dòng)。
| 指定 contentLayoutGuide | 發(fā)生滾動(dòng)時(shí) |
|---|---|
![]() |
![]() |
- contentInsetAdjustmentBehavior屬性用來(lái)配置adjustedContentInset的行為,該結(jié)構(gòu)體有以下幾種類型:
typedef NS_ENUM(NSInteger, UIScrollViewContentInsetAdjustmentBehavior) {
UIScrollViewContentInsetAdjustmentAutomatic, // Similar to .scrollableAxes, but for backward compatibility will also adjust the top & bottom contentInset when the scroll view is owned by a view controller with automaticallyAdjustsScrollViewInsets = YES inside a navigation controller, regardless of whether the scroll view is scrollable
UIScrollViewContentInsetAdjustmentScrollableAxes, // Edges for scrollable axes are adjusted (i.e., contentSize.width/height > frame.size.width/height or alwaysBounceHorizontal/Vertical = YES)
UIScrollViewContentInsetAdjustmentNever, // contentInset is not adjusted
UIScrollViewContentInsetAdjustmentAlways, // contentInset is always adjusted by the scroll view's safeAreaInsets
} API_AVAILABLE(ios(11.0),tvos(11.0));
/* Configure the behavior of adjustedContentInset.
Default is UIScrollViewContentInsetAdjustmentAutomatic.
*/
@property(nonatomic) UIScrollViewContentInsetAdjustmentBehavior contentInsetAdjustmentBehavior API_AVAILABLE(ios(11.0),tvos(11.0));
/* When contentInsetAdjustmentBehavior allows, UIScrollView may incorporate
its safeAreaInsets into the adjustedContentInset.
*/
@property(nonatomic, readonly) UIEdgeInsets adjustedContentInset API_AVAILABLE(ios(11.0),tvos(11.0));
當(dāng)adjustedContentInset 值被改變后回調(diào)的代理方法有:
/* Also see -[UIScrollView adjustedContentInsetDidChange]
*/
- (void)scrollViewDidChangeAdjustedContentInset:(UIScrollView *)scrollView API_AVAILABLE(ios(11.0), tvos(11.0));
三、Table Views
- 我們知道在iOS 8引入Self-Sizing 之后,可以通過(guò)實(shí)現(xiàn)estimatedRowHeight 相關(guān)的屬性來(lái)展示動(dòng)態(tài)的內(nèi)容,當(dāng)實(shí)現(xiàn)了estimatedRowHeight 屬性后,tableview 會(huì)得到的初始 contenSize ,這是一個(gè)估算值,是通過(guò)estimatedRowHeight * cell的個(gè)數(shù)得到的,并不是最終的 contenSize。
因?yàn)橛泄浪愕?contentSize 值,所以tableView就不會(huì)一次性計(jì)算所有的cell的高度了,只會(huì)計(jì)算當(dāng)前屏幕能夠顯示的cell個(gè)數(shù)再加上幾個(gè)。
滑動(dòng)時(shí),tableView 不停地得到新的 cell,更新自己的 contenSize,在滑到最后的時(shí)候,會(huì)得到正確的 contenSize 。在測(cè)試Demo中,創(chuàng)建tableView 到顯示出來(lái)的過(guò)程中,contentSize 的計(jì)算過(guò)程如下圖:
- Self-Sizing
在iOS 11中默認(rèn)啟用 Self-Sizing, 也就是說(shuō)你 cell、header、footer對(duì)應(yīng)的 estimated heights 默認(rèn)值都從 iOS 11 之前的0 變?yōu)閁ITableViewAutomaticDimension。
因?yàn)槟J(rèn)開啟了 Self-Sizing,你在布局 cell 時(shí)需要確保內(nèi)部子控件具備完整約束來(lái)讓 tableview 自動(dòng)計(jì)算出其需要的大小或者你在對(duì)應(yīng)的 delegate 方法中返回每一個(gè) cell 的真實(shí)高度值。同理也需要處理對(duì)應(yīng) header 和 footer 問(wèn)題。
如果目前項(xiàng)目中沒(méi)有使用estimateRowHeight屬性,在iOS11的環(huán)境下就要注意了,因?yàn)殚_啟Self-Sizing之后,tableView是使用estimateRowHeight屬性的,這樣就會(huì)造成contentSize和contentOffset值的變化,如果是有動(dòng)畫是觀察這兩個(gè)屬性的變化進(jìn)行的,就會(huì)造成動(dòng)畫的異常,因?yàn)樵诠浪阈懈邫C(jī)制下,contentSize的值是一點(diǎn)點(diǎn)地變化更新的,所有cell顯示完后才是最終的contentSize值。因?yàn)椴粫?huì)緩存正確的行高,tableView reloadData的時(shí)候,會(huì)重新計(jì)算contentSize,就有可能會(huì)引起contentOffset的變化。
如果你想 link 到 iOS 11 而不想使用這個(gè)默認(rèn)開啟的新特性(Self-Sizing)的話,你可以取消它,代碼如下:
override func viewDidLoad() {
//取消 estimated sizes 功能和 tableview 的 Self-Sizing 功能
tableView.estimatedRowHeight = 0
tableView.estimatedSectionHeaderHeight = 0
tableView.estimatedSectionFooterHeight = 0
}
iOS11下,如果沒(méi)有設(shè)置estimateRowHeight的值,也沒(méi)有設(shè)置rowHeight的值,那contentSize計(jì)算初始值是 44 * cell的個(gè)數(shù),如下圖:rowHeight和estimateRowHeight都是默認(rèn)值UITableViewAutomaticDimension 而rowNum = 15;則初始contentSize = 44 * 15 = 660;

- separatorInset
tableView 的 readable content guide 概念,它是 View 內(nèi)的一部分,也是內(nèi)容布局的推薦區(qū)域。即使在大屏幕的 iPad 下,在 readable content guide 內(nèi)布局的內(nèi)容都能夠獲得不錯(cuò)的用戶閱讀體驗(yàn)。

默認(rèn)情況下 tableview 在 readable content guide 內(nèi)有一個(gè) separatorInset,它可以影響 cell 的默認(rèn)分隔線位置 和 在 cell 內(nèi) labels 的位置。
| separator.left = 0 | separator.left = 30 |
|---|---|
![]() |
![]() |
可見(jiàn) separatorInset 是對(duì) readable content view 的 inset 處理。
iOS 11 之后,separatorInset 值影響的是,tableview 邊框與屏幕的邊緣的間隔大小,當(dāng)設(shè)置左右為0時(shí),效果如下

如下是 separatorInset 值使用對(duì)別,其中在 iOS 11后添加可設(shè)置參照的屬性UITableViewSeparatorInsetReference
typedef NS_ENUM(NSInteger, UITableViewSeparatorInsetReference) {
UITableViewSeparatorInsetFromCellEdges, //默認(rèn)值,表示separatorInset是從cell的邊緣的偏移量
UITableViewSeparatorInsetFromAutomaticInsets //表示separatorInset屬性值是從一個(gè)insets的偏移量
}
對(duì)比使用如下:
| UITableViewSeparatorInsetFromCellEdges | UITableViewSeparatorInsetFromAutomaticInsets |
|---|---|
![]() |
![]() |
- tableview 與 Safe Area 交互需要注意幾點(diǎn):
- separatorInset 被自動(dòng)地關(guān)聯(lián)到 safe area insets,因此,默認(rèn)情況下,tabelview的整個(gè)內(nèi)容區(qū)域避免了ViewController安全區(qū)域的插入。
- UITableviewCell 和 UITableViewHeaderFooterView的 content view 在安全區(qū)域內(nèi);因此你應(yīng)該始終在 content view 中使用add-subviews操作
- 你應(yīng)該使用帶有 content view 的 UITableViewHeaderFooterView類實(shí)例作為table headers 和 footers、section headers 和 footers。
- Swipe Actions
- 新的滾動(dòng)條:帶有 time stamps 時(shí)碼的滾動(dòng)條
- 實(shí)現(xiàn) full swipe-to-delete 功能
- 添加了又滑功能
測(cè)試默認(rèn)開啟Self-Sizing的 iOS 11問(wèn)題。
問(wèn)題1:如下代碼是運(yùn)行在 iOS 10下正常,但運(yùn)行在 iOS 11則在 tabelView 上下有留白問(wèn)題
//
// ViewController.m
// ios10TabelView
//
// Created by Jacob_Liang on 2017/9/21.
// Copyright ? 2017年 Jacob. All rights reserved.
//
#import "ViewController.h"
static NSString * const CELLID = @"CELLID";
@interface ViewController ()<UITableViewDelegate, UITableViewDataSource>
@property (nonatomic, weak) UITableView *tableView;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
[self setUpInit];
[self setUpNav];
[self setUpTableView];
}
- (void)setUpInit {
self.automaticallyAdjustsScrollViewInsets = NO; //iOS 11下被廢棄了,寫了也沒(méi)用
self.view.backgroundColor = [UIColor purpleColor];
}
- (void)setUpNav {
self.navigationItem.title = @"出席統(tǒng)計(jì)";
}
- (void)setUpTableView {
CGFloat screenW = [UIScreen mainScreen].bounds.size.width;
CGFloat screenH = [UIScreen mainScreen].bounds.size.height;
UITableView *tableView = [[UITableView alloc] initWithFrame:CGRectMake(0, 64, screenW, screenH - 64) style:UITableViewStyleGrouped];
[self.view addSubview:tableView];
_tableView = tableView;
tableView.backgroundColor = [UIColor lightGrayColor];
tableView.delegate = self;
tableView.dataSource = self;
[tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:CELLID];
}
#pragma mark - UITableViewDelegate & UITableViewDataSource
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return 15;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CELLID forIndexPath:indexPath];
cell.textLabel.text = [NSString stringWithFormat:@"%@",indexPath];
return cell;
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
return 50;
}
- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section {
return 0.01;
}
- (CGFloat)tableView:(UITableView *)tableView heightForFooterInSection:(NSInteger)section {
return 0.01;
}
@end
上述代碼運(yùn)行情況,注意此時(shí) tableview 的 Style 為 UITableViewStyleGrouped
| 在 iOS 10 下 | iOS 11下 | iOS 11下 |
|---|---|---|
| self.automaticallyAdjustsScrollViewInsets = NO 有效 | self.automaticallyAdjustsScrollViewInsets = NO 無(wú)效 | self.automaticallyAdjustsScrollViewInsets = NO 無(wú)效 |
| 沒(méi)有調(diào)用viewForFooterInSection和viewForHeaderInSection運(yùn)行正常 | 沒(méi)有調(diào)用viewForFooterInSection和viewForHeaderInSection運(yùn)行有留白 | 調(diào)用viewForFooterInSection和viewForHeaderInSection運(yùn)行正常 |
![]() iOS10NOReturnViewFooterHeader.gif
|
![]() NOReturnHeaderOrFooterViewQuestion.gif
|
![]() wihtReturnHeaderOrFooterView.gif
|
另一宗辦法就是,關(guān)閉 iOS 11默認(rèn)打開的 Self-Sizing 功能
tableView.estimatedRowHeight = 0;
tableView.estimatedSectionFooterHeight = 0;
tableView.estimatedSectionHeaderHeight = 0;















