/KRTestLayout

总结iOS 从7 到11 布局变化,适配问题

Primary LanguageObjective-CMIT LicenseMIT

iOS 各种边距

1.UIViewController的边距

edgesForExtendedLayout、automaticallyAdjustsScrollViewInsets属性

edgesForExtendedLayout

默认值为UIRectEdgeAll

作用:

指定边缘延伸的方向,默认值是UIRectEdgeAll四周延伸,就是说即使视图中上有navigationBar,下有tabBar,那么视图仍会延伸覆盖到四周的区域。

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);

automaticallyAdjustsScrollViewInsets

iOS7以后布局是全局的如果设置了frame y从0开始的话则,在有导航栏的情况下,tableView的布局会被遮住一部分,为了解决这个问题增加了automaticallyAdjustsScrollViewInsets属性,默认YES

在一个界面有tatus bar,navigationbar,tabbar时候,UIScrollViw及其子类如UITableView,UIWebView,UITextView就会调整内边距,让frame的 y 不是从tatus bar,navigationbar底部开始为。

(1)当automaticallyAdjustsScrollViewInsets为YES的时候

(2) self.automaticallyAdjustsScrollViewInsets = NO;

(3) 当automaticallyAdjustsScrollViewInsets为YES的时候edgesForExtendedLayout属性没啥作用

(4) 当automaticallyAdjustsScrollViewInsets为NO的时候edgesForExtendedLayout属性默认的时候既UIRectEdgeAll时候 情况同(2)一样 edgesForExtendedLayout属性为UIRectEdgeNone时候同(1)一样

综上所述,两个属性作用平时开发起来很恶心,因为实际场景中会经常遇到隐藏导航栏的情况,有些时候记住这些属性比较麻烦,因此同意设定为

self.automaticallyAdjustsScrollViewInsets = NO;
self.edgesForExtendedLayout = UIRectEdgeAll;

所有的情况由我们自己控制。

2.safeArea

上述属性是在iOS7 - iOS10中的到了iOS 11中又出现了新的特性safeArea,并且把automaticallyAdjustsScrollViewInsets给废除了,又导致了很多蛋疼的问题。

因为iPhone X的奇葩外观,必须确保布局充满屏幕,并且布局不会被设备的圆角、传感器外壳或者用于访问主屏幕的指示灯遮挡住。因此,苹果提出了safe area(安全区)的概念,就是上述可能遮挡界面的区域以外的区域被定义为安全区。

苹果推荐的安全区,竖屏下如图所示,栏高度为 44pt,下方指示灯处的高度为 34pt

横屏下,上下安全边距分别为 0pt/21pt,左右安全边距为 44pt/44pt

使用了UINavigationBar和UITabBar,安全区的上边缘会变成导航栏下边缘的位置

实际使用中可以通过下面的宏进行适配

状态栏高度的宏定义为:

#define STATUS_HEIGHT   (IS_IPHONE_X?44:20)

增加安全区域下面的区域的高度宏定义为:

#define BOTTOM_SAFEAREA_HEIGHT (IS_IPHONE_X? 34 : 0)

如果你同时也用了自定义的UITabBar那么就需要修改TABBAR_HEIGHT的宏定义为:

#define TABBAR_HEIGHT   (IS_IPHONE_X? (49 + 34) : 49)

安全区主要是通过下面两个属性一个方法确定

safeAreaInsets和safeAreaLayoutGuide
-(void)viewSafeAreaInsetsDidChange

方法测试 有navgationbar

- (void)viewSafeAreaInsetsDidChange {
    [super viewSafeAreaInsetsDidChange];
    NSLog(@"%@", NSStringFromUIEdgeInsets(self.view.safeAreaInsets));
    NSLog(@"%@", NSStringFromUIEdgeInsets(self.additionalSafeAreaInsets));
    /**
     * 在iOS 11 iPhone 8 有navgationbar 打印如下
     * 2017-12-12 17:27:34.789117+0800 KRTestLayout[7520:1607431] {64, 0, 0, 0}
     * 2017-12-12 17:27:34.789314+0800 KRTestLayout[7520:1607431] {0, 0, 0, 0}
     *
     * 在iOS 11 iPhone X 有navgationbar 打印如下
     * 2017-12-12 17:30:55.503033+0800 KRTestLayout[7633:1623120] {88, 0, 34, 0}
     *  2017-12-12 17:30:55.503169+0800 KRTestLayout[7633:1623120] {0, 0, 0, 0}
     */
}

无navgationbar

- (void)viewWillAppear:(BOOL)animated {
    [super viewWillAppear:animated];
    
    [self.navigationController setNavigationBarHidden:YES animated:animated];
    
    /**
     * 在iOS 11 iPhone 8 无navgationbar 打印如下
     * 2017-12-12 17:34:37.382000+0800 KRTestLayout[7756:1637921] {20, 0, 0, 0}
     * 2017-12-12 17:34:37.382157+0800 KRTestLayout[7756:1637921] {0, 0, 0, 0}
     *
     * 在iOS 11 iPhone X 无navgationba 打印如下
     * 2017-12-12 17:35:18.205627+0800 KRTestLayout[7801:1646680] {44, 0, 34, 0}
     * 2017-12-12 17:35:18.205811+0800 KRTestLayout[7801:1646680] {0, 0, 0, 0}
     */
}

- (void)viewWillDisappear:(BOOL)animated {
    [super viewWillDisappear:animated];

    [self.navigationController setNavigationBarHidden:NO animated:animated];
}

3.UIScrollView、UITableView的各种边距

UIScrollView 可滚动视图,UITableView集成UIScrollView.

令人迷惑的三个属性

contentSize
contentInset
contentOffset

**contentSize:**可以滚动的区域 **contentOffset:**偏移量,其中分为contentOffset.y=内容的顶部和frame顶部的差值,contentOffset.x=内容的左边和frame左边的差值, **contentInset:**即内边距,contentInset = 在内容周围增加的间距(粘着内容),contentInset的单位是UIEdgeInsets,默认值为UIEdgeInsetsZero。

几者之间关系:

contentOffset.y = 内容的顶部 与 frame顶部差值 contentInset = 在内容周围增加边距

tableView的contentSize包括的:

Cell、TableViewHeaderView、TableViewFooterView、SectionHeader、 SectionFooter

(1)情景一:没有cell,没有contentInset,没有tableHeaderView\tableFooterView

(2)情景二:没有cell,没有contentInset,有tableHeaderView\tableFooterView

(3)情景三:有cell,没有contentInset,没有tableHeaderView\tableFooterView

(4)情景四:有cell,有contentInset,没有tableHeaderView\tableFooterView

(5)情景五:有cell,没有contentInset,有tableHeaderView\tableFooterView

(6)情景六:有cell,有contentInset,有tableHeaderView\tableFooterView

(7)情景七:有cell,有contentInset,没有tableHeaderView\tableFooterView,添加了一个子控件,frame是CGRectMake(0, 0, 300, 50),子控件的frame以父控件内容左上角为坐标原点{0,0}

(8)情景八:有cell,有contentInset,没有tableHeaderView\tableFooterView,添加了一个子控件,frame是CGRectMake(0,-50, 300, 50),子控件的frame以父控件内容左上角为坐标原点{0,0}

(9)情景九:有cell,没有contentInset,有tableHeaderView\tableFooterVi添加了一个子控件,frame是CGRectMake(0,-50, 300, 50),子控件的frame以父控件内容左上角为坐标原点{0,0}

在iOS11增加了两个新的属性

adjustContentInset
contentInsetAdjustmentBehavior

进头文件里面去看看

/* When contentInsetAdjustmentBehavior allows, UIScrollView may incorporate
 its safeAreaInsets into the adjustedContentInset.
 */
@property(nonatomic, readonly) UIEdgeInsets adjustedContentInset API_AVAILABLE(ios(11.0),tvos(11.0));

/* Also see -scrollViewDidChangeAdjustedContentInset: in the UIScrollViewDelegate protocol.
 */
- (void)adjustedContentInsetDidChange API_AVAILABLE(ios(11.0),tvos(11.0)) NS_REQUIRES_SUPER;

/* Configure the behavior of adjustedContentInset.
 Default is UIScrollViewContentInsetAdjustmentAutomatic.
 */
@property(nonatomic) UIScrollViewContentInsetAdjustmentBehavior contentInsetAdjustmentBehavior API_AVAILABLE(ios(11.0),tvos(11.0));

adjustedContentInset是一个只读属性,我们不可以改变,那么它的值代表什么呢?怎么才会改变的?

adjustedContentInset代表的含义和contentInset 是一样的,contentView.frame.origin偏移了scrollview.frame.origin多少。

adjustedContentInset是只读的,它的值由safeAreaInset 与 contentInset 计算而来的,计算方式由以下4种方式,由新加的枚举属性contentInsetAdjustmentBehavior决定

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

测试代码见项目中KRSafeAreaViewController控制器,测试值在iOS 11 iPhone 8中打印

(1)当contentInsetAdjustmentBehavior为UIScrollViewContentInsetAdjustmentAutomatic时

如果scrollview在一个contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentAutomatic的controller上,并且这个Controller包含在一个navigation controller中,这种情况下会设置在top & bottom上 adjustedContentInset = safeAreaInset + contentInset 不管是否滚动。

- (void)testAdjustContentInsetAutomatic {
    if (@available(iOS 11.0, *)) {
        self.tableView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentAutomatic;
        
        self.tableView.contentInset = UIEdgeInsetsMake(60, 0, 0, 0);
//        2017-12-13 16:21:09.759001+0800 KRTestLayout[6338:1032624] contentInset-> {60, 0, 0, 0}
//        2017-12-13 16:21:09.759123+0800 KRTestLayout[6338:1032624] safeAreaInsets-> {64, 0, 0, 0}
//        2017-12-13 16:21:09.759214+0800 KRTestLayout[6338:1032624] additionalSafeAreaInsets-> {0, 0, 0, 0}
//        2017-12-13 16:21:09.759306+0800 KRTestLayout[6338:1032624] adjustedContentInset-> {124, 0, 0, 0}
        self.additionalSafeAreaInsets =  UIEdgeInsetsMake(120, 0, 0, 0);
//        2017-12-13 16:22:20.659263+0800 KRTestLayout[6368:1037697] contentInset-> {60, 0, 0, 0}
//        2017-12-13 16:22:20.659419+0800 KRTestLayout[6368:1037697] safeAreaInsets-> {184, 0, 0, 0}
//        2017-12-13 16:22:20.659537+0800 KRTestLayout[6368:1037697] additionalSafeAreaInsets-> {120, 0, 0, 0}
//        2017-12-13 16:22:20.659649+0800 KRTestLayout[6368:1037697] adjustedContentInset-> {244, 0, 0, 0}
    }
}

(2)当contentInsetAdjustmentBehavior为UIScrollViewContentInsetAdjustmentScrollableAxes时

在可滚动方向上adjustedContentInset = safeAreaInset + contentInset,在不可滚动方向上adjustedContentInset = contentInset;依赖于scrollEnabled和alwaysBounceHorizontal / vertical = YES,scrollEnabled默认为yes,所以大多数情况下,计算方式还是adjustedContentInset = safeAreaInset + contentInset

- (void)testAdjustContentInsetScrollableAxes {
    if (@available(iOS 11.0, *)) {
        self.tableView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentScrollableAxes;
        
        self.tableView.contentInset = UIEdgeInsetsMake(60, 0, 0, 0);
//        2017-12-13 16:30:01.264643+0800 KRTestLayout[6507:1064131] contentInset-> {60, 0, 0, 0}
//        2017-12-13 16:30:01.264804+0800 KRTestLayout[6507:1064131] safeAreaInsets-> {64, 0, 0, 0}
//        2017-12-13 16:30:01.264890+0800 KRTestLayout[6507:1064131] additionalSafeAreaInsets-> {0, 0, 0, 0}
//        2017-12-13 16:30:01.264983+0800 KRTestLayout[6507:1064131] adjustedContentInset-> {124, 0, 0, 0}
        
        self.tableView.scrollEnabled = NO;
//        2017-12-13 16:30:47.083295+0800 KRTestLayout[6539:1067398] contentInset-> {60, 0, 0, 0}
//        2017-12-13 16:30:47.083438+0800 KRTestLayout[6539:1067398] safeAreaInsets-> {0, 0, 0, 0}
//        2017-12-13 16:30:47.083549+0800 KRTestLayout[6539:1067398] additionalSafeAreaInsets-> {0, 0, 0, 0}
//        2017-12-13 16:30:47.083636+0800 KRTestLayout[6539:1067398] adjustedContentInset-> {60, 0, 0, 0}
    }
}

(3)当contentInsetAdjustmentBehavior为UIScrollViewContentInsetAdjustmentNever时

adjustContentInset值不受SafeAreaInset值的影响。

adjustedContentInset = contentInset

- (void)testAdjustContentInsetNever {
    if (@available(iOS 11.0, *)) {
        self.tableView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
        self.tableView.contentInset = UIEdgeInsetsMake(60, 0, 0, 0);
        
//        2017-12-13 16:33:20.537079+0800 KRTestLayout[6618:1080094] contentInset-> {60, 0, 0, 0}
//        2017-12-13 16:33:20.537246+0800 KRTestLayout[6618:1080094] safeAreaInsets-> {0, 0, 0, 0}
//        2017-12-13 16:33:20.537390+0800 KRTestLayout[6618:1080094] additionalSafeAreaInsets-> {0, 0, 0, 0}
//        2017-12-13 16:33:20.537490+0800 KRTestLayout[6618:1080094] adjustedContentInset-> {60, 0, 0, 0}
    }
}

(4)当contentInsetAdjustmentBehavior为UIScrollViewContentInsetAdjustmentAlways时

adjustedContentInset = safeAreaInset + contentInset

- (void)testAdjustContentInsetAlways {
    if (@available(iOS 11.0, *)) {
        self.tableView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentAlways;
        self.tableView.contentInset = UIEdgeInsetsMake(60, 0, 0, 0);
        
//        2017-12-13 16:35:05.990422+0800 KRTestLayout[6666:1088653] contentInset-> {60, 0, 0, 0}
//        2017-12-13 16:35:05.990543+0800 KRTestLayout[6666:1088653] safeAreaInsets-> {64, 0, 0, 0}
//        2017-12-13 16:35:05.990670+0800 KRTestLayout[6666:1088653] additionalSafeAreaInsets-> {0, 0, 0, 0}
//        2017-12-13 16:35:05.990771+0800 KRTestLayout[6666:1088653] adjustedContentInset-> {124, 0, 0, 0}
    }
}

UITableView中莫名多20个像素的问题

4.NavigationBar的各种边距