iOS13 适配

1,864 阅读19分钟

此文章是整合,非原创下方有原创链接。

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 月前完成接入。

解决方法开发者网站 在 Sign in with Apple 开启功能

开启Sign in With apple
Xcode 里面 Signing & Capabilities 开启 Sign in with Apple 功能
Xcode 设置

//利用 ASAuthorizationAppleIDButton
ASAuthorizationAppleIDButton *button = [ASAuthorizationAppleIDButton buttonWithType:ASAuthorizationAppleIDButtonTypeSignIn style:ASAuthorizationAppleIDButtonStyleWhite];
[button addTarget:self action:@selector(signInWithApple) forControlEvents:UIControlEventTouchUpInside];
button.center = self.view.center;
button.bounds = CGRectMake(0, 0, 40, 40); // 宽度过小就没有文字了,只剩图标
[self.view addSubview:button];

ASAuthorizationControllerPresentationContextProviding 就一个方法,主要是告诉 ASAuthorizationController 展示在哪个 window 上
#pragma mark - ASAuthorizationControllerPresentationContextProviding
- (ASPresentationAnchor)presentationAnchorForAuthorizationController:(ASAuthorizationController *)controller API_AVAILABLE(ios(13.0))
{
    return self.view.window;
}

Authorization 发起授权登录请求
- (void)signInWithApple API_AVAILABLE(ios(13.0)) {
    ASAuthorizationAppleIDProvider *provider = [[ASAuthorizationAppleIDProvider alloc] init];
    ASAuthorizationAppleIDRequest *request = [provider createRequest];
    request.requestedScopes = @[ASAuthorizationScopeFullName, ASAuthorizationScopeEmail];
    
    ASAuthorizationController *vc = [[ASAuthorizationController alloc] initWithAuthorizationRequests:@[request]];
    vc.delegate = self;
    vc.presentationContextProvider = self;
    [vc performRequests];
}
/*ASAuthorizationAppleIDProvider 这个类比较简单,头文件中可以看出,主要用于创建一个 ASAuthorizationAppleIDRequest 以及获取对应 userID 的用户授权状态。在上面的方法中我们主要是用于创建一个 ASAuthorizationAppleIDRequest ,用户授权状态的获取后面会提到。
给创建的 request 设置 requestedScopes ,这是个 ASAuthorizationScope 数组,目前只有两个值,ASAuthorizationScopeFullName 和 ASAuthorizationScopeEmail ,根据需求去设置即可。
然后,创建 ASAuthorizationController ,它是管理授权请求的控制器,给其设置 delegate 和 presentationContextProvider ,最后启动授权 performRequests*/

//回调处理
#pragma mark - ASAuthorizationControllerDelegate
- (void)authorizationController:(ASAuthorizationController *)controller didCompleteWithAuthorization:(ASAuthorization *)authorization API_AVAILABLE(ios(13.0))
{
    if ([authorization.credential isKindOfClass:[ASAuthorizationAppleIDCredential class]])       {
        ASAuthorizationAppleIDCredential *credential = authorization.credential;
        
        NSString *state = credential.state;
        NSString *userID = credential.user;
        NSPersonNameComponents *fullName = credential.fullName;
        NSString *email = credential.email;
        NSString *authorizationCode = [[NSString alloc] initWithData:credential.authorizationCode encoding:NSUTF8StringEncoding]; // refresh token
        NSString *identityToken = [[NSString alloc] initWithData:credential.identityToken encoding:NSUTF8StringEncoding]; // access token
        ASUserDetectionStatus realUserStatus = credential.realUserStatus;
        
        NSLog(@"state: %@", state);
        NSLog(@"userID: %@", userID);
        NSLog(@"fullName: %@", fullName);
        NSLog(@"email: %@", email);
        NSLog(@"authorizationCode: %@", authorizationCode);
        NSLog(@"identityToken: %@", identityToken);
        NSLog(@"realUserStatus: %@", @(realUserStatus));
    }
}

- (void)authorizationController:(ASAuthorizationController *)controller didCompleteWithError:(NSError *)error API_AVAILABLE(ios(13.0))
{
    NSString *errorMsg = nil;
    switch (error.code) {
        case ASAuthorizationErrorCanceled:
            errorMsg = @"用户取消了授权请求";
            break;
        case ASAuthorizationErrorFailed:
            errorMsg = @"授权请求失败";
            break;
        case ASAuthorizationErrorInvalidResponse:
            errorMsg = @"授权请求响应无效";
            break;
        case ASAuthorizationErrorNotHandled:
            errorMsg = @"未能处理授权请求";
            break;
        case ASAuthorizationErrorUnknown:
            errorMsg = @"授权请求失败未知原因";
            break;
    }
    NSLog(@"%@", errorMsg);
}
/*
User ID: Unique, stable, team-scoped user ID,苹果用户唯一标识符,该值在同一个开发者账号下的所有 App 下是一样的,开发者可以用该唯一标识符与自己后台系统的账号体系绑定起来。
Verification data: Identity token, code,验证数据,用于传给开发者后台服务器,然后开发者服务器再向苹果的身份验证服务端验证本次授权登录请求数据的有效性和真实性,详见 Sign In with Apple REST API。如果验证成功,可以根据 userIdentifier 判断账号是否已存在,若存在,则返回自己账号系统的登录态,若不存在,则创建一个新的账号,并返回对应的登录态给 App。
Account information: Name, verified email,苹果用户信息,包括全名、邮箱等。
Real user indicator: High confidence indicator that likely real user,用于判断当前登录的苹果账号是否是一个真实用户,取值有:unsupported、unknown、likelyReal。
失败情况会走 authorizationController:didCompleteWithError: 这个方法,具体看代码吧
*/

other

用户终止 App 中使用 Sign in with Apple 功能 用户在设置里注销了 AppleId

App 需要获取到这些状态,然后做退出登录操作,或者重新登录。 我们需要在 App 启动的时候,通过 getCredentialState:completion: 来获取当前用户的授权状态

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    
    if (@available(iOS 13.0, *)) {
        NSString *userIdentifier = 钥匙串中取出的 userIdentifier;
        if (userIdentifier) {
            ASAuthorizationAppleIDProvider *appleIDProvider = [ASAuthorizationAppleIDProvider new];
            [appleIDProvider getCredentialStateForUserID:userIdentifier
                                              completion:^(ASAuthorizationAppleIDProviderCredentialState credentialState,
                                                           NSError * _Nullable error)
            {
                switch (credentialState) {
                    case ASAuthorizationAppleIDProviderCredentialAuthorized:
                        // The Apple ID credential is valid
                        break;
                    case ASAuthorizationAppleIDProviderCredentialRevoked:
                        // Apple ID Credential revoked, handle unlink
                        break;
                    case ASAuthorizationAppleIDProviderCredentialNotFound:
                        // Credential not found, show login UI
                        break;
                }
            }];
        }
    }
    return YES;
}
/*ASAuthorizationAppleIDProviderCredentialState 解析如下:
ASAuthorizationAppleIDProviderCredentialAuthorized 授权状态有效;
ASAuthorizationAppleIDProviderCredentialRevoked 上次使用苹果账号登录的凭据已被移除,需解除绑定并重新引导用户使用苹果登录;
ASAuthorizationAppleIDProviderCredentialNotFound 未登录授权,直接弹出登录页面,引导用户登录
*/

通过通知方法来监听 revoked 状态,可以添加 ASAuthorizationAppleIDProviderCredentialRevokedNotification 这个通知

- (void)observeAppleSignInState {
    if (@available(iOS 13.0, *)) {
        [[NSNotificationCenter defaultCenter] addObserver:self
                                                 selector:@selector(handleSignInWithAppleStateChanged:)
                                                     name:ASAuthorizationAppleIDProviderCredentialRevokedNotification
                                                   object:nil];
    }
}

- (void)handleSignInWithAppleStateChanged:(NSNotification *)notification {
    // Sign the user out, optionally guide them to sign in again
    NSLog(@"%@", notification.userInfo);
}

苹果还把 iCloud KeyChain password 集成到了这套 API 里,我们在使用的时候,只需要在创建 request 的时候,多创建一个 ASAuthorizationPasswordRequest ,这样如果 KeyChain 里面也有登录信息的话,可以直接使用里面保存的用户名和密码进行登录。代码如下

- (void)perfomExistingAccountSetupFlows API_AVAILABLE(ios(13.0))
{
    ASAuthorizationAppleIDProvider *appleIDProvider = [ASAuthorizationAppleIDProvider new];
    ASAuthorizationAppleIDRequest *authAppleIDRequest = [appleIDProvider createRequest];
    ASAuthorizationPasswordRequest *passwordRequest = [[ASAuthorizationPasswordProvider new] createRequest];
    
    NSMutableArray <ASAuthorizationRequest *>* array = [NSMutableArray arrayWithCapacity:2];
    if (authAppleIDRequest) {
        [array addObject:authAppleIDRequest];
    }
    if (passwordRequest) {
        [array addObject:passwordRequest];
    }
    NSArray <ASAuthorizationRequest *>* requests = [array copy];
    
    ASAuthorizationController *authorizationController = [[ASAuthorizationController alloc] initWithAuthorizationRequests:requests];
    authorizationController.delegate = self;
    authorizationController.presentationContextProvider = self;
    [authorizationController performRequests];
}

#pragma mark - ASAuthorizationControllerDelegate
- (void)authorizationController:(ASAuthorizationController *)controller didCompleteWithAuthorization:(ASAuthorization *)authorization API_AVAILABLE(ios(13.0))
{
    if ([authorization.credential isKindOfClass:[ASPasswordCredential class]]) {
        ASPasswordCredential *passwordCredential = authorization.credential;
        NSString *userIdentifier = passwordCredential.user;
        NSString *password = passwordCredential.password;
        
        NSLog(@"userIdentifier: %@", userIdentifier);
        NSLog(@"password: %@", password);
    }
}

3. 私有方法 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]
}];

4. 推送的 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 = (const unsigned *)[deviceToken bytes];
    NSString *hexToken = [NSString stringWithFormat:@"%08x%08x%08x%08x%08x%08x%08x%08x",
                          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);
}

5. 模态弹出默认样式改变

在 iOS 13,使用 presentViewController 方式打开视图,默认的如下图所示的视差效果,通过下滑页面返回。

iOS13 present 效果

这是因为苹果将 UIViewControllermodalPresentationStyle 属性的默认值改成了新加的一个枚举值 UIModalPresentationAutomatic,对于多数 UIViewController,此值会映射成 UIModalPresentationPageSheet

新增加枚举

解决方案 在baseViewController 设置 改回全屏显示的界面,需要手动设置弹出样式:

- (UIModalPresentationStyle)modalPresentationStyle {
    return UIModalPresentationFullScreen;
} 

6. UISearchBar 黑线处理导致崩溃

为了处理 搜索框的黑线问题,通常会遍历 searchBarsubViews,找到并删除 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'

解决方案 设置 UISearchBarBackground 的 layer.contents 为 nil:

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

或者 设置 UISearchBar 的背景图片为空:

[_searchBar setBackgroundImage:[UIImage new]];

7. UINavigationBar 设置按钮边距导致崩溃

从 iOS 11 开始,UINavigationBar 使用了自动布局,左右两边的按钮到屏幕之间会有 16 或 20 的边距。

导航按钮

为了避免点击到间距的空白处没有响应,通常做法是:定义一个 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;
        }
    }
}

8. UIWebView 将被禁止提交审核

在 iOS 13 推出后,苹果在 UIWebView 的说明上将其支持的系统范围定格在了 iOS 2 ~ iOS 12。目前,如果开发者将包含 UIWebView api 的应用更新上传到 App Store 审核后,其将会收到包含 ITMS-90809 信息的回复邮件,提示你在下一次提交时将应用中 UIWebView 的 api 移除。

解决方案 以后都要用WKWebView 用 WKWebView 替代 UIWebView,确保所有 UIWebView 的 api 都要移除,如果需要适配 iOS 7 的可以通过 openURL 的方式在 Safari 打开。

9.UITabBarButton 不同状态下结构不同

在 iOS 13 中,UITabBarButton 的控件结构会随着其选中状态的变化而变化,主要体现为 UITabBarSwappableImageView 和 UITabBarButtonLabel 的位置变化。在选中时和以前一样,是 UITabBarButton 的子控件。而在未选中状态下放到了 UIVisualEffectView 的 _UIVisualEffectContentView 里面。

image.png
我们在自定义 UITabBar 时,通常会遍历 UITabBarButton 的子控件获取 UITabBarSwappableImageView,比如自定义红点时添加到这个 ImageView 的右上角,这在 iOS 13 中可能就会导致异常.

解决方案

可以使用递归遍历 UITabBarButton 的所有 subviews 获取 UITabBarSwappableImageView,具体可以参照上面 私有方法 KVC 可能导致崩溃章节中给出的递归遍历方法。 另外需要注意,未选中状态下,添加的红点会和 tabBar 的图片一样变成灰色,这一点应该也是因为其结构变化造成的。具体可以见下图:

image.png
如果想要和以前一样未选中时也是红色,也很简单,把红点添加到 UITabBarButton 上,位置再根据 UITabBarSwappableImageView 调整即可。

10.使用 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的组合方案。

11.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 作为视频播放控件。 推荐一个第三方库 ZFPlayer )

11.蓝牙权限字段更新导致崩溃以及提交审核失败

在 iOS 13 中,苹果将原来蓝牙申请权限用的 NSBluetoothPeripheralUsageDescription 字段,替换为 NSBluetoothAlwaysUsageDescription 字段。

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

如果在 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.

12.CNCopyCurrentNetworkInfo 使用要求更严格

从 iOS 12 开始,CNCopyCurrentNetworkInfo 函数需要开启 Access WiFi Information 的功能后才会返回正确的值。在 iOS 13 中,这个函数的使用要求变得更严格,根据 CNCopyCurrentNetworkInfo 文档说明,应用还需要符合下列三项条件中的至少一项才能得到正确的值:

  • 使用 Core Location 的应用, 并获得定位服务权限。
  • 使用 NEHotspotConfiguration 来配置 WiFi 网络的应用。
  • 目前正处于启用状态的 VPN 应用。

苹果作出这项改变主要为了保障用户的安全,因为根据 MAC 地址容易推算出用户当前所处的地理位置。同样,蓝牙设备也具有 MAC 地址,所以苹果也为蓝牙添加了新的权限,可见上一点。 解决方案 根据应用需求,添加三项要求其中一项。可以选择第一项获取定位权限,因为添加的成本不会太大,只需要用户允许应用使用定位服务即可。

13.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 进行审批。 解决方法 使用LaunchScreen.storyboard设置启动页,弃用 LaunchImage

14.UISegmentedControl 默认样式改变

默认样式变为白底黑字,如果设置修改过颜色的话,页面需要修改。

image.png
原本设置选中颜色的 tintColor 已经失效,新增了 selectedSegmentTintColor 属性用以修改选中的颜色。

15.Xcode 11 创建的工程在低版本设备上运行黑屏

使用 Xcode 11 创建的工程,运行设备选择 iOS 13.0 以下的设备,运行应用时会出现黑屏。这是因为 Xcode 11 默认是会创建通过 UIScene 管理多个 UIWindow 的应用,工程中除了 AppDelegate 外会多一个 SceneDelegate:

新建Xcode工程多出来的文件
这是为了 iPadOS 的多进程准备的,也就是说 UIWindow 不再是 UIApplication 中管理,但是旧版本根本没有 UIScene。 解决方案 在 AppDelegate 的头文件加上:

@property (strong, nonatomic) UIWindow *window;

16.子线程修改界面都整崩溃(相册首次授权回掉必现)

在使用相册时我们会调用 [PHPhotoLibrary requestAuthorization:] 方法获取权限,获取的结果会通过一个带有 PHAuthorizationStatus 信息的 block 进行回调。

[PHPhotoLibrary requestAuthorization:^(PHAuthorizationStatus status) {
    // 根据 status 判断不同状态
}];

在 iOS 13 中,如果在第一次获取权限的回调中直接修改界面,会导致崩溃,崩溃信息如下:

This application is modifying the autolayout engine from a background thread after the engine was accessed from the main thread. This can lead to engine corruption and weird crashes.

*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Modifications to the layout engine must not be performed from a background thread after it has been accessed from the main thread.'

实际测试,第一次授权崩溃必先,再次授权偶现。另外,正如崩溃信息所言,不只是相册授权回调线程,其他子线程修改界面都有一定概率导致崩溃,而在 iOS 13 中貌似概率更高。

解决方案

在 Xcode 中调试运行时,子线程修改界面会有紫色感叹号标出,注意修改成回到主线程即可。

17.默认弹出样式打开在WKWebview中获取图片崩溃

iOS13 模态视图的默认样式改变,使用UIModalPresentationPageSheet 弹出VC,使用WKWebview通过HTML获取照片:

[_webView loadHTMLString:@"<input accept='image/*' type='file'>" baseURL:nil];

崩溃日志

*** Terminating app due to uncaught exception 'NSGenericException', reason: 'Your application has presented a UIDocumentMenuViewController (<UIDocumentMenuViewController: 0x101226860>). In its current trait environment, the modalPresentationStyle of a UIDocumentMenuViewController with this style is UIModalPresentationPopover. You must provide location information for this popover through the view controller's popoverPresentationController. You must provide either a sourceView and sourceRect or a barButtonItem.  If this information is not known when you present the view controller, you may provide it in the UIPopoverPresentationControllerDelegate method -prepareForPopoverPresentation.'

原因是,点击获取系统照片时,会弹出一个模态视图的样式为 UIModalPresentationPopover 的 UIDocumentMenuViewController,如果其父 UIViewController 以非全屏方式 present 的,那么就需要像 iPad 一样指定其 sourceView 和 sourceRect,或者指定一个 barButtonItem,否则会出现上述崩溃。而使用 UIModalPresentationFullScreen 的方式弹出的话就不会有这个问题. 解决方法

- (UIModalPresentationStyle)modalPresentationStyle {
    return UIModalPresentationFullScreen;
}

另一种方式 指定 sourceView、sourceRect,barButtonItem。

- (void)presentViewController:(UIViewController *)viewControllerToPresent animated:(BOOL)flag completion:(void (^)(void))completion {
    [self setUIDocumentMenuViewControllerSoureViewsIfNeeded:viewControllerToPresent];
    [super presentViewController:viewControllerToPresent animated:flag completion:completion];
}

- (void)setUIDocumentMenuViewControllerSoureViewsIfNeeded:(UIViewController *)viewControllerToPresent{
    if (@available(iOS 13, *)) {
        if([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPhone && [viewControllerToPresent isKindOfClass:UIDocumentMenuViewController.class]){
            viewControllerToPresent.popoverPresentationController.sourceView = self.webView;
            viewControllerToPresent.popoverPresentationController.sourceRect = CGRectMake(15, 5, 1, 1); // 具体看按钮的位置
        }
    }
}

// 如果顶层有 UINavigationController 的话,需要如下指定
- (void)presentViewController:(UIViewController *)viewControllerToPresent animated:(BOOL)flag completion:(void (^)(void))completion {
    if([self.viewControllers.lastObject isKindOfClass:WKWebViewController.class]){
        WKWebViewController *vc = self.viewControllers.lastObject;
        [vc setUIDocumentMenuViewControllerSoureViewsIfNeeded:viewControllerToPresent];
    }
    [super presentViewController:viewControllerToPresent animated:flag completion:completion];
}

18. WKWebview 在iPad上默认显示桌面版网页

原因是iOS13.0 中为WKWebview添加了切换桌面版本和手机本本的方法,其通过在一个新加的枚举值 控制:

typedef NS_ENUM(NSInteger, WKContentMode) {
    WKContentModeRecommended,
    WKContentModeMobile,
    WKContentModeDesktop
} API_AVAILABLE(ios(13.0));

此枚举默认值为WKContentModeRecommended,在 iPhone 和 iPad mini 上映射为WKContentModeMobile,在其他 iPad 上则为 WKContentModeDesktop,因此 iPad 上打开网页默认显示桌面版本。 解决方法

可通过WKWebViewConfiguration的新属性 defaultWebpagePreferences 来设置,目前其仅包含一个 WKContentMode 类型的属性 preferredContentMode,默认值为 WKContentModeRecommended 可以通过改变其值来修改显示的版本:

WKWebViewConfiguration *configuration = [[WKWebViewConfiguration alloc] init];
if (@available(iOS 13.0, *)) {
    configuration.defaultWebpagePreferences.preferredContentMode = WKContentModeMobile;
}

2.除了在初始化时候设置外,还可以通过新的代理方法实现桌面版和移动版切换,苹果给出了一个具体的例子: Viewing Desktop or Mobile Web Content Using a Web View ,其中关键的方法为:

- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction preferences:(WKWebpagePreferences *)preferences decisionHandler:(void (^)(WKNavigationActionPolicy, WKWebpagePreferences * _Nonnull))decisionHandler {
    preferences.preferredContentMode = WKContentModeMobile;
    decisionHandler(WKNavigationActionPolicyAllow, preferences);
}

19.CNCopyCurrentNetworkInfo 使用要求更严格

从 iOS 12 开始,CNCopyCurrentNetworkInfo 函数需要开启 Access WiFi Information 的功能后才会返回正确的值。在 iOS 13 中,这个函数的使用要求变得更严格,根据 CNCopyCurrentNetworkInfo 文档说明,应用还需要符合下列三项条件中的至少一项才能得到正确的值:

使用Core Location 的应用, 并获得定位服务权限。 使用NEHotspotConfiguration来配置 WiFi 网络的应用。 目前正处于启用状态的 VPN 应用。

苹果作出这项改变主要为了保障用户的安全,因为根据 MAC 地址容易推算出用户当前所处的地理位置。同样,蓝牙设备也具有 MAC 地址,所以苹果也为蓝牙添加了新的权限,可见上一点。

解决方案

根据应用需求,添加三项要求其中一项。可以选择第一项获取定位权限,因为添加的成本不会太大,只需要用户允许应用使用定位服务即可。

感谢:

juejin.cn/post/684490…

juejin.cn/post/684490…

www.jianshu.com/p/4654f8f6e…

www.jianshu.com/p/ab67782cc…

www.cnblogs.com/guoshaobin/…

blog.csdn.net/weixin_3873…

juejin.cn/post/684490…

END