內(nèi)容均為原創(chuàng), 如有任何疑問(wèn)或者錯(cuò)誤合瓢,請(qǐng)?jiān)谖恼孪铝粞曰蛘咧苯优c我聯(lián)系坦胶,一定及時(shí)回復(fù): )
本文要討論的問(wèn)題
- 是什么影響了導(dǎo)航控制器的子控制器view布局變化?
- safeAreaInsets介紹以及如何利用它適配View布局晴楔。
- 適配全面屏的新思路
- safeAreaInsets是如何影響著scrollView及其子類(lèi)(tableView)
為了更好的說(shuō)明問(wèn)題顿苇,本文的代碼均在iphoneX模擬器中運(yùn)行。
1.是什么影響了導(dǎo)航控制器的子控制器view布局變化税弃?
當(dāng)一個(gè)viewController在一個(gè)導(dǎo)航控制器中顯示纪岁,有時(shí)候布局會(huì)被導(dǎo)航欄遮擋,控件的Y值需要從64(home鍵型號(hào)手機(jī))或者88(全面屏手機(jī))開(kāi)始布局, 而有時(shí)候控件的Y值從0開(kāi)始布局,也不會(huì)被導(dǎo)航欄遮擋吏夯。如果你對(duì)此也有疑惑,那么這篇文章或許可以幫到你溺拱。
我們新建一個(gè)UINavigationController將其作為keyWindow的rootViewController,并且里面包含了一個(gè)UIViewController,viewController中,我們添加一個(gè)y值為0開(kāi)始布局的testView贡定。
UIView *testV = [[UIView alloc] initWithFrame:CGRectMake(0, 0 ,[UIScreen mainScreen].bounds.size.width , 88)];
testV.backgroundColor = [UIColor redColor];
[self.view addSubview:testV];
會(huì)出現(xiàn)兩種情況如圖:
同樣的布局,是什么導(dǎo)致了testView呈現(xiàn)形式不一樣呢可都?
· 答案是navigationBar
。
iOS的導(dǎo)航控制器會(huì)根據(jù)導(dǎo)航欄是否會(huì)遮擋子控制器的view來(lái)決定是否需要將其挪到安全區(qū)域
顯示蚓耽,什么是安全區(qū)域渠牲,我下面會(huì)講。
細(xì)心的同學(xué)們發(fā)現(xiàn)圖1的導(dǎo)航欄是透明
的步悠,那么既然是透明的签杈,導(dǎo)航控制器會(huì)認(rèn)為其是可視的,導(dǎo)航控制器中子控制器view會(huì)從狀態(tài)欄頂部
開(kāi)始布局鼎兽,注意4鹄选!谚咬!這里是狀態(tài)欄頂部而不是導(dǎo)航欄頂部p懈丁!
圖2的導(dǎo)航欄為不透明
的择卦,所以導(dǎo)航控制器會(huì)認(rèn)為其為不可視的敲长,導(dǎo)航控制器中子控制器view從導(dǎo)航欄底部
開(kāi)始布局郎嫁。
· 什么時(shí)候系統(tǒng)會(huì)認(rèn)為導(dǎo)航欄遮住了視圖,需要將子控制器的view挪到安全區(qū)域展示呢祈噪?
以下兩種情況泽铛,只要滿(mǎn)足一個(gè),導(dǎo)航控制器中子控制器view就會(huì)從導(dǎo)航欄底部
開(kāi)始布局辑鲤。
// 將導(dǎo)航欄設(shè)置為不透明
self.navigationController.navigationBar.translucent = NO;
// 為導(dǎo)航欄設(shè)置背景圖片
[self.navigationController.navigationBar setBackgroundImage:[UIImage imageNamed:@"test"] forBarMetrics:UIBarMetricsDefault];
2.safeAreaInsets介紹以及如何利用它適配View布局盔腔。
· 那么問(wèn)題又來(lái)了,當(dāng)我們自己寫(xiě)代碼的時(shí)候月褥,怎么判斷屏幕上顯示的view有沒(méi)有被navigationBar铲觉,tabbar,或者是iphoneX系列的操作條給遮擋呢吓坚。
· 答案是safeAreaInsets
,也就是蘋(píng)果在iOS11撵幽,全面屏手機(jī)推出后的一個(gè)新的UIView的屬性,這個(gè)UIEdgeInset
類(lèi)型的屬性會(huì)告訴我們這個(gè)view的上下左右礁击,各被navigationBar盐杂,tabbar,或者是iphoneX系列的操作條遮擋了多少哆窿,這些被遮擋以外的地方链烈,就是我們所說(shuō)的安全區(qū)域
。
舉個(gè)例子:
導(dǎo)航欄沒(méi)有隱藏且沒(méi)有設(shè)置圖片或者不透明挚躯,也就是說(shuō)系統(tǒng)認(rèn)為導(dǎo)航欄沒(méi)有產(chǎn)生遮擋
@interface ViewController ()<UITableViewDataSource>
@property (nonatomic, strong) UIView *testView;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor whiteColor];
UIView *testV = [[UIView alloc] initWithFrame:CGRectMake(0, 0 ,[UIScreen mainScreen].bounds.size.width , 100)];
testV.backgroundColor = [UIColor redColor];
[self.view addSubview:testV];
self.testView = testV;
}
-(void)viewDidLayoutSubviews{
NSLog(@"self.view.safeAreaInsets: %@",NSStringFromUIEdgeInsets(self.view.safeAreaInsets));
NSLog(@"self.navigationController.view.safeAreaInsets: %@",NSStringFromUIEdgeInsets(self.navigationController.view.safeAreaInsets));
}
打印結(jié)果:
self.view.safeAreaInsets: {88, 0, 34, 0}
self.navigationController.view.safeAreaInsets: {44, 0, 34, 0}
· 通過(guò)打印結(jié)果我們可以發(fā)現(xiàn)强衡,在導(dǎo)航欄沒(méi)有隱藏且系統(tǒng)認(rèn)為導(dǎo)航欄不產(chǎn)生遮擋效果的情況下,self.navigationController.view.safeAreaInsets.top = 44
码荔,這44就是狀態(tài)欄的高度漩勤,所以我們可以看到,navigationBar是從狀態(tài)欄的底部開(kāi)始布局的缩搅。
· self.view.safeAreaInsets.top = 88
由于不產(chǎn)生遮擋越败,導(dǎo)航控制內(nèi)的view會(huì)從狀態(tài)欄的頂部
開(kāi)始布局就像剛開(kāi)始我們像圖1那樣,那么view就被遮擋了導(dǎo)航欄高度44 + 狀態(tài)欄高度44硼瓣,也就是88究飞;
再舉個(gè)例子:
導(dǎo)航欄不隱藏并且設(shè)置了不透明或者背景圖片,也就是說(shuō)系統(tǒng)認(rèn)為導(dǎo)航欄產(chǎn)生了遮擋堂鲤。
@interface ViewController ()<UITableViewDataSource>
@property (nonatomic, strong) UIView *testView;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor whiteColor];
//這里設(shè)置導(dǎo)航欄不透明亿傅,讓子控制器的view從導(dǎo)航欄底部開(kāi)始布局。
self.navigationController.navigationBar.translucent = NO;
UIView *testV = [[UIView alloc] initWithFrame:CGRectMake(0, 0 ,[UIScreen mainScreen].bounds.size.width , 100)];
testV.backgroundColor = [UIColor redColor];
[self.view addSubview:testV];
self.testView = testV;
}
-(void)viewDidLayoutSubviews{
NSLog(@"self.view.safeAreaInsets: %@",NSStringFromUIEdgeInsets(self.view.safeAreaInsets));
NSLog(@"self.navigationController.view.safeAreaInsets: %@",NSStringFromUIEdgeInsets(self.navigationController.view.safeAreaInsets));
}
打印結(jié)果:
self.view.safeAreaInsets: {0, 0, 34, 0}
self.navigationController.view.safeAreaInsets: {44, 0, 34, 0}
通過(guò)打印結(jié)果瘟栖,我們可以得知當(dāng)子控制器view從導(dǎo)航欄底部開(kāi)始布局的時(shí)候葵擎,不會(huì)產(chǎn)生導(dǎo)航欄和狀態(tài)欄的遮擋,所以子控制器view的safeAreaInsets.top
為0慢宗;導(dǎo)航控制器由于被狀態(tài)欄遮擋坪蚁,所以safeAreaInsets.top
還是44奔穿。safeAreaInsets.bottom
當(dāng)然就是iphoneX系列的操作條了,高度為34敏晤。
3.適配全面屏的新思路
· 那么既然有了safeAreaInsets
這個(gè)屬性之后贱田,我們是不可以直接通過(guò)這個(gè)屬性來(lái)適配iOS11后的所有機(jī)型適配呢,這樣以后不就可以發(fā)版適配新的機(jī)型了嘴脾?(因?yàn)槟壳拔覀兌际峭ㄟ^(guò)屏幕尺寸來(lái)適配全面屏系列的男摧,如果發(fā)布了新的尺寸的全面屏機(jī)型,是需要重新判斷是否為全面屏译打,然后發(fā)版解決適配問(wèn)題的)
· 我們來(lái)看下蘋(píng)果文檔對(duì)于safeAreaInsets
這個(gè)屬性的解釋?zhuān)?/p>
If the view is not currently installed in a view hierarchy, or is not yet visible onscreen, the edge insets in this property are 0.
· 當(dāng)一個(gè)view沒(méi)有超出安全區(qū)域被遮擋或者view還沒(méi)有顯示在屏幕上的話(huà)耗拓,view的safeAreaInsets這個(gè)屬性,是不會(huì)計(jì)算的奏司,也就是說(shuō)乔询,返回的是{0,0,0,0}。所以說(shuō)韵洋,在viewDidLoad方法中寫(xiě)布局的話(huà)竿刁,是取不到safeAreaInsets
的值的。
· 那么什么時(shí)候獲取到最新的safeAreaInsets
呢搪缨?在safeAreaInsets
更新的時(shí)候我們根據(jù)safeAreaInsets來(lái)調(diào)整iphoneX的適配食拜,是不是就可以了呢?
· 我們?cè)賮?lái)看下蘋(píng)果的文檔
Declaration
- (void)viewSafeAreaInsetsDidChange;
Discussion
Use this method to update your interface to accommodate the new safe area. UIKit updates the safe area in response to size changes to system bars or when you modify the additional safe area insets of your view controller. UIKit also calls this method immediately before your view appears onscreen.
· 當(dāng)safeAreaInset發(fā)生變化時(shí),控制器會(huì)調(diào)用這個(gè)方法告訴我們safeAreaInset的值更新了副编,那我們?cè)谶@個(gè)方法中负甸,對(duì)iphoneX進(jìn)行適配,是不是就可以了呢痹届?
· 來(lái)寫(xiě)個(gè)demo試一下
@interface ViewController ()<UITableViewDataSource>
/** <#注釋#> */
@property (nonatomic, strong) UITableView *tb;
/** <#注釋#> */
@property (nonatomic, strong) UIView *testView;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor whiteColor];
self.navigationController.navigationBar.translucent = NO;
UIView *testV = [[UIView alloc] initWithFrame:CGRectMake(0, 0 ,[UIScreen mainScreen].bounds.size.width , 100)];
testV.backgroundColor = [UIColor redColor];
[self.view addSubview:testV];
self.testView = testV;
}
-(void)viewSafeAreaInsetsDidChange{
[super viewSafeAreaInsetsDidChange];
self.testView.frame = CGRectMake(0,self.view.safeAreaInsets.top ,[UIScreen mainScreen].bounds.size.width , 100);
}
· 我們可以看到呻待,testView從導(dǎo)航欄底部,開(kāi)始布局了短纵,達(dá)到了我們的預(yù)期效果带污。
在實(shí)際開(kāi)發(fā)中,我們可以在這個(gè)方法內(nèi)香到,對(duì)iOS11之后的所有設(shè)備進(jìn)行適配(這個(gè)方法和safeAreaInsets屬性iOS11之后才有,我們只需要在此方法內(nèi)重寫(xiě)需要適配的控件frame就行了)
舉個(gè)例子:
@interface ViewController ()<UITableViewDataSource>
@property (nonatomic, strong) UIView *testView;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor whiteColor];
// 這里是按照home鍵機(jī)型寫(xiě)的代碼 從導(dǎo)航欄底部開(kāi)始布局 狀態(tài)欄20 + 導(dǎo)航欄44 = 64
UIView *testV = [[UIView alloc] initWithFrame:CGRectMake(0, 64 ,[UIScreen mainScreen].bounds.size.width , 100)];
testV.backgroundColor = [UIColor redColor];
[self.view addSubview:testV];
self.testView = testV;
}
// 全面屏手機(jī)統(tǒng)一進(jìn)入這個(gè)方法適配
-(void)viewSafeAreaInsetsDidChange{
[super viewSafeAreaInsetsDidChange];
self.testView.frame = CGRectMake(0,self.view.safeAreaInsets.top ,[UIScreen mainScreen].bounds.size.width , 100);
}
· 這個(gè)做法和現(xiàn)在目前主流的通過(guò)屏幕尺寸判斷是否為全面屏手機(jī)的方法比確實(shí)是麻煩了一些报破,但是如果蘋(píng)果發(fā)布了新的尺寸iphone悠就,通過(guò)尺寸宏就沒(méi)辦法判斷了。比如這次的iphoneXR和iphoneX MAX充易,就非彻Fⅲ坑爹,需要將宏擴(kuò)展后發(fā)版盹靴,才能解決適配問(wèn)題炸茧。
3. safeAreaInsets是如何影響著scrollView及其子類(lèi)(tableView)
有了前面1瑞妇、2兩個(gè)知識(shí)點(diǎn)的儲(chǔ)備以后,tableView在navigationController中的適配會(huì)簡(jiǎn)單很多梭冠。我們還是直接用代碼來(lái)說(shuō)明問(wèn)題辕狰,還是分兩種情況:導(dǎo)航欄產(chǎn)生遮擋 和 不產(chǎn)生遮擋
導(dǎo)航欄不遮擋代碼:
@interface ViewController ()<UITableViewDataSource>
@property (nonatomic, strong) UITableView *tb;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor whiteColor];
UITableView *tb = [[UITableView alloc] initWithFrame:self.view.bounds style:UITableViewStylePlain];
tb.dataSource = self;
[self.view addSubview:tb];
self.tb = tb;
}
-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{
return 60;
}
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell"];
if (cell == nil) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"cell"];
}
cell.textLabel.text = [NSString stringWithFormat:@"%ld",indexPath.row];
return cell;
}
-(void)viewDidLayoutSubviews{
NSLog(@"self.view.safeAreaInsets: %@",NSStringFromUIEdgeInsets(self.view.safeAreaInsets));
NSLog(@"self.navigationController.view.safeAreaInsets: %@",NSStringFromUIEdgeInsets(self.navigationController.view.safeAreaInsets));
NSLog(@"self.tb.safeAreaInsets: %@",NSStringFromUIEdgeInsets(self.tb.safeAreaInsets));
NSLog(@"self.tb.adjustedContentInset: %@",NSStringFromUIEdgeInsets(self.tb.adjustedContentInset));
NSLog(@"self.tb.contentInset: %@",NSStringFromUIEdgeInsets(self.tb.contentInset));
}
打印結(jié)果:
self.view.safeAreaInsets: {88, 0, 34, 0}
self.navigationController.view.safeAreaInsets: {44, 0, 34, 0}
self.tb.safeAreaInsets: {88, 0, 34, 0}
self.tb.adjustedContentInset: {88, 0, 34, 0}
self.tb.contentInset: {0, 0, 0, 0}
從圖4中我們可以看到,即便導(dǎo)航欄沒(méi)有產(chǎn)生遮擋控漠,tableview從狀態(tài)欄頂部開(kāi)始布局蔓倍,contentInset為0,tablView的的上下inset還是多了88 和 34盐捷,這兩個(gè)數(shù)字我相信大家已經(jīng)很熟悉了偶翅,88為狀態(tài)欄的高度加上導(dǎo)航欄的高度,34為底部操作條的高度碉渡。
基于上面的代碼我們把tableView的contentInset屬性改一下聚谁,再來(lái)看看效果
tb.contentInset = UIEdgeInsetsMake(100, 0, 100, 0);
打印結(jié)果
self.tb.safeAreaInsets: {88, 0, 34, 0}
self.tb.adjustedContentInset: {188, 0, 134, 0}
self.tb.contentInset: {100, 0, 100, 0}
我們可以看到雖然contentInset我們只給了100,但是tableView的content頂部卻偏移了188滞诺。
由此可見(jiàn)iOS11之后形导,決定tableView的inset不再是
contentInset
這個(gè)屬性,而是adjustedContentInset
這個(gè)屬性铭段。通過(guò)打印結(jié)果骤宣,我們可以發(fā)現(xiàn)
adjustedContentInset
= contentInset + safeAreaInsets
導(dǎo)航欄遮擋代碼:我們基于上面的代碼,將導(dǎo)航欄設(shè)置成不透明的
self.navigationController.navigationBar.translucent = NO;
tb.contentInset = UIEdgeInsetsMake(0, 0, 0, 0);
打印結(jié)果:
self.tb.safeAreaInsets: {0, 0, 34, 0}
self.tb.adjustedContentInset: {0, 0, 134, 0}
self.tb.contentInset: {0, 0, 0, 0}
由于導(dǎo)航欄設(shè)置成了不透明序愚,導(dǎo)航控制器中的子控制器View的布局從navigationBar底部開(kāi)始憔披,所以tableView不存在遮擋的情況,tableView.safeAreaInsets.top
也就理所當(dāng)然的變成了0爸吮;
問(wèn)題這個(gè)時(shí)候又來(lái)了芬膝,我們會(huì)發(fā)現(xiàn)tableView滾到底部的時(shí)候,就滾不下去了形娇,下面差了88pt锰霜。產(chǎn)生這個(gè)問(wèn)題的原因很簡(jiǎn)單,因?yàn)閚avigationBar產(chǎn)生了遮擋桐早,子控制器view從導(dǎo)航欄底部布局癣缅,但是我們的tableView高度卻是self.view.bounds
,所以tableView超出了屏幕88pt(導(dǎo)航欄高度+狀態(tài)欄的高度)哄酝。
我們上代碼友存,看一下如何適配。
@interface ViewController ()<UITableViewDataSource>
@property (nonatomic, strong) UITableView *tb;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor whiteColor];
self.navigationController.navigationBar.translucent = NO;
UITableView *tb = [[UITableView alloc] initWithFrame:self.view.bounds style:UITableViewStylePlain];
CGFloat navBarHeight = self.navigationController.navigationBar.frame.size.height;
CGFloat statusBarHeight = [UIApplication sharedApplication].statusBarFrame.size.height;
// 給tableView.contentInset加上導(dǎo)航欄高度+狀態(tài)欄高度 補(bǔ)償下移超出屏幕的部分
tb.contentInset = UIEdgeInsetsMake(0, 0, navBarHeight + statusBarHeight , 0);
tb.dataSource = self;
[self.view addSubview:tb];
self.tb = tb;
}
-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{
return 60;
}
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell"];
if (cell == nil) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"cell"];
}
cell.textLabel.text = [NSString stringWithFormat:@"%ld",indexPath.row];
return cell;
}
總結(jié)
1.導(dǎo)航控制器內(nèi)的子控制器view的布局由navigationBar setBackgroundImage:
陶衅,navigationBar.translucent
決定屡立。
2.利用iOS11的新特性safeAreaInsets
以及-(void)viewSafeAreaInsetsDidChange
來(lái)完成iOS11之后機(jī)型的所有適配,免去了判斷全面屏宏的設(shè)置搀军,將來(lái)再出新的機(jī)型不用動(dòng)任何代碼也可以完美適配膨俐。
如果這篇文章的內(nèi)容讓你有所收獲勇皇,請(qǐng)記得點(diǎn)贊喲
如有問(wèn)題,請(qǐng)留言或者直接私信我焚刺。今天就先到這了 byebye~