【iOS】论如何优雅的使用安全区来适配iPhone X屏幕
简述
一般人而言,对屏幕的适配仅仅只是机型的适配,不会考虑到iOS系统版本(iOS6到7的适配除外)与Xcode版本。对新机型也是加个判断的事,但这样子容易造成代码过多,并且对以后新增的机型适配不利(可能要重构代码等)。接下来我要通过一些例子,提供一些优雅适配系统、机型和Xcode版本的思路。
使用masonry适配
控制器内适配
首先看一段在控制器中的代码:
SettingTableView *tableView = [[SettingTableView alloc] initWithFrame:CGRectZero style:UITableViewStyleGrouped];
[[self view] addSubview:tableView];
[tableView mas_makeConstraints:^(MASConstraintMaker *make) {
[[make trailing] leading].equalTo([self view]);
MASViewAttribute *top = [self mas_topLayoutGuideBottom];
MASViewAttribute *bottom = [self mas_bottomLayoutGuideTop];
#ifdef __IPHONE_11_0
if (@available(iOS 11.0, *))
{
top = [[self view] mas_safeAreaLayoutGuideTop];
bottom = [[self view] mas_safeAreaLayoutGuideBottom];
}
#endif
[make top].equalTo(top);
[make bottom].equalTo(bottom);
}];
[[make trailing] leading].equalTo([self view]); // 这行不需要多说,一般而言,add到控制器的view左右都是贴边的。重点在于下面的:
MASViewAttribute *top = [self mas_topLayoutGuideBottom];
MASViewAttribute *bottom = [self mas_bottomLayoutGuideTop]; // 这里是默认适配iOS10及以下机型
#ifdef __IPHONE_11_0 // 如果有这个宏,说明Xcode版本是9开始
if (@available(iOS 11.0, *)) // 判断iOS系统是不是11以上
{
// 如果是11以上,则使用安全区来适配
top = [[self view] mas_safeAreaLayoutGuideTop];
bottom = [[self view] mas_safeAreaLayoutGuideBottom];
}
#endif
[make top].equalTo(top);
[make bottom].equalTo(bottom);
__IPHONE_11_0 这个宏只有Xcode9及以上版本才会有,如果有这个宏,说明需要支持到iOS11以上,如果没有这个宏,下面的if语句会在预编译时忽然掉,所以即使是低版本的Xcode没有下面的方法也不会报错。
另外需要注意下
mas_bottomLayoutGuide是与tabbar、toolbar等相关的,默认值是mas_bottomLayoutGuideTop
mas_bottomLayoutGuideTop是指参考到tabbar的top
mas_bottomLayoutGuideBottom是指参考到tabbar的bottom
所以,在没有显示有tabbar等系统控件的情况下,Top和Bottom都是一样的。
在自定义view内适配
在自定义view中,如果涉及到安全区和屏幕旋转(比如视频播放器里的view),我们该如何适配呢?
同样的,我们先来看段代码:
[lockButton mas_makeConstraints:^(MASConstraintMaker *make) {
[make centerY].equalTo(self);
[[make width] height].mas_equalTo(50);
MASViewAttribute *leading = [self mas_leading];
#ifdef __IPHONE_11_0
if (@available(iOS 11.0, *))
{
leading = [self mas_safeAreaLayoutGuide];
}
#endif
[make leading].equalTo(leading);
}];
这是播放器里面常见的小锁头按钮,这个按钮我们先让它居中,然后放在左侧的安全区内。这段代码是允许屏幕旋转的。
mas_safeAreaLayoutGuide表示在安全区内,并没有表示方向,事实上可以加Leading来表示左侧(即mas_safeAreaLayoutGuideLeading)。
使用frame布局
虽然我不提倡使用这种落后的布局方式,但有些情况下还是挺有用的,现在我们来看下如何使用frame来适配X的屏幕。
在控制器内适配
首先,我们在工具类里面增加一个类方法,我这里的工具类的名字是Tools。
+ (UIEdgeInsets)safeAreaInsetsWithView:(UIView *)view // 获取安全区域
{
#ifdef __IPHONE_11_0
if (@available(iOS 11.0, *))
{
return [view safeAreaInsets];
}
#endif
return UIEdgeInsetsZero;
}
然后,我们来看看控制器里的方法。
如果是在某个时候出现的view(比如点击的时候才会创建并显示),我们可以使用以下代码来适配:
CGFloat height = kToolBarHeight;
CGRect frame = CGRectMake(0, self.view.frame.size.height, self.view.frame.size.width, height);
UIEdgeInsets insets = [Tools safeAreaInsetsWithView:[self view]];
frame.origin.y += insets.bottom;
frame.size.height += insets.bottom;
UIView *view = [[UIView alloc] initWithFrame:frame];
但是,如果是需要进来就显示的view,就不能用上面的方法去适配了,因为safeAreaInsets直到viewSafeAreaInsetsDidChange调用前,都是UIEdgeInsetsZero。
viewSafeAreaInsetsDidChange的调用在viewDidLayoutSubviews之前,所以如果我们需要进来就布局好的话,可以在viewDidLayoutSubviews里布局。
但是,viewDidLayoutSubviews的调用是很频繁的,如果你在viewDidLoad已经布局好,只想当安全区改变的时候去适配安全区,那就应该重写viewSafeAreaInsetsDidChange方法,在viewSafeAreaInsetsDidChange里适配,而不是在viewDidLayoutSubviews里适配。
#ifdef __IPHONE_11_0
- (void)viewSafeAreaInsetsDidChange
{
[super viewSafeAreaInsetsDidChange];
// 高度增加到最底部
CGRect frame = [[self bottomView] frame];
UIEdgeInsets insets = [Tools safeAreaInsetsWithView:[self view]];
CGFloat height = kToolBarHeight;
height += insets.bottom;
frame.size.height = height;
[[self bottomView] setFrame:frame];
return;
}
#endif
事实上这里的safeAreaInsetsWithView方法调用可以换成直接使用[[self view] safeAreaInsets],但是为了以后考虑(鬼知道苹果之后会出什么奇葩操作。。。),统一使用该方法来获取安全区,如果以后需要修改,我们只需要修改safeAreaInsetsWithView方法的实现即可。
在自定义view内适配
自定义view里其实和控制器是差不多的,只需要把viewSafeAreaInsetsDidChange换成safeAreaInsetsDidChange即可:
#ifdef __IPHONE_11_0
- (void)safeAreaInsetsDidChange
{
[super safeAreaInsetsDidChange];
// 把y调到安全区内
CGRect frame = [[self indicator] frame];
UIEdgeInsets insets = [Tools safeAreaInsetsWithView:self];
frame.origin.y = insets.top;
[[self indicator] setFrame:frame];
return;
}
#endif
此外,你可能会使用到以下的宏,我一般把它们定义在pch文件里:
#define kScreenHeight [[UIScreen mainScreen] bounds].size.height // 物理屏幕高度
#define kScreenWidth [[UIScreen mainScreen] bounds].size.width // 物理屏幕宽度
#define kIsFullScreen ((([[[UIDevice currentDevice] systemVersion] floatValue] >= 11.0f) && ([[[[UIApplication sharedApplication] delegate] window] safeAreaInsets].bottom > 0.0))? YES : NO) // 判断是否全面屏
#define kIsiPhoneX CGSizeEqualToSize(CGSizeMake(1125, 2436), [[UIScreen mainScreen] currentMode].size) // 判断是否是iPhone X
#define kStatusBarHeight (kIsFullScreen ? 44.f : 20.f) // 状态栏高度
#define kNavigationBarHeight (kIsFullScreen ? 88.f : 64.f) // 导航栏高度
#define kTabBarHeight (kIsFullScreen? (49.f + 34.f) : 49.f) // tabBar高度
#define kHomeIndicatorHeight (kIsFullScreen ? 34.f : 0.f) // home指示器高度
结语
简洁优雅的代码都是大家努力追求的,平时留点心就能为维护带来想不到的好处,何乐而不为呢?好了,抛砖引玉就到此结束了,有什么好的建议与不足可以在评论或者我的QQ群(139322447)指出,我会根据实际情况来更新,谢谢大家的阅读。
最后,感谢我女朋友在我饿着肚子写文章的时候,给我买了我喜欢吃的😌