【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指示器高度 

结语

简洁优雅的代码都是大家努力追求的,平时留点心就能为维护带来想不到的好处,何乐而不为呢?好了,抛砖引玉就到此结束了,有什么好的建议与不足可以在评论中指出,我会根据实际情况来更新,谢谢大家的阅读。

最后,感谢我女朋友在我饿着肚子写文章的时候,给我买了我喜欢吃的

iOS OC Swift Flutter开发群 139322447

你可能感兴趣的:(【iOS】论如何优雅的使用安全区来适配iPhone X屏幕)