iOS 13 支持適配的機型
目前最新 iPhone 11、iPhone 11 Pro和iPhone 11 Pro Max
iPhone X、iPhone XR、iPhone XS、iPhone XS Max
iPhone 8、iPhone 8 Plus
iPhone 7、iPhone 7 Plus
iPhone 6s、iPhone 6s Plus
iPhone SE
iPod touch (第七代)
適配要求
Starting April, 2020, all iPhone and iPad apps submitted to the App Store will need to be built with the iOS 13 SDK or later. They must also support the all-screen design of iPhone XS Max or the 12.9-inch iPad Pro (3rd generation), or later.
根據(jù)官網(wǎng)的說法,2020年4月之后所有提交到 App Store 的 iPhone 和 iPad 應(yīng)用必須使用 iOS 13 以上的 SDK 進行編譯,并支持 iPhone Xs Max 或 12.9 寸 iPad Pro (3代) 及以后版本的全屏幕設(shè)計。
1.UIWebview替換成WKWebview
第一步
全局搜索UIWebview ,將相關(guān)代碼刪除或者替換
但是第一步不能檢測到.a 文件或者一些沒有添加到項目目錄中的文件中是否使用UIWebview api 現(xiàn)在就需要第二步
第二步
①從終端cd到要檢查的項目根目錄
②grep -r UIWebView .
以上兩步就可以檢測出項目中的.a文件中是否調(diào)用到UIWebview ,對其進行替換或者刪除
2.私有方法 KVC不讓使用
在 iOS 13 中不再允許使用 valueForKey、setValue:forKey: 等方法獲取或設(shè)置私有屬性,雖然編譯可以通過,但是在運行時會直接崩潰。
// 崩潰 api
UITextField *textField = [searchBar valueForKey:@"_searchField"];
// 替代方案 1,使用 iOS 13 的新屬性 searchTextField
searchBar.searchTextField.placeholder = @"search";
// 替代方案 2,遍歷獲取指定類型的屬性
- (UIView *)findViewWithClassName:(NSString *)className inView:(UIView *)view{
Class specificView = NSClassFromString(className);
if ([view isKindOfClass:specificView]) {
return view;
}
if (view.subviews.count > 0) {
for (UIView *subView in view.subviews) {
UIView *targetView = [self findViewWithClassName:className inView:subView];
if (targetView != nil) {
return targetView;
}
}
}
return nil;
}
// 調(diào)用方法
UITextField *textField = [self findViewWithClassName:@"UITextField" inView:_searchBar];
// 崩潰 api
[searchBar setValue:@"取消" forKey:@"_cancelButtonText"];
// 替代方案,用同上的方法找到子類中 UIButton 類型的屬性,然后設(shè)置其標(biāo)題
UIButton *cancelButton = [self findViewWithClassName:NSStringFromClass([UIButton class]) inView:searchBar];
[cancelButton setTitle:@"取消" forState:UIControlStateNormal];
// 崩潰 api。獲取 _placeholderLabel 不會崩潰,但是獲取 _placeholderLabel 里的屬性就會
[textField setValue:[UIColor blueColor] forKeyPath:@"_placeholderLabel.textColor"];
[textField setValue:[UIFont systemFontOfSize:20] forKeyPath:@"_placeholderLabel.font"];
// 替代方案 1,去掉下劃線,訪問 placeholderLabel
[textField setValue:[UIColor blueColor] forKeyPath:@"placeholderLabel.textColor"];
[textField setValue:[UIFont systemFontOfSize:20] forKeyPath:@"placeholderLabel.font"];
// 替代方案 2
textField.attributedPlaceholder = [[NSAttributedString alloc] initWithString:@"輸入" attributes:@{
NSForegroundColorAttributeName: [UIColor blueColor],
NSFontAttributeName: [UIFont systemFontOfSize:20]
}];
3.使用藍(lán)牙需要權(quán)限申請
CBCentralManager,iOS13以前,使用藍(lán)牙時可以直接用,不會出現(xiàn)權(quán)限提示,iOS13后,再使用就會提示了。 在info.plist里增加
<key>NSBluetoothAlwaysUsageDescription</key>
<string>對使用藍(lán)牙的說明文案</string>
在iOS13中,藍(lán)牙變成了和位置,通知服務(wù)等同樣的可以針對單個app授權(quán)的服務(wù)。
- (NSString*) getWifiSsid {
if (@available(iOS 13.0, *)) {
//用戶明確拒絕,可以彈窗提示用戶到設(shè)置中手動打開權(quán)限
if ([CLLocationManager authorizationStatus] == kCLAuthorizationStatusDenied) {
NSLog(@"User has explicitly denied authorization for this application, or location services are disabled in Settings.");
//使用下面接口可以打開當(dāng)前應(yīng)用的設(shè)置頁面
//[[UIApplication sharedApplication] openURL:[NSURL URLWithString:UIApplicationOpenSettingsURLString]];
return nil;
}
CLLocationManager* cllocation = [[CLLocationManager alloc] init];
if(![CLLocationManager locationServicesEnabled] || [CLLocationManager authorizationStatus] == kCLAuthorizationStatusNotDetermined){
//彈框提示用戶是否開啟位置權(quán)限
[cllocation requestWhenInUseAuthorization];
usleep(50);
//遞歸等待用戶選選擇
return [self getWifiSsidWithCallback:callback];
}
}
NSString *wifiName = nil;
CFArrayRef wifiInterfaces = CNCopySupportedInterfaces();
if (!wifiInterfaces) {
return nil;
}
NSArray *interfaces = (__bridge NSArray *)wifiInterfaces;
for (NSString *interfaceName in interfaces) {
CFDictionaryRef dictRef = CNCopyCurrentNetworkInfo((__bridge CFStringRef)(interfaceName));
if (dictRef) {
NSDictionary *networkInfo = (__bridge NSDictionary *)dictRef;
NSLog(@"network info -> %@", networkInfo);
wifiName = [networkInfo objectForKey:(__bridge NSString *)kCNNetworkInfoKeySSID];
CFRelease(dictRef);
}
}
CFRelease(wifiInterfaces);
return wifiName;
}
4.MPMoviePlayerController 被棄用
在 iOS 9 之前播放視頻可以使用 MediaPlayer.framework 中的MPMoviePlayerController類來完成,它支持本地視頻和網(wǎng)絡(luò)視頻播放。但是在 iOS 9 開始被棄用,如果在 iOS 13 中繼續(xù)使用的話會直接拋出異常:
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'MPMoviePlayerController is no longer available. Use AVPlayerViewController in AVKit.'
解決方案使用 AVFoundation 里的 AVPlayer。
5.推送的 deviceToken 獲取到的格式發(fā)生變化
原本可以直接將 NSData 類型的 deviceToken 轉(zhuǎn)換成 NSString 字符串,然后替換掉多余的符號即
- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken {
NSString *token = [deviceToken description];
for (NSString *symbol in @[@" ", @"<", @">", @"-"]) {
token = [token stringByReplacingOccurrencesOfString:symbol withString:@""];
}
NSLog(@"deviceToken:%@", token);
}
在 iOS 13 中,這種方法已經(jīng)失效,NSData類型的 deviceToken 轉(zhuǎn)換成的字符串變成了:
{length = 32, bytes = 0xd7f9fe34 69be14d1 fa51be22 329ac80d ... 5ad13017 b8ad0736 }
解決方案
需要進行一次數(shù)據(jù)格式處理,參考友盟的做法,可以適配新舊系統(tǒng),獲取方式如下
#include
- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken {
if (![deviceToken isKindOfClass:[NSData class]]) return;
const unsigned *tokenBytes = [deviceToken bytes];
NSString *hexToken = [NSString stringWithFormat:@"xxxxxxxx",
ntohl(tokenBytes[0]), ntohl(tokenBytes[1]), ntohl(tokenBytes[2]),
ntohl(tokenBytes[3]), ntohl(tokenBytes[4]), ntohl(tokenBytes[5]),
ntohl(tokenBytes[6]), ntohl(tokenBytes[7])];
NSLog(@"deviceToken:%@", hexToken);
}
6.UISearchBar 黑線處理導(dǎo)致崩潰
之前為了處理搜索框的黑線問題,通常會遍歷 searchBar 的 subViews,找到并刪除 UISearchBarBackground。
for (UIView *view in _searchBar.subviews.lastObject.subviews) {
if ([view isKindOfClass:NSClassFromString(@"UISearchBarBackground")]) {
[view removeFromSuperview];
break;
}
}
在 iOS13 中這么做會導(dǎo)致 UI 渲染失敗,然后直接崩潰,崩潰信息如下:
*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Missing or detached view for search bar layout'
解決方案
設(shè)置 UISearchBarBackground 的 layer.contents 為 nil:
for (UIView *view in _searchBar.subviews.lastObject.subviews) {
if ([view isKindOfClass:NSClassFromString(@"UISearchBarBackground")]) {
view.layer.contents = nil;
break;
}
}
7.UINavigationBar 設(shè)置按鈕邊距導(dǎo)致崩潰
從 iOS 11 開始,UINavigationBar使用了自動布局,左右兩邊的按鈕到屏幕之間會有 16 或 20 的邊距。
為了避免點擊到間距的空白處沒有響應(yīng),通常做法是:定義一個 UINavigationBar 子類,重寫 layoutSubviews 方法,在此方法里遍歷 subviews 獲取 _UINavigationBarContentView,并將其 layoutMargins 設(shè)置為 UIEdgeInsetsZero。
- (void)layoutSubviews {
[super layoutSubviews];
for (UIView *subview in self.subviews) {
if ([NSStringFromClass([subview class]) containsString:@"_UINavigationBarContentView"]) {
subview.layoutMargins = UIEdgeInsetsZero;
break;
}
}
}
然而,這種做法在 iOS 13 中會導(dǎo)致崩潰,崩潰信息如下:
*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Client error attempting to change layout margins of a private view'
解決方案
使用設(shè)置 frame 的方式,讓 _UINavigationBarContentView 向兩邊伸展,從而抵消兩邊的邊距。
- (void)layoutSubviews {
[super layoutSubviews];
for (UIView *subview in self.subviews) {
if ([NSStringFromClass([subview class]) containsString:@"_UINavigationBarContentView"]) {
if ([UIDevice currentDevice].systemVersion.floatValue >= 13.0) {
UIEdgeInsets margins = subview.layoutMargins;
subview.frame = CGRectMake(-margins.left, -margins.top, margins.left + margins.right + subview.frame.size.width, margins.top + margins.bottom + subview.frame.size.height);
} else {
subview.layoutMargins = UIEdgeInsetsZero;
}
break;
}
}
}
8.使用 UISearchDisplayController 導(dǎo)致崩潰
在 iOS 8 之前,我們在 UITableView 上添加搜索框需要使用 UISearchBar + UISearchDisplayController 的組合方式,而在 iOS 8 之后,蘋果就已經(jīng)推出了 UISearchController 來代替這個組合方式。在 iOS 13 中,如果還繼續(xù)使用 UISearchDisplayController 會直接導(dǎo)致崩潰,崩潰信息如下:
*** Terminating app due to uncaught exception 'NSGenericException', reason: 'UISearchDisplayController is no longer supported when linking against this version of iOS. Please migrate your application to UISearchController.'
解決方案
使用 UISearchController 替換 UISearchBar + UISearchDisplayController 的組合方案。
9.Xcode 11 創(chuàng)建的工程在低版本設(shè)備上運行黑屏
使用 Xcode 11 創(chuàng)建的工程,運行設(shè)備選擇 iOS 13.0 以下的設(shè)備,運行應(yīng)用時會出現(xiàn)黑屏。這是因為 Xcode 11 默認(rèn)是會創(chuàng)建通過 UIScene 管理多個 UIWindow 的應(yīng)用,工程中除了 AppDelegate 外會多一個 SceneDelegate:
這是為了 iPadOS 的多進程準(zhǔn)備的,也就是說 UIWindow 不再是 UIApplication 中管理,但是舊版本根本沒有 UIScene。
解決方案
在 AppDelegate 的頭文件加上:
@property (strong, nonatomic) UIWindow *window;
10.使用 @available 導(dǎo)致舊版本 Xcode 編譯出錯。
在 Xcode 11 的 SDK 工程的代碼里面使用了 @available 判斷當(dāng)前系統(tǒng)版本,打出來的包放在 Xcode 10 中編譯,會出現(xiàn)一下錯誤:
Undefine symbols for architecture i386:
"__isPlatformVersionAtLeast", referenced from:
...
ld: symbol(s) not found for architecture i386
從錯誤信息來看,是 __isPlatformVersionAtLeast 方法沒有具體的實現(xiàn),但是工程里根本沒有這個方法。實際測試無論在哪里使用@available ,并使用 Xcode 11 打包成動態(tài)庫或靜態(tài)庫,把打包的庫添加到 Xcode 10 中編譯都會出現(xiàn)這個錯誤,因此可以判斷是 iOS 13 的 @available 的實現(xiàn)中使用了新的 api。
解決方案
如果你的 SDK 需要適配舊版本的 Xcode,那么需要避開此方法,通過獲取系統(tǒng)版本來進行判斷:
if ([UIDevice currentDevice].systemVersion.floatValue >= 13.0) {
...
}
另外,在 Xcode 10 上打開 SDK 工程也應(yīng)該可以正常編譯,這就需要加上編譯宏進行處理
#ifndef __IPHONE_13_0
#define __IPHONE_13_0 130000
#endif
#if __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_13_0
...
#endif
11.暗黑模式
Dark Mode iOS 13 推出暗黑模式,UIKit 提供新的系統(tǒng)顏色和 api 來適配不同顏色模式,xcassets 對素材適配也做了調(diào)整,具體適配可見: Implementing Dark Mode on iOS。 如果不打算適配 Dark Mode,可以直接在 Info.plist 中添加一欄:User Interface Style : Light,即可在應(yīng)用內(nèi)禁用暗黑模式。不過即使設(shè)置了顏色方案,申請權(quán)限的系統(tǒng)彈窗還是會依據(jù)系統(tǒng)的顏色進行顯示,自己創(chuàng)建的 UIAlertController 就不會
12.UISegmentedControl 默認(rèn)樣式改變
默認(rèn)樣式變?yōu)榘椎缀谧郑绻O(shè)置修改過顏色的話,頁面需要修改。
原本設(shè)置選中顏色的 tintColor 已經(jīng)失效,新增了 selectedSegmentTintColor 屬性用以修改選中的顏色。
Web Content適配
13.textfield.leftview
如下方式,直接給 textfield.leftView 賦值一個 UILabel 對象,他的寬高會被 sizeToFit,而不是創(chuàng)建時的值。
// left view label
UILabel *phoneLabel = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, 63, 50)];
phoneLabel.text = @"手機號";
phoneLabel.font = [UIFont systemFontOfSize:16];
// set textfield left view
self.textfieldName.leftView = phoneLabel;
實際leftview的width為59,height為19
解決方法:嵌套一個UIView
// label
UILabel *phoneLabel = [[UILabel alloc] init];
phoneLabel.text = @"手機號";
phoneLabel.font = [UIFont systemFontOfSize:16];
[phoneLabel sizeToFit];
phoneLabel.centerY = 50/2.f;
// left view
UIView *leftView = [[UIView alloc] initWithFrame:(CGRect){0, 0, 63, 50}];
[leftView addSubview:phoneLabel];
// set textfield left view
self.textfieldName.leftView = leftView;
14.NSAttributedString優(yōu)化
對于UILabel、UITextField、UITextView,在設(shè)置NSAttributedString時也要考慮適配Dark Mode,否則在切換模式時會與背景色融合,造成不好的體驗
不建議的做法
NSDictionary *dic = @{NSFontAttributeName:[UIFont systemFontOfSize:16]};
NSAttributedString *str = [[NSAttributedString alloc] initWithString:@"富文本文案" attributes:dic];
推薦的做法
// 添加一個NSForegroundColorAttributeName屬性
NSDictionary *dic = @{NSFontAttributeName:[UIFont systemFontOfSize:16],NSForegroundColorAttributeName:[UIColor labelColor]};
NSAttributedString *str = [[NSAttributedString alloc] initWithString:@"富文本文案" attributes:dic];
15.TabBar紅點偏移
如果之前有通過TabBar上圖片位置來設(shè)置紅點位置,在iOS13上會發(fā)現(xiàn)顯示位置都在最左邊去了。遍歷UITabBarButton的subViews發(fā)現(xiàn)只有在TabBar選中狀態(tài)下才能取到UITabBarSwappableImageView,解決辦法是修改為通過UITabBarButton的位置來設(shè)置紅點的frame
16.WKWebView 中測量頁面內(nèi)容高度的方式變更
iOS 13以前 document.body.scrollHeight
iOS 13中 document.documentElement.scrollHeight 兩者相差55 應(yīng)該是瀏覽器定義高度變了
17.StatusBar 與之前版本不同
之前 Status Bar 有兩種狀態(tài),default 和 lightContent
現(xiàn)在 Status Bar 有三種狀態(tài),default, darkContent 和 lightContent
現(xiàn)在的 darkContent 對應(yīng)之前的 default,現(xiàn)在的 default 會根據(jù)情況自動選擇 darkContent 和 lightContent
18. UIActivityIndicatorView
之前的 UIActivityIndicatorView 有三種 style 分別為 whiteLarge, white 和 gray,現(xiàn)在全部廢棄。
增加兩種 style 分別為 medium 和 large,指示器顏色用 color 屬性修改。
19.使用MJExtension 中處理NSNull的不同
這個直接會導(dǎo)致Crash的在將服務(wù)端數(shù)據(jù)字典轉(zhuǎn)換為模型時,如果遇到服務(wù)端給的數(shù)據(jù)為NSNull時, mj_JSONObject,其中 class_copyPropertyList方法得到的屬性里,多了一種EFSQLBinding類型的東西,而且屬性數(shù)量也不準(zhǔn)確, 這個組件沒有更新的情況下,寫了一個方法swizzling掉把當(dāng)遇到 NSNull時,直接轉(zhuǎn)為nil了。
其實目前我們項目在ios13下面還沒遇到這種情況,發(fā)一個之前項目處理null的方法,在項目里面寫一個NSObject的分類,添加下面的方法就可以了。大家請根據(jù)項目情況、數(shù)據(jù)格式修改下這個方法,MJ庫里面自己會進行替換的:
- (id)mj_newValueFromOldValue:(id)oldValue property:(MJProperty *)property {
//為了解決json字符串先賦值給oc字典后,類型轉(zhuǎn)換crash問題,如:
//json->oldValue:0
//model中值為NSString類型
//如果先將json轉(zhuǎn)為dic,dic中對應(yīng)value值為NSNumber類型,則向oldValue發(fā)送isEqualToString消息會crash
id tempValue = oldValue;
if ([property.type.code isEqualToString:@"NSString"]) {
tempValue = [NSString stringWithFormat:@"%@", tempValue];
if ([tempValue isKindOfClass:[NSNull class]] || tempValue == nil || [tempValue isEqual:[NSNull null]] || [tempValue isEqualToString:@"(null)"] || [tempValue isEqualToString:@"(\n)"] ) {
return @"";
}
}
if ([property.type.code isEqualToString:@"NSNumber"]) {
// tempValue = [NSNumber numberWithFloat:[tempValue floatValue]];
if ([tempValue isKindOfClass:[NSNull class]] || tempValue == nil || [tempValue isEqual:[NSNull null]] || [tempValue isEqualToString:@"(null)"] || [tempValue isEqualToString:@"(\n)"] ) {
return @0;
}
}
return tempValue;
}
20.Sign In with Apple
在 iOS 13 中蘋果推出一種在 App 和網(wǎng)站上快速、便捷登錄的方式: Sign In With Apple。這是 iOS 13 新增的功能,因此需要使用 Xcode 11 進行開發(fā)。關(guān)于應(yīng)用是否要求接入此登錄方式,蘋果在 App Store 應(yīng)用審核指南 中提到:
Apps that exclusively use a third-party or social login service (such as Facebook Login, Google Sign-In, Sign in with Twitter, Sign In with LinkedIn, Login with Amazon, or WeChat Login) to set up or authenticate the user’s primary account with the app must also offer Sign in with Apple as an equivalent option.
如果你的應(yīng)用使用了第三方或社交賬號登錄服務(wù)(如Facebook、Google、Twitter、LinkedIn、Amazon、微信等)來設(shè)置或驗證用戶的主賬號,就必須把 Sign In With Apple 作為同等的選項添加到應(yīng)用上。如果是下面這些類型的應(yīng)用則不需要添加:
- 僅僅使用公司內(nèi)部賬號來注冊和登錄的應(yīng)用;
- 要求用戶使用現(xiàn)有的教育或企業(yè)賬號進行登錄的教育、企業(yè)或商務(wù)類型的應(yīng)用;
- 使用政府或業(yè)界支持的公民身份識別系統(tǒng)或電子標(biāo)識對用戶進行身份驗證的應(yīng)用;
- 特定第三方服務(wù)的應(yīng)用,用戶需要直接登錄其郵箱、社交媒體或其他第三方帳戶才能訪問其內(nèi)容。
另外需要注意,關(guān)于何時要求接入Sign In With Apple
,蘋果在 News and Updates 中提到:
Starting today, new apps submitted to the App Store must follow these guidelines. Existing apps and app updates must follow them by April 2020.
2019 年 9 月 12 日 起,提交到 App Store 的新應(yīng)用必須按照應(yīng)用審核指南中的標(biāo)準(zhǔn)進行接入;現(xiàn)有應(yīng)用和應(yīng)用更新必須也在 2020 年 4 月前完成接入。
21.LaunchImage 被棄用
修改 App 啟動畫面時我們需要通過 Launch Images 進行修改。
在 iOS 8 蘋果引入了 LaunchScreen.storyboard,支持界面布局用的 AutoLayout + SizeClass。雖然兩種方式都可以正常工作,但是蘋果更希望開發(fā)者們能夠使用 LaunchScreen.storyboard 進行操作。
由于 Launch Images 需要在對應(yīng)的 aseets 里面放入所有尺寸的 Launch Images ,這樣每當(dāng)出現(xiàn)一個屏幕尺寸不同的設(shè)備時,都需要做出對應(yīng)的修改,操作復(fù)雜。
因此在 2020 年 4 月之后,所有 App 都必須使用 LaunchScreen.storyboard 的方式操作啟動畫面,否則將無法提交到 App Store 進行審批。
解決方法
創(chuàng)建LaunchScreen.storyboard,拖入一個UIImageView,并設(shè)置約束為上下左右都為0,在info plist文件中添加Launch screen interface file base name:Launch Screen,在General->App Icons and Launch Images->Launch Screen File 選擇為LaunchScreen,清理項目并刪除已安裝的app