iOS 13 適配要點總結

_森宇_· 2019-10-11
本文來自 掘金 ,作者 _森宇_

iOS 13 支持適配的機型

  • 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.

根據官網的說法,2020年4月之后所有提交到 App Store 的 iPhone 和 iPad 應用必須使用 iOS 13 以上的 SDK 進行編譯,并支持 iPhone Xs Max 或 12.9 寸 iPad Pro (3代) 及以后版本的全屏幕設計。

新特性適配

1. Dark Mode

iOS 13 推出暗黑模式,UIKit 提供新的系統顏色和 api 來適配不同顏色模式,xcassets 對素材適配也做了調整,具體適配可見: Implementing Dark Mode on iOS

如果不打算適配 Dark Mode,可以直接在 Info.plist 中添加一欄:User Interface Style : Light,即可在應用內禁用暗黑模式。不過即使設置了顏色方案,申請權限的系統彈窗還是會依據系統的顏色進行顯示,自己創建的 UIAlertController 就不會。

2. Sign In with Apple

在 iOS 13 中蘋果推出一種在 App 和網站上快速、便捷登錄的方式: Sign In With Apple。這是 iOS 13 新增的功能,因此需要使用 Xcode 11 進行開發。關于應用是否要求接入此登錄方式,蘋果在 App Store 應用審核指南 中提到:

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.

如果你的應用使用了第三方或社交賬號登錄服務(如Facebook、Google、Twitter、LinkedIn、Amazon、微信等)來設置或驗證用戶的主賬號,就必須把 Sign In With Apple 作為同等的選項添加到應用上。如果是下面這些類型的應用則不需要添加:

  • 僅僅使用公司內部賬號來注冊和登錄的應用;

  • 要求用戶使用現有的教育或企業賬號進行登錄的教育、企業或商務類型的應用;

  • 使用政府或業界支持的公民身份識別系統或電子標識對用戶進行身份驗證的應用;

  • 特定第三方服務的應用,用戶需要直接登錄其郵箱、社交媒體或其他第三方帳戶才能訪問其內容。

另外需要注意,關于何時要求接入 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 的新應用必須按照應用審核指南中的標準進行接入;現有應用和應用更新必須也在 2020 年 4 月前完成接入。

API 適配

1. 私有方法 KVC 可能導致崩潰

在 iOS 13 中部分方法屬性不允許使用 valueForKeysetValue:forKey:  來獲取或者設置私有屬性,具體表現為在運行時會直接崩潰,并提示以下崩潰信息:

*** Terminating app due to uncaught exception 'NSGenericException', reason: 'Access to UISearchBar's _searchField ivar is prohibited. This is an application bug'

目前整理的會導致崩潰的私有 api 和對應替代方案如下,感謝 @君賞 的反饋,也歡迎各位大佬補充和指正 :

// 崩潰 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;
}

// 調用方法
 UITextField *textField = [self findViewWithClassName:@"UITextField" inView:_searchBar];

// 崩潰 api
[searchBar setValue:@"取消" forKey:@"_cancelButtonText"];

// 替代方案,用同上的方法找到子類中 UIButton 類型的屬性,然后設置其標題
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]
}];

2. 推送的 deviceToken 獲取到的格式發生變化

原本可以直接將 NSData 類型的 deviceToken 轉換成 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 中,這種方法已經失效,NSData類型的 deviceToken 轉換成的字符串變成了:

{length = 32, bytes = 0xd7f9fe34 69be14d1 fa51be22 329ac80d ... 5ad13017 b8ad0736 }

解決方案

需要進行一次數據格式處理,參考友盟的做法,可以適配新舊系統,獲取方式如下:

#include <arpa/inet.h>
- (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);
}

3. 模態彈出默認樣式改變

在 iOS 13,使用 presentViewController 方式打開視圖,默認的如下圖所示的視差效果,通過下滑返回。

16d66db07ffd0894.gif

這是因為蘋果將 UIViewControllermodalPresentationStyle 屬性的默認值改成了新加的一個枚舉值  UIModalPresentationAutomatic,對于多數 UIViewController,此值會映射成 UIModalPresentationPageSheet

需要注意,這種效果彈出來的頁面導航欄部分是會被砍掉的,在 storyboard 中也可以看到,頁面布局時需要注意導航欄的內容不要被遮擋。

image.png

還有一點注意的是,我們原來以全屏的樣式彈出一個頁面,那么將這個頁面彈出的那個 ViewController 會依次調用 viewWillDisappearviewDidDisappear。然后在這個頁面被 dismiss 的時候,將他彈出的那個 ViewController 的 viewWillAppearviewDidAppear 會被依次調用。然而使用默認的視差效果彈出頁面,將他彈出的那個 ViewController 并不會調用這些方法,原先寫在這四個函數中的代碼以后都有可能會存在問題。

解決方案

如果視差效果的樣式可以接受的話,就不需要修改;如果需要改回全屏顯示的界面,需要手動設置彈出樣式:

- (UIModalPresentationStyle)modalPresentationStyle {
    return UIModalPresentationFullScreen;
}

4. UISearchBar 黑線處理導致崩潰

之前為了處理搜索框的黑線問題,通常會遍歷 searchBar 的 subViews,找到并刪除 UISearchBarBackground

for (UIView *view in _searchBar.subviews.lastObject.subviews) {
    if ([view isKindOfClass:NSClassFromString(@"UISearchBarBackground")]) {
        [view removeFromSuperview];
        break;
    }
}

在 iOS13 中這么做會導致 UI 渲染失敗,然后直接崩潰,崩潰信息如下:

*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Missing or detached view for search bar layout'

解決方案

設置 UISearchBarBackgroundlayer.contentsnil:

for (UIView *view in _searchBar.subviews.lastObject.subviews) {
    if ([view isKindOfClass:NSClassFromString(@"UISearchBarBackground")]) {
        view.layer.contents = nil;
        break;
    }
}

5. UITabBarButton 不同狀態下結構不同

在 iOS 13 中,UITabBarButton 的控件結構會隨著其選中狀態的變化而變化,主要體現為 UITabBarSwappableImageViewUITabBarButtonLabel 的位置變化。在選中時和以前一樣,是 UITabBarButton 的子控件。而在未選中狀態下放到了 UIVisualEffectView_UIVisualEffectContentView 里面。感謝@關燈俠的提醒,具體可以看下圖的對比:

image.png

我們在自定義 UITabBar 時,通常會遍歷 UITabBarButton 的子控件獲取 UITabBarSwappableImageView,比如自定義紅點時添加到這個 ImageView 的右上角,這在 iOS 13 中可能就會導致異常。

解決方案

可以使用遞歸遍歷 UITabBarButton 的所有 subviews 獲取 UITabBarSwappableImageView,具體可以參照上面 私有方法 KVC 可能導致崩潰 章節中給出的遞歸遍歷方法。

另外需要注意,未選中狀態下,添加的紅點會和 tabBar 的圖片一樣變成灰色,這一點應該也是因為其結構變化造成的。具體可以見下圖:

image.png

如果想要和以前一樣未選中時也是紅色,也很簡單,把紅點添加到 UITabBarButton 上,位置再根據 UITabBarSwappableImageView 調整即可。

6. UINavigationBar 設置按鈕邊距導致崩潰

從 iOS 11 開始,UINavigationBar 使用了自動布局,左右兩邊的按鈕到屏幕之間會有 16 或 20 的邊距。

image.png
為了避免點擊到間距的空白處沒有響應,通常做法是:定義一個 UINavigationBar 子類,重寫 layoutSubviews 方法,在此方法里遍歷 subviews 獲取 _UINavigationBarContentView,并將其 layoutMargins 設置為 UIEdgeInsetsZero

- (void)layoutSubviews {
    [super layoutSubviews];
    
    for (UIView *subview in self.subviews) {
        if ([NSStringFromClass([subview class]) containsString:@"_UINavigationBarContentView"]) {
            subview.layoutMargins = UIEdgeInsetsZero;
            break;
        }
    }
}

然而,這種做法在 iOS 13 中會導致崩潰,崩潰信息如下:

*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Client error attempting to change layout margins of a private view'

解決方案

使用設置 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;
        }
    }
}

方法棄用

1. UIWebView 將被禁止提交審核

在 iOS 13 推出后,蘋果在 UIWebView 的說明上將其支持的系統范圍定格在了 iOS 2 ~ iOS 12。目前,如果開發者將包含 UIWebView api 的應用更新上傳到 App Store 審核后,其將會收到包含 ITMS-90809 信息的回復郵件,提示你在下一次提交時將應用中 UIWebView 的 api 移除。

Dear Developer,

We identified one or more issues with a recent delivery for your app, "xxx". Your delivery was successful, but you may wish to correct the following issues in your next delivery:

ITMS-90809: Deprecated API Usage - Apple will stop accepting submissions of apps that use UIWebView APIs . See developer.apple.com/documentati… for more information.

After you’ve corrected the issues, you can use Xcode or Application Loader to upload a new binary to App Store Connect.

Best regards,

The App Store Team

解決方案

WKWebView 替代 UIWebView,確保所有 UIWebView 的 api 都要移除,如果需要適配 iOS 7 的可以通過 openURL 的方式在 Safari 打開。

2. 使用 UISearchDisplayController 導致崩潰

在 iOS 8 之前,我們在 UITableView 上添加搜索框需要使用 UISearchBar + UISearchDisplayController 的組合方式,而在 iOS 8 之后,蘋果就已經推出了 UISearchController 來代替這個組合方式。在 iOS 13 中,如果還繼續使用 UISearchDisplayController 會直接導致崩潰,崩潰信息如下:

*** 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 的組合方案。

3. MPMoviePlayerController 被棄用

在 iOS 9 之前播放視頻可以使用 MediaPlayer.framework 中的MPMoviePlayerController類來完成,它支持本地視頻和網絡視頻播放。但是在 iOS 9 開始被棄用,如果在 iOS 13 中繼續使用的話會直接拋出異常:

*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'MPMoviePlayerController is no longer available. Use AVPlayerViewController in AVKit.'

解決方案

使用 AVFoundation 里的 AVPlayer 作為視頻播放控件。

工程適配

1. 藍牙權限字段更新導致崩潰以及提交審核失敗

在 iOS 13 中,蘋果將原來藍牙申請權限用的 NSBluetoothPeripheralUsageDescription 字段,替換為 NSBluetoothAlwaysUsageDescription 字段。

For apps with a deployment target of iOS 13 and later, use NSBluetoothAlwaysUsageDescription instead.

感謝 @dengChaoJie 的反饋,如果在 iOS 13 中使用舊的權限字段獲取藍牙權限,會導致崩潰,崩潰信息如下:

This app has crashed because it attempted to access privacy-sensitive data without a usage description.  The app's Info.plist must contain an NSBluetoothAlwaysUsageDescription key with a string value explaining to the user how the app uses this data.

另外,如果將沒有新字段的包提交審核,將會收到包含 ITMS-90683 的郵件,并提示審核不通過。

Dear Developer,

We identified one or more issues with a recent delivery for your app, "xxx". Please correct the following issues, then upload again.

ITMS-90683: Missing Purpose String in Info.plist - Your app's code references one or more APIs that access sensitive user data. The app's Info.plist file should contain a NSBluetoothAlwaysUsageDescription key with a user-facing purpose string explaining clearly and completely why your app needs the data. Starting Spring 2019, all apps submitted to the App Store that access user data are required to include a purpose string. If you're using external libraries or SDKs, they may reference APIs that require a purpose string. While your app might not use these APIs, a purpose string is still required. You can contact the developer of the library or SDK and request they release a version of their code that doesn't contain the APIs. Learn more (developer.apple.com/documentati…).

Best regards,

The App Store Team

解決方案

官網文檔也有說明,就是在 Info.plist 中把兩個字段都加上。

For deployment targets earlier than iOS 13, add both NSBluetoothAlwaysUsageDescription and NSBluetoothPeripheralUsageDescription to your app’s Information Property List file.

2. CNCopyCurrentNetworkInfo 使用要求更嚴格

從 iOS 12 開始,CNCopyCurrentNetworkInfo 函數需要開啟 Access WiFi Information 的功能后才會返回正確的值。在 iOS 13 中,這個函數的使用要求變得更嚴格,根據 CNCopyCurrentNetworkInfo 文檔說明,應用還需要符合下列三項條件中的至少一項才能得到正確的值:

蘋果作出這項改變主要為了保障用戶的安全,因為根據 MAC 地址容易推算出用戶當前所處的地理位置。同樣,藍牙設備也具有 MAC 地址,所以蘋果也為藍牙添加了新的權限,可見上一點。

解決方案

根據應用需求,添加三項要求其中一項。可以選擇第一項獲取定位權限,因為添加的成本不會太大,只需要用戶允許應用使用定位服務即可。

3. LaunchImage 被棄用

iOS 8 之前我們是在LaunchImage 來設置啟動圖,每當蘋果推出新的屏幕尺寸的設備,我們需要 assets 里面放入對應的尺寸的啟動圖,這是非常繁瑣的一個步驟。因此在 iOS 8 蘋果引入了 LaunchScreen,可以直接在 Storyboard 上設置啟動界面樣式,可以很方便適配各種屏幕。

需要注意的是,蘋果在 Modernizing Your UI for iOS 13   section 中提到 ,從2020年4月開始,所有支持 iOS 13 的 App 必須提供 LaunchScreen.storyboard,否則將無法提交到 App Store 進行審批。

image.png

解決方案

使用 LaunchScreen.storyboard 設置啟動頁,棄用 LaunchImage

4. UISegmentedControl 默認樣式改變

默認樣式變為白底黑字,如果設置修改過顏色的話,頁面需要修改。

image.png

原本設置選中顏色的 tintColor 已經失效,新增了  selectedSegmentTintColor 屬性用以修改選中的顏色。

5. Xcode 11 創建的工程在低版本設備上運行黑屏

使用 Xcode 11 創建的工程,運行設備選擇 iOS 13.0 以下的設備,運行應用時會出現黑屏。這是因為 Xcode 11 默認是會創建通過 UIScene 管理多個 UIWindow 的應用,工程中除了 AppDelegate 外會多一個 SceneDelegate

image.png

這是為了 iPadOS 的多進程準備的,也就是說 UIWindow 不再是 UIApplication 中管理,但是舊版本根本沒有 UIScene

解決方案

AppDelegate 的頭文件加上:

@property (strong, nonatomic) UIWindow *window;

SDK 適配

1.  使用 @available 導致舊版本 Xcode 編譯出錯。

在 Xcode 11 的 SDK 工程的代碼里面使用了 @available 判斷當前系統版本,打出來的包放在 Xcode 10 中編譯,會出現一下錯誤:

Undefine symbols for architecture i386:
    "__isPlatformVersionAtLeast", referenced from:
        ...
ld: symbol(s) not found for architecture i386

從錯誤信息來看,是 __isPlatformVersionAtLeast 方法沒有具體的實現,但是工程里根本沒有這個方法。實際測試無論在哪里使用@available ,并使用 Xcode 11 打包成動態庫或靜態庫,把打包的庫添加到 Xcode 10 中編譯都會出現這個錯誤,因此可以判斷是 iOS 13 的 @available 的實現中使用了新的 api。

解決方案

如果你的 SDK 需要適配舊版本的 Xcode,那么需要避開此方法,通過獲取系統版本來進行判斷:

if ([UIDevice currentDevice].systemVersion.floatValue >= 13.0) {
    ...
}

另外,在 Xcode 10 上打開 SDK 工程也應該可以正常編譯,這就需要加上編譯宏進行處理:

#ifndef __IPHONE_13_0
#define __IPHONE_13_0 130000
#endif

#if __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_13_0
...
#endif

參考文章

本文結合個人遇到的問題和以下文章部分內容,對常見適配問題進行總結

奔驰宝马机漏洞玩法