iOS 13 适配总结

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或者iPad Pro(三代)及以后版本的全屏幕设计。

新特性的适配

  1. Dark Mode
    iOS 13推出黑暗模式,UIKit 提供新的系统颜色和api来适配不同颜色模式,xcassets对素材也做了适配,可以添加Dark模式切图,如果不打算适配Dark Mode,可以直接在info.plist中添加一栏:User Interface Style: Light,即可以在应用中禁止黑暗模式
  2. Sign In with Apple
    如果使用第三方或者社交账号登陆服务来设置或者验证用户的主账号,必须添加 Sign In With Apple 作为同等的选项添加到应用上 ,新上架的从2019年9月12日起,现有应用的更新也必须在2020年4月前完成接入。

API 适配

  1. 私有方法kvc可能导致崩溃

在iOS 13 中禁止使用valueForKey、setValue:forKey: 来获取或者设置私有属性,误用会在运行时直接崩溃,整理了一些会导致崩溃的方法,以及替换解决的方法:

// 崩溃 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]
}];
  1. 推送的 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 }

//解决方案

//需要进行一次数据格式处理,参考[友盟](https://link.juejin.im/?target=https%3A%2F%2Fdeveloper.umeng.com%2Fdocs%2F66632%2Fdetail%2F126489)的做法,可以适配新旧系统,获取方式如下:

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);
  1. 模态弹出默认样式发生改变需要设置UIViewController 的 modalPresentationStyle 属性,如果要恢复之前的样式的话需要做一下设置:
- (UIModalPresentationStyle)modalPresentationStyle {
    return UIModalPresentationFullScreen;
}

4.UISearchBar 黑线处理导致崩溃的问题,解决方式设置 UISearchBarBackground 的 layer.contents 为 nil:

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

5.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;
        }
    }
}

但是这种做法在iOS13中会导致崩溃,崩溃信息如下:

*** 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…](https://link.juejin.im/?target=https%3A%2F%2Fdeveloper.apple.com%2Fdocumentation%2Fuikit%2Fuiwebview) 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 打开。

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

  1. 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 字段。如果在 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…](https://link.juejin.im/?target=https%3A%2F%2Fdeveloper.apple.com%2Fdocumentation%2Fuikit%2Fcore_app%2Fprotecting_the_user_s_privacy)).

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.
  1. CNCopyCurrentNetworkInfo 使用要求更严格

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

  • 使用 Core Location 的应用, 并获得定位服务权限。

  • 使用 NEHotspotConfiguration 来配置 WiFi 网络的应用。

  • 目前正处于启用状态的 VPN 应用。

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

解决方案

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

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

  1. UISegmentedControl 默认样式改变

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

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

  1. Xcode 11 创建的工程在低版本设备上运行黑屏
    使用 Xcode 11 创建的工程,运行设备选择 iOS 13.0 以下的设备,运行应用时会出现黑屏。这是因为 Xcode 11 默认是会创建通过 UIScene 管理多个 UIWindow 的应用,工程中除了 AppDelegate 外会多一个 SceneDelegate:
    这是为了 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```
转载自 http://www.cocoachina.com/articles/83448?filter=rec

你可能感兴趣的:(iOS 13 适配总结)