前言
很久之前寫過兩篇文章, 都是關於適配佈局的, 分別是iOS6與iOS7屏幕適配 edgesForExtendedLayout和影響屏幕適配的因素及tableview的ContentSize不正確的問題。當然也歡迎大家先看下這兩篇文章預熱一下, 因爲這篇文章其實是對上面這兩篇的不足補充和勘正。
今天重新總結一下關於UIScrollView及其子類的佈局的問題及影響因素, 只是本文是以TableView爲例的, 請大家注意。
UIScrollView佈局的影響因素
博主一共發現了9個UIScrollView佈局的影響因素。 我把它們歸爲兩個類型, 一類通過影響當前ViewController的View的frame從而影響佈局的; 另一類是通過影響scrollView的contentInset從而影響佈局的。接下來的每一個因素都會對ScrollView的佈局產生影響。
第一類: navigationBar.translucent
, vc.edgesForExtendedLayout
, vc.extendedLayoutIncludesOpaqueBars
。
第二類: scrollView.contentInset
, vc.automaticallyAdjustsScrollViewInsets
, navigationBar/statusBar的隱藏
, NavigationBar/TabBar的有無
, TabBar隱藏的方式
, 第一個addSubview的是不是當前的scrollView
。
iOS11
之後, API對於UIScrollView和其子類又做了一些調整, 所以第二類影響因素又分爲iOS10及之前版本的影響因素和iOS11及之後版本的影響因素。
而且從iOS11
開始, 除了之前的影響因素還多了兩個別的因素:
contentInsetAdjustmentBehavior
和 additionalSafeAreaInsets
。
一個默認條件下的例子
這裏我用
UITabBarViewController->UINavigationController->UIViewController->UITableView的結構來進行舉例。
- (void)viewDidLoad {
[super viewDidLoad];
// self.navigationController.navigationBar.translucent = NO;
// self.extendedLayoutIncludesOpaqueBars = YES;
// self.edgesForExtendedLayout = UIRectEdgeNone;
// self.navigationController.navigationBar.hidden = YES;
// self.automaticallyAdjustsScrollViewInsets = NO;
// [self createAnyView];
[self createTableView];
if (@available(iOS 11.0, *)) {
// _tableView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
// self.additionalSafeAreaInsets = UIEdgeInsetsMake(80.0f, 0.0f, 30.0f, 0.0f);
}
}
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
[self printRelatedParameters];
}
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
[self printRelatedParameters];
}
- (void)createTableView {
self.tableView = [[UITableView alloc]initWithFrame:CGRectMake(0, 0, kWidth, kHeight) style:UITableViewStylePlain];
_tableView.backgroundColor = [UIColor lightGrayColor];
_tableView.delegate = self;
_tableView.dataSource = self;
[self.view addSubview:_tableView];
[_tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:@"cell"];
UILabel *header = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, kWidth, 64)];
header.text = @"This is header";
header.textAlignment = NSTextAlignmentCenter;
header.backgroundColor = [UIColor cyanColor];
_tableView.tableHeaderView = header;
UILabel *footer = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, kWidth, 49)];
footer.text = @"This is footer";
footer.textAlignment = NSTextAlignmentCenter;
footer.backgroundColor = [UIColor purpleColor];
_tableView.tableFooterView = footer;
}
#pragma mark - UITableViewDelegate/UITableViewDataSource
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return 30;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell" forIndexPath:indexPath];
cell.textLabel.text = [NSString stringWithFormat:@"The Num of %ld", indexPath.row];
return cell;
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
return 40;
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
[self printRelatedParameters];
}
- (void)printRelatedParameters {
NSLog(@"ContentInset---%f_%f_%f_%f", _tableView.contentInset.top, _tableView.contentInset.left, _tableView.contentInset.bottom, _tableView.contentInset.right);
NSLog(@"ContentOffset---%f_%f", _tableView.contentOffset.x, _tableView.contentOffset.y);
NSLog(@"Self.View.Frame---%f_%f_%f_%f", self.view.frame.origin.x, self.view.frame.origin.y, self.view.frame.size.width, self.view.frame.size.height);
if (@available(iOS 11.0, *)) {
NSLog(@"safeAreaInsets---%f_%f_%f_%f", _tableView.safeAreaInsets.top, _tableView.safeAreaInsets.left, _tableView.safeAreaInsets.bottom, _tableView.safeAreaInsets.right);
NSLog(@"adjustedContentInset---%f_%f_%f_%f", _tableView.adjustedContentInset.top, _tableView.adjustedContentInset.left, _tableView.adjustedContentInset.bottom, _tableView.adjustedContentInset.right);
NSLog(@"Self.additionalSafeAreaInsets---%f_%f_%f_%f", self.additionalSafeAreaInsets.top, self.additionalSafeAreaInsets.left, self.additionalSafeAreaInsets.bottom, self.additionalSafeAreaInsets.right);
}
NSLog(@"\n\n");
}
效果是這樣的:
第一類影響因素
navigationBar.translucent
@property(nonatomic,assign,getter=isTranslucent) BOOL translucent NS_AVAILABLE_IOS(3_0) UI_APPEARANCE_SELECTOR; // Default is NO on iOS 6 and earlier. Always YES if barStyle is set to UIBarStyleBlackTranslucent
導航欄透明狀態影響佈局, iOS6及之前默認爲不透明, 之後就默認爲透明的了. 這也是爲什麼 iOS6和 iOS7在添加視圖時, frame 不一致需要適配的區別之處.
這個屬性影響的是當前VC.View.frame
, 在默認的佈局下, translucent
爲YES, 即導航欄爲半透明, 此時self.View.Frame
的值爲0.000000_0.000000_375.000000_667.000000.
self.navigationController.navigationBar.translucent = NO;
當我們把此屬性改爲NO後, self.View.Frame
的值爲0.000000_64.000000_375.000000_603.000000.
默認屬性下, 向上滑動tableView, 淺藍色的header是可以透過NavigationBar的, 修改translucent
屬性爲NO後, 正是因爲VC的view佈局不在navigationBar底下了, 所以這是看不到淺藍色的header的。
edgesForExtendedLayout
@property(nonatomic,assign) BOOL extendedLayoutIncludesOpaqueBars NS_AVAILABLE_IOS(7_0); // Defaults to NO, but bars are translucent by default on 7_0.
This property is applied only to view controllers that are embedded in a container such as UINavigationController. The window’s root view controller does not react to this property. The default value of this property is UIRectEdgeAll.
這個枚舉如下:
typedef NS_OPTIONS(NSUInteger, UIRectEdge) {
UIRectEdgeNone = 0,
UIRectEdgeTop = 1 << 0,
UIRectEdgeLeft = 1 << 1,
UIRectEdgeBottom = 1 << 2,
UIRectEdgeRight = 1 << 3,
UIRectEdgeAll = UIRectEdgeTop | UIRectEdgeLeft | UIRectEdgeBottom | UIRectEdgeRight
} NS_ENUM_AVAILABLE_IOS(7_0);
我這裏的控制器是既有TabBar又有NavigationBar的, 所以, 在默認值UIRectEdgeAll
下, 當前控制器就做了上面和下面的延展布局。Self.View.Frame
的值爲0.000000_0.000000_375.000000_667.000000。
self.edgesForExtendedLayout = UIRectEdgeNone;
當我將此屬性改變爲UIRectEdgeNone
時, 上面和下面的延展全部會禁止。Self.View.Frame
的值爲0.000000_64.000000_375.000000_554.000000, 這說明view從導航欄開始佈局, 而且高度爲 667-64-49。
這裏footer顯示不全的原因就是VC的View的高度比tableView的高度小。
extendedLayoutIncludesOpaqueBars
@property(nonatomic,assign) BOOL extendedLayoutIncludesOpaqueBars NS_AVAILABLE_IOS(7_0); // Defaults to NO, but bars are translucent by default on 7_0.
A Boolean value indicating whether or not the extended layout includes opaque bars.
The default value of this property is NO.
這個屬性是指是否在不透明導航欄情況下延展布局。
首先要爲默認條件添加一行代碼:
self.navigationController.navigationBar.translucent = NO;
在導航欄不透明, extendedLayoutIncludesOpaqueBars
又爲默認值NO的情況下, 當然這個效果就是我們所說的第一條中的圖片。self.View.Frame
的值爲0.000000_64.000000_375.000000_603.000000.
當我們將此屬性改爲YES:
self.extendedLayoutIncludesOpaqueBars = YES;
此時self.View.Frame
的值爲0.000000_0.000000_375.000000_667.000000.
第二類影響因素(iOS10及之前版本)
scrollView.contentInset
@property(nonatomic) UIEdgeInsets contentInset; // default UIEdgeInsetsZero. add additional scroll area around content
The distance that the content view is inset from the enclosing scroll view.
Use this property to add to the scrolling area around the content. The unit of size is points. The default value is UIEdgeInsetsZero.
關於ScrollView的這個屬性, 初始值爲0, 所有對contentInset
值的自動調整都是在viewWillAppear
和viewDidAppear
之間進行調整的。默認條件下, viewWillAppear
中打印ContentInset—0.000000_0.000000_0.000000_0.000000, viewDidAppear
中打印ContentInset—64.000000_0.000000_49.000000_0.000000。
!!!特別注意!!!
正常情況下想要設置contentInset
的值需要在viewDidLoad
中進行. 需要注意的是, 設置contentInset
值並不是覆蓋了原值, 而是與原值進行了各項相加的處理。
比如, 我在創建tableView的時候, 添加一行代碼:
_tableView.contentInset = UIEdgeInsetsMake(-64, 0, 0, 0);
默認值top應該是64的, 設置新值後並沒有覆蓋, 而應該是-64+64=0;
這時, 打印的結果爲: viewWillAppear
中打印ContentInset—-64.000000_0.000000_0.000000_0.000000, viewDidAppear
中打印ContentInset–ContentInset—0.000000_0.000000_49.000000_0.000000。
automaticallyAdjustsScrollViewInsets
@property(nonatomic,assign) BOOL automaticallyAdjustsScrollViewInsets API_DEPRECATED(“Use UIScrollView’s contentInsetAdjustmentBehavior instead”, ios(7.0,11.0),tvos(7.0,11.0)); // Defaults to YES
A Boolean value that indicates whether the view controller should automatically adjust its scroll view insets.
The default value of this property is YES, which lets container view controllers know that they should adjust the scroll view insets of this view controller’s view to account for screen areas consumed by a status bar, search bar, navigation bar, toolbar, or tab bar. Set this property to NO if your view controller implementation manages its own scroll view inset adjustments.
這個屬性爲當前控制器是否要自動調整contentInset
, 默認爲YES。
而且這個屬性在iOS7.0
開始啓用, iOS11
開始廢棄, 被UIScrollView
的 contentInsetAdjustmentBehavior
屬性所替代(它的具體介紹請看下文)。
默認條件下, viewWillAppear
中打印ContentInset—0.000000_0.000000_0.000000_0.000000, viewDidAppear
中打印ContentInset—64.000000_0.000000_49.000000_0.000000。
當修改其值爲NO後,
self.automaticallyAdjustsScrollViewInsets = NO;
修改後控制器不會根據系統的默認情況自動調整contentInset
了, 所以在viewWillAppear
中打印ContentInset—0.000000_0.000000_0.000000_0.000000, viewDidAppear
中打印ContentInset—0.000000_0.000000_0.000000_0.000000。
NavigationBar/TabBar 的有無
就如上面說過的, NavigationBar和TabBar的存在纔是以上那些影響因素的前提。如果沒有它們這些屬性是無效果的, 只有它們的存在才能通過這些屬性影響ScrollView的佈局。沒有導航欄和TabBar的情況是這樣的:
NavigationBar/StatusBar 的隱藏
默認情況下NavigationBar和StatusBar都是不隱藏的, 當我們設置NavigationBar的hide爲YES後, 會有什麼顯示效果?
NavigationBar和StatusBar都隱藏後顯示效果呢?
如圖, 我們看到的, NavigationBar隱藏後, 在默認自動調整contentInset
的情況下, 其top的值爲20。所以, 想要做透明導航欄又不隱藏狀態欄的需求的, 需要注意下這裏。 當然使用上面的設置scrollView.contentInset的方法可以解決此問題。
TabBar 隱藏的方式
一般的App在每個Tab首頁的VC纔會展示出TabBar, 而在第二層頁面開始就要隱藏TabBar了。但是TabBar不同的隱藏方式, 會產生某些影響, 從而影響scrollView.contentInset。
關於TabBar的隱藏方式有興趣的可以瞭解我的另一篇文章iOS各種 bar 隱藏的方法。
比如說我們使用的是系統的TabBar, 通過hidesBottomBarWhenPushed
方法進行隱藏TabBar和tabBar.hidden
兩種方式來對比。
- (BOOL) hidesBottomBarWhenPushed {
return YES;
}
此方法下, viewWillAppear
中打印ContentInset—0.000000_0.000000_0.000000_0.000000, viewDidAppear
中打印ContentInset—64.000000_0.000000_0.000000_0.000000。
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
[self.tabBarController.tabBar setHidden:YES];
}
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
[self.tabBarController.tabBar setHidden:YES];
}
此方法下, viewWillAppear
中打印ContentInset—0.000000_0.000000_0.000000_0.000000, viewDidAppear
中打印ContentInset—64.000000_0.000000_49.000000_0.000000。
兩種方法的區別在於, 執行tabBar隱藏的時機不同, hidesBottomBarWhenPushed
方法在viewDidLoad
之前執行, 而tabBar.hidden
是在viewWillAppear
中執行的。正是因爲此, 造成ScrollView.contentInset
的不同, 因爲hidesBottomBarWhenPushed
方法在佈局配置前就隱藏了tabBar了, 所以不會有內嵌的影響了。
雖與本主題無關, 但還是推薦大家使用hidesBottomBarWhenPushed`方法。
第一個addSubview的是不是當前的scrollView
當前控制器中, self.view
上第一個addSubview
的如果是ScrollView, 則以上的所有因素纔可以影響其佈局。如果不是第一個添加的, 則以上所有的屬性影響失效。
當我們在viewDidLoad
中第一個添加一個任何的View
- (void)createAnyView {
UIView *firstView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 100, 200)];
firstView.backgroundColor = [UIColor redColor];
[self.view addSubview:firstView];
}
代碼添加的效果如下:
而且還有一個佈局方面的問題, 在默認情況下只要是第一個添加的ScrollView, 無論ScrollView.frame
爲多少, 它的contentInset
是會一直存在的。
_tableView.frame = CGRectMake(0, 100, kWidth, kHeight - 200);
效果如下:
第二類影響因素(iOS11及之後版本)
iOS11
開始, 關於影響UIScrollView的佈局的API有了一些調整, 棄用了一些之前版本的屬性, 又加入了一些新的屬性。比如automaticallyAdjustsScrollViewInsets
在iOS11開始被棄用, 取而代之的是contentInsetAdjustmentBehavior
屬性。我們先了解下iOS11
之後新添加的一些概念和屬性。
但最大的區別是, iOS11
引入一個全新的概念 safeAreaLayoutGuide
, 它改變了整個iOS的佈局機制, 當然也影響了ScrollView的佈局。還有相應的一系列的屬性, adjustedContentInset
, additionalSafeAreaInsets
, contentInsetAdjustmentBehavior
, 它們與UIScrollView的佈局相關。
默認的示例代碼在iOS11
中, 的顯示結果如下:
打印結果如下:
safeAreaLayoutGuide 和 safeArea
@property(nonatomic,readonly,strong) UILayoutGuide *safeAreaLayoutGuide API_AVAILABLE( ios(11.0), tvos(11.0));
指可以與自動佈局交互的矩形區域。使用佈局指南來替換您可能創建的虛擬視圖來表示視圖間空間或封裝在用戶界面中。傳統上,有一些自動佈局技術需要虛擬視圖。虛擬視圖是一個空視圖,它沒有任何自己的可視元素,僅用於在視圖層次結構中定義一個矩形區域。
這個屬性是UIView的一個只讀屬性,意味着所有UIView對象都有並且是系統自動定義好的。繼承自UILayoutGuide,它有layoutFrame
屬性意味着它能代表一塊區域, 代表的這塊區域就是safeArea
。它反映了UIView對象避開Navigation bar, tab bar,tool bar以及隱藏視圖控制器視圖的父View中覆蓋的區域, 從而形成的一個安全區域。safeAreaLayoutGuide
是一個相對抽象的概念, 但我們可以理解爲它的layoutFrame
屬性, 即一個矩形區域可以稱爲safeArea
。
以iPhone X爲例, safeArea區域:
iOS11中 ContentInset 和 safeAreaInsets
其實在iOS11
開始, 第二類影響因素(除第一個addSubview的是不是當前的scrollView
外)依然會影響到UIScrollView的佈局。而且影響的效果也與iOS10及之前一致, 那麼區別是什麼?
看一段打印結果你就明白了, 默認舉例的代碼中viewDidAppear
中打印結果對比。在iOS10及之前版本中:
iOS11中:
根據上面的打印結果, 可以得出: iOS10及之前版本
中所有第二類影響因素最終都會以系統自動調整UIScrollView的contentInset
屬性的值來對其佈局產生影響。而iOS11
開始所有的自動調整都會通過safeAreaInsets
中的值來影響佈局。所以我們會看到無論我們如何設置第二類影響因素, UIScrollView的contentInset
默認情況下都會是0.000000_0.000000_0.000000_0.000000。反而, 這些值, 會出現在safeArea
中64.000000_0.000000_49.000000_0.000000。
總結一下就是, 在iOS11
中, 第二類影響因素同樣會影響UIScrollView的佈局, 只是在iOS10
自動調整的值有contentInset
改爲了 safeAreaInsets
。但有一個因素在iOS11
將不受影響, 就是第一個addSubview的是不是當前的scrollView
這個因素。
contentInsetAdjustmentBehavior
iOS11
開始, Controller的屬automaticallyAdjustsScrollViewInsets
被棄用, 新加入了ScrollView的屬性 contentInsetAdjustmentBehavior
與其有相同的功能。
@property(nonatomic) UIScrollViewContentInsetAdjustmentBehavior contentInsetAdjustmentBehavior API_AVAILABLE( ios(11.0), tvos(11.0));
typedef NS_ENUM(NSInteger, UIScrollViewContentInsetAdjustmentBehavior) {
UIScrollViewContentInsetAdjustmentAutomatic,
UIScrollViewContentInsetAdjustmentScrollableAxes,
UIScrollViewContentInsetAdjustmentNever,
UIScrollViewContentInsetAdjustmentAlways,
} API_AVAILABLE(ios(11.0),tvos(11.0));
ScrollView的屬性 contentInsetAdjustmentBehavior
分爲四個枚舉, 分別是Automatic, ScrollableAxes, Never, Always。有些疑惑的可能是前面兩個枚舉, 那我做個解析。
先說ScrollableAxes, 是在可滑動方向上根據安全區域來自動調整contentInset, 不可滑動的方向不會自動調整contentInset。 就像API中描述的 (contentSize.width/height > frame.size.width/height or alwaysBounceHorizontal/Vertical = YES), 可滑動時或者強制設置爲橫向/縱向可回彈時, 纔會自動調整contentInset。
再來說Automatic, 默認枚舉, 與ScrollableAxes類似, 但它會根據你的佈局來判斷是否需要自動調整contentInset。在NavigationBar, TabBar, StatusBar 等情況下, 一定會自動調整contentInset, 只有在特殊情況它也不會做自動調整的。 比如, 下圖綠色箭頭處自動調整了, 但左側的iphoneX的齊劉海沒有自動調整:
這裏參考了contentInsetAdjustmentBehavior各個值之間的區別 這篇文章, 具體情況, 裏面也有代碼舉例。
在扯回我們的主題中, 當在示例代碼中添加這樣的代碼
_tableView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
打印的結果如下:
adjustedContentInset
@property(nonatomic, readonly) UIEdgeInsets adjustedContentInset API_AVAILABLE( ios(11.0), tvos(11.0));
這是UIScrollView的屬性, 且爲只讀屬性, 當contentInsetAdjustmentBehavior允許時,他是safeAreaInsets
和adjustedContentInset
的和。
如果當我們調整tablevew的contentInset時,
_tableView.contentInset = UIEdgeInsetsMake(100.0f, 0.0f, 50.0f, 0.0f);
顯示結果如下:
打印結果如下:
additionalSafeAreaInsets
@property(nonatomic) UIEdgeInsets additionalSafeAreaInsets API_AVAILABLE( ios(11.0), tvos(11.0));
這個屬性是UIViewController的屬性, UIViewController的子類可以使用這個屬性來調整UIViewController的safeAreaInsets。我們可以使用此屬性來擴展安全區域,以在界面中包含自定義內容。 例如,繪圖應用程序可能會使用此屬性來避免在工具選項板下顯示內容。
顯示結果如下:
打印結果如下:
此文章僅僅是拋磚引玉, 若有問題或者新的因素的話, 麻煩在我的博客留言, 謝謝。
相關代碼下載地址:
Scrollview/TableView佈局的影響因素
相關資料:
iOS6與iOS7屏幕適配 edgesForExtendedLayout
影響屏幕適配的因素及tableview的ContentSize不正確的問題
iOS各種 bar 隱藏的方法