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 (第七代)
新特性适配
1. Dark Mode
iOS 13 推出暗黑模式,UIKit
提供新的系统颜色和 api 来适配不同颜色模式,xcassets
对素材适配也做了调整,具体适配可见: Implementing Dark Mode on iOS
。
2. Sign In with Apple
Sign In with Apple will be available for beta testing this summer. It will be required as an option for users in apps that support third-party sign-in when it is commercially available later this year.
如果你的应用支持使用第三方登录,那么就必须加上苹果新推出的登录方式:
Introducing Sign In with Apple。目前苹果只在 News and Updates 上提到正式发布时要求加上,具体发布时间还没确定。
API 适配
1. 私有方法 KVC 不允许使用
在 iOS 13 中不再允许使用 valueForKey
、setValue:forKey:
等方法获取或设置私有属性,虽然编译可以通过,但是在运行时会直接崩溃,并提示一下崩溃信息:
// 使用的私有方法
[_textField setValue:[UIColor redColor] forKeyPath:@"_placeholderLabel.textColor"];
// 崩溃提示信息
*** Terminating app due to uncaught exception 'NSGenericException', reason: 'Access to UITextField's _placeholderLabel ivar is prohibited. This is an application bug'
解决方案只能是使用其他方法:
// 替换的方案
_textField.attributedPlaceholder = [[NSAttributedString alloc] initWithString:@"输入"attributes:@{NSForegroundColorAttributeName: [UIColor redColor]}];
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:@"%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);
}
工程适配
1. UISearchBar 黑线处理导致崩溃
之前为了处理搜索框的黑线问题,通常会遍历 searchBar 的 subViews,找到并删除 UISearchBarBackground
,在 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 removeFromSuperview];
view.layer.contents = nil;
break;
}
}
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.'
另外说一下,在 iOS 13 中终于可以获取直接获取搜索的文本框:
_searchBar.searchTextField.text = @"search";
3. 模态弹出默认交互改变
在 iOS 13 UIModalPresentationStyle
枚举的定义中,苹果新加了一个枚举值:
typedef NS_ENUM(NSInteger, UIModalPresentationStyle) {
...
UIModalPresentationAutomatic API_AVAILABLE(ios(13.0)) = -2,
};
在 iOS 13 中此枚举值直接成为了模态弹出的默认值,因此 presentViewController
方式打开视图是如下的视差效果,默认是下滑返回。
模态弹出来的页面导航栏部分是被砍掉的,在 storyboard 中也可以看到,导航栏的内容是会被遮挡住的。
storyboard如果需要做成全屏显示的界面,需要手动设置弹出样式:
- (UIModalPresentationStyle)modalPresentationStyle {
return UIModalPresentationFullScreen;
}
有一点注意的是,我们原来以全屏的样式弹出一个页面,那么将这个页面弹出的那个 ViewController 会依次调用 viewWillDisappear
和 viewDidDisappear
。然后在这个页面被 dismiss 的时候,将他弹出的那个 ViewController 的 viewWillAppear
和 viewDidAppear
会被依次调用。然而使用默认的视差效果弹出页面,将他弹出的那个 ViewController 并不会调用这些方法,原先写在这四个函数中的代码以后都有可能会存在问题。
4. UISegmentedControl 默认样式改变
默认样式变为白底黑字,如果设置修改过颜色的话,页面需要修改。
原本设置选中颜色的
tintColor
已经失效,新增了 selectedSegmentTintColor 属性用以修改选中的颜色。
5. 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
。
6. LaunchImage 被弃用
iOS 8 之前我们是在LaunchImage
来设置启动图,但是随着苹果设备尺寸越来越多,我们需要在对应的 aseets 里面放入所有尺寸的启动图,这是非常繁琐的一个步骤。因此在 iOS 8 苹果引入了 LaunchScreen.storyboard
,支持界面布局用的 AutoLayout
+ SizeClass
,可以很方便适配各种屏幕。
需要注意的是,苹果在 Modernizing Your UI for iOS 13 section 中提到
,从2020年4月开始,所有支持 iOS 13 的 App 必须提供 LaunchScreen.storyboard
,否则将无法提交到 App Store 进行审批。
7. 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
参考文献
本文结合以下文档内容和个人遇到的问题,对常见适配问题进行总结