iOS 11正式發布了,下面整理了一些該版本下的特點還有如何進行兼容適配工作。
1. UIView變化
1.1. 更加方便的RTL邊距設置
在之前的系統中我們會使用layoutMargins
來獲取和設置控件顯示內容部分的邊緣與控件邊緣的距離。在iOS 11中,新增directionalLayoutMargins
屬性來指定邊距。這兩個屬性的結構定義如下:
typedef struct UIEdgeInsets {
CGFloat top, left, bottom, right;
} UIEdgeInsets;
typedef struct NSDirectionalEdgeInsets {
CGFloat top, leading, bottom, trailing;
} NSDirectionalEdgeInsets
從結構上看主要是將UIEdgeInsets
結構的left
和right
調整為NSDirectionalEdgeInsets
結構的leading
和trailing
。這一調整主要是為了Right To Left(RTL)語言下可以進行自動適配,例如:要實現文本每行尾部邊距設置為30px,在以前做法則需要判斷語言來區分哪些是RTL語言,然后再做設置,如:
if ([UIView userInterfaceLayoutDirectionForSemanticContentAttribute:self.view.semanticContentAttribute] == UIUserInterfaceLayoutDirectionRightToLeft)
{
// Right to left 語言下每行尾部在左邊
self.view.layoutMargins.left = 30;
}
else
{
self.view.layoutMargins.right = 30;
}
iOS 11 后則可以一步到位,如:
self.view.directionalLayoutMargins = NSDirectionalEdgeInsetsMake(0, 0, 0, 30);
注:測試時需要添加RTL本地化語言才能看到效果
1.2. 安全區域
在iOS 11中新增了安全區域的概念,目的是告訴開發者在這個區域下繪制的內容的顯示才是有效的,否則會存在被遮擋的情況(特別是iPhoneX那帥氣的劉海)。在UIView中新增safeAreaLayoutGuide
和safeAreaInsets
來獲取屏幕的安全區域(對于frame布局時是很有用的)。如圖所示:
舉個例子,在一個空白的UIViewController
中,分別在viewDidLoad
和viewDidAppear
方法中輸出view.safeAreaInsets
觀察邊距情況,代碼如下:
- (void)viewDidLoad
{
[super viewDidLoad];
NSString *edgeStr = NSStringFromUIEdgeInsets(self.view.safeAreaInsets);
NSString *layoutFrmStr = NSStringFromCGRect(self.view.safeAreaLayoutGuide.layoutFrame);
NSLog(@"viewDidLoad safeAreaInsets = %@, layoutFrame = %@", edgeStr, layoutFrmStr);=
}
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
NSString *edgeStr = NSStringFromUIEdgeInsets(self.view.safeAreaInsets);
NSString *layoutFrmStr = NSStringFromCGRect(self.view.safeAreaLayoutGuide.layoutFrame);
NSLog(@"viewDidAppear safeAreaInsets = %@, layoutFrame = %@", edgeStr, layoutFrmStr);
}
可以看到其輸出為:
2017-09-19 14:45:50.246095+0800 Sample[5608:1365070] viewDidLoad safeAreaInsets = {0, 0, 0, 0}, layoutFrame = {{0, 0}, {375, 667}}
2017-09-19 14:45:50.257807+0800 Sample[5608:1365070] viewDidAppear safeAreaInsets = {20, 0, 0, 0}, layoutFrame = {{0, 20}, {375, 603}}
可見,在視圖顯示完成的時候View的頂部邊距變為了20px,而這20px正是狀態欄的高度。同樣原理,如果你的是一個UINavigationController
那在顯示的時候view.safeAreaInsets
就會變成{64, 0, 0, 0}
。注意:在該VC下所有的UIView及其子類獲取到safeAreaInsets
的值是相同的。
如果你想準確地知道安全區域是什么時候被改變的,可以重寫UIView
的safeAreaInsetsDidChange
方法,在這個方法里面可以監聽安全區域的邊距調整的事件(如果使用的是UIViewController
,其也提供相應方法來實現監聽,下一章節會講述該部分內容),代碼如下:
- (void)safeAreaInsetsDidChange
{
//寫入變更安全區域后的代碼...
}
如果你不想讓safeAreaInsets
影響你的視圖布局,則可以將insetsLayoutMarginsFromSafeArea
設置為NO,所有的視圖布局將會忽略safeAreaInsets
這個屬性了。要注意的是,insetsLayoutMarginsFromSafeArea
僅用于使用代碼實現AutoLayout(如果你是使用Xib或者SB布局你的視圖,那么對該屬性的設置是無效的,至少我沒有發現怎么可以讓布局產生變化),即使該屬性為NO,視圖的safeAreaInsets
還是一樣有值,而且安全區域變更方法safeAreaInsetsDidChange
一樣被調用??梢詤⒖枷旅媸纠a:
@interface ViewController ()
@property (nonatomic, strong) UITableView *tableView;
@end
@implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad];
self.view.backgroundColor = [UIColor yellowColor];
self.view.insetsLayoutMarginsFromSafeArea = NO;
self.tableView = [[UITableView alloc] initWithFrame:CGRectZero style:UITableViewStylePlain];
self.tableView.translatesAutoresizingMaskIntoConstraints = NO;
[self.view addSubview:self.tableView];
NSArray<__kindof NSLayoutConstraint *> *constraints = [NSLayoutConstraint constraintsWithVisualFormat:@"V:|-[tableView]-|" options:0 metrics:nil views:@{@"tableView" : self.tableView}];
[self.view addConstraints:constraints];
constraints = [NSLayoutConstraint constraintsWithVisualFormat:@"H:|[tableView]|" options:0 metrics:nil views:@{@"tableView" : self.tableView}];
[self.view addConstraints:constraints];
}
@end
上面代碼在insetsLayoutMarginsFromSafeArea
屬性尚未設置時其布局受SafeArea影響,效果如下:
設置后不再受SafeArea影響,效果如下:
2. UIViewController變化
2.1. 廢除API
2.1.1. automaticallyAdjustsScrollViewInsets
方法
iOS 7中使用該方法來自動調整UIScrollView
的contentInset
。在iOS 11之后將會使用UIScrollView
的contentInsetAdjustmentBehavior
屬性來代替該方法。
2.1.2. topLayoutGuide
和bottomLayoutGuide
屬性
iOS 7中使用這兩個屬性來指導帶有導航欄(NaviagtionBar)和頁簽欄(TabBar)的視圖排版。其作用如下圖所示:
在iOS 11之后將使用安全區域(Safe Area)來代替該部分功能的實現。
2.2. 排版
2.2.1. additionalSafeAreaInsets
屬性
iOS 11加入安全區域后,對于VC則可以通過該屬性來對該區域附加一個邊距信息。如:
self.additionalSafeAreaInsets = UIEdgeInsetsMake(30, 0, 0, 30);
注意:這里是附加邊距,意思就是在原有的safeAreaInsets
值中增加對應的邊距值。如果原來的是{10, 0, 0, 10}, 則最后得出的邊距是{40, 0, 0, 40}。
2.2.2. systemMinimumLayoutMargins
和viewRespectsSystemMinimumLayoutMargins
屬性
該屬性表示了一個系統最小的邊距信息,所有的視圖排版都應該遵循這個邊距信息的。除非將viewRespectsSystemMinimumLayoutMargins
設置為NO。
2.2.3. viewLayoutMarginsDidChange
方法
根視圖的邊距變更時會觸發該方法的回調??梢酝ㄟ^該方法來處理當邊距改變時子視圖的布局。
2.2.4. viewSafeAreaInsetsDidChange
方法
當視圖的安全區域發生變更時會觸發該方法的回調??梢酝ㄟ^該方法來處理安全區域變更時的子視圖布局。
3. UINavigationBar變化
iOS 11中加入了大標題模式,其顯示效果如下所示:
實現該效果需要將導航欄的prefersLargeTitles
設置為YES,如:
self.navigationController.navigationBar.prefersLargeTitles = YES;
4. UINavigationItem變化
4.1 控制大標題的顯示
如果你想控制每個視圖的大標題是否顯示,這需要使用UINavigationItem
的largeTitleDisplayMode
屬性來控制大標題的顯示。該屬性為枚舉類型,定義如下:
typedef NS_ENUM(NSInteger, UINavigationItemLargeTitleDisplayMode)
{
/// 自動模式,會繼承前一個NavigationItem所設置的模式
UINavigationItemLargeTitleDisplayModeAutomatic,
/// 當前 Navigationitem 總是啟用大標題模式
UINavigationItemLargeTitleDisplayModeAlways,
/// 當前 Navigationitem 總是禁用大標題模式
UINavigationItemLargeTitleDisplayModeNever,
}
根據上面的描述,可以在VC初始化init
或者awakeFromNib
方法中設置顯示圖標模式:
self.navigationItem.largeTitleDisplayMode = UINavigationItemLargeTitleDisplayModeAlways;
4.2 控制搜索控制器
iOS 11 中新增了兩個屬性searchController
和hidesSearchBarWhenScrolling
。這兩個屬性主要用于簡化VC對UISearchController
的集成以及視覺優化。其中searchController
屬性用于指定當前VC的一個搜索控制器。而hidesSearchBarWhenScrolling
屬性則用于控制當視圖滾動時是否隱藏搜索欄的UI,當該值為YES時,搜索欄只有在內容視圖(UIScrollView
及其子類)頂部是才會顯示,在滾動過程中會隱藏起來;當該值為NO時,則不受滾動影響一直顯示在導航欄中。具體的代碼實現如下:
- (void)awakeFromNib
{
[super awakeFromNib];
//設置SearchController到navigationItem
self.searchController = [[UISearchController alloc] initWithSearchResultsController:self];
self.navigationItem.searchController = self.searchController;
self.navigationItem.hidesSearchBarWhenScrolling = YES;
}
效果如下圖所示:
5. UIScrollView變化
之前的系統中,如果你的滾動視圖包含在一個導航控制器下,系統會自動地調整你的滾動視圖的contentInset
。而iOS 11新增adjustedContentInset
屬性取替之前contentInset
的處理方式。這兩者之間的關系如下圖所示:
通過一個例子來驗證這說法,代碼如下:
- (void)viewDidLoad
{
[super viewDidLoad];
NSLog(@"viewDidLoad");
NSLog(@"self.tableView.contentInset = %@", NSStringFromUIEdgeInsets(self.tableView.contentInset));
NSLog(@"self.tableView.adjustedContentInset = %@", NSStringFromUIEdgeInsets(self.tableView.adjustedContentInset));
}
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
NSLog(@"viewDidAppear");
NSLog(@"self.tableView.contentInset = %@", NSStringFromUIEdgeInsets(self.tableView.contentInset));
NSLog(@"self.tableView.adjustedContentInset = %@", NSStringFromUIEdgeInsets(self.tableView.adjustedContentInset));
}
執行后輸出下面信息:
2017-09-20 11:54:09.361348+0800 Sample[1276:375286] viewDidLoad
2017-09-20 11:54:09.361432+0800 Sample[1276:375286] self.tableView.contentInset = {0, 0, 0, 0}
2017-09-20 11:54:09.361462+0800 Sample[1276:375286] self.tableView.adjustedContentInset = {0, 0, 0, 0}
2017-09-20 11:54:09.420000+0800 Sample[1276:375286] viewDidAppear
2017-09-20 11:54:09.420378+0800 Sample[1276:375286] self.tableView.contentInset = {0, 0, 0, 0}
2017-09-20 11:54:09.420554+0800 Sample[1276:375286] self.tableView.adjustedContentInset = {20, 0, 0, 0}
可見,tableView的adjustedContentInset
自動改變了,但是contentInset
的值是保持不變的。注:一定要是VC的根視圖為UIScrollView
或者其子類才能夠得到adjustedContentInset
的值,否則獲取到的是空值。而且非根視圖的滾動視圖就會被安全區域所裁剪,看到的樣式如下圖所示:
通過使用contentInsetAdjustmentBehavior
屬性可以控制 adjustedContentInset
的變化。該屬性為枚舉類型,其定義如下:
typedef NS_ENUM(NSInteger, UIScrollViewContentInsetAdjustmentBehavior) {
UIScrollViewContentInsetAdjustmentAutomatic,
UIScrollViewContentInsetAdjustmentScrollableAxes,
UIScrollViewContentInsetAdjustmentNever,
UIScrollViewContentInsetAdjustmentAlways,
}
其中UIScrollViewContentInsetAdjustmentAutomatic
與UIScrollViewContentInsetAdjustmentScrollableAxes
一樣,ScrollView會自動計算和適應頂部和底部的內邊距并且在scrollView 不可滾動時,也會設置內邊距;UIScrollViewContentInsetAdjustmentNever
表示不計算內邊距;UIScrollViewContentInsetAdjustmentAlways
則根據視圖的安全區域來計算內邊距。
如果需要感知adjustedContentInset
的變化,然后根據變化進行不同操作則可以通過重寫新增的adjustedContentInsetDidChange
方法或者實現UIScrollViewDelegate
中的scrollViewDidChangeAdjustedContentInset
方法來實現。如:
//重寫方法
- (void)adjustedContentInsetDidChange
{
[super adjustedContentInsetDidChange];
//執行操作...
}
//實現委托
- (void)scrollViewDidChangeAdjustedContentInset:(UIScrollView *)scrollView
{
//執行操作...
}
除了新增上述所說的邊距相關屬性外,還新增了contentLayoutGuide
和frameLayoutGuide
屬性,用于描述內容布局和整體布局信息。
6. UI主線程操作日志提醒
之前的系統中如果你不小心將UI放入非主線程操作時,Debug日志是沒有任何信息反饋的,導致有時候在排錯時非常困難。在新的Xcode 9中,如果你處于調試狀態,將UI放入非主線程操作,如:
dispatch_async(dispatch_get_global_queue(0, 0), ^{
self.tv = [[UITableView alloc] initWithFrame:self.view.bounds];
[self.view addSubview:self.tv];
NSLog(@"self.tv.adjustedContentInset = %@", NSStringFromUIEdgeInsets(self.tv.adjustedContentInset));
});
Log中會出現下面提示:
=================================================================
Main Thread Checker: UI API called on a background thread: -[UIView bounds]
PID: 16919, TID: 2972321, Thread name: (none), Queue name: com.apple.root.default-qos, QoS: 21
Backtrace:
4 Sample 0x00000001004885dc __29-[ViewController viewDidLoad]_block_invoke + 112
5 libdispatch.dylib 0x000000010077149c _dispatch_call_block_and_release + 24
6 libdispatch.dylib 0x000000010077145c _dispatch_client_callout + 16
7 libdispatch.dylib 0x000000010077d56c _dispatch_queue_override_invoke + 980
8 libdispatch.dylib 0x0000000100782b54 _dispatch_root_queue_drain + 616
9 libdispatch.dylib 0x0000000100782880 _dispatch_worker_thread3 + 136
10 libsystem_pthread.dylib 0x000000018300b130 _pthread_wqthread + 1268
11 libsystem_pthread.dylib 0x000000018300ac30 start_wqthread + 4
從日志中了解到一個Main Thread Checker的東西,根據蘋果官方文檔來看他是作用在AppKit(OSX中)、UIKit還有一些相關API上的后臺線程,主要是用來監控這些框架中的接口是否在主線程中進行調用,如果沒有則發出警告日志。因此,利用這個功能可以讓我們快速地定位那些地方存在問題。
7. 關于UIButton的設置圖片變形問題
在iOS 11中如果調用UIButton
的setImage
或者setBackgrounImage
方法,如果圖片的尺寸大于按鈕尺寸時則會被進行拉伸。如下圖:
對于上面問題,可以通過對按鈕的寬度和高度進行約束來控制圖標的大小。處理代碼如下:
if (@available(iOS 11.0, *))
{
NSLayoutConstraint *constraint = [btn.widthAnchor constraintEqualToConstant:35];
constraint.active = YES;
constraint = [btn.heightAnchor constraintEqualToConstant:35];
constraint.active = YES;
}
調整后,圖標顯示正常:
注:widthAnchor
和heightAnchor
是iOS9之后增加的
持續更新
先寫到這,其他同學可以針對iOS 11的問題進行提問,我會根據實際情況來補充文檔并回答各位的問題。