前言:
iOS13的API的变动和适配问题,我从新特性适配、API 适配、方法弃用、工程适配、SDK 适配、其他问题分类归纳整理。
新特性适配
1.暗黑模式Dark Mode
2.苹果登录 Sign In with Apple
API 适配
1.模态视图弹出方式改变
2.私有方法 KVC 被禁用
3.searchBar设置textField问题
4.UISearchBar 黑线处理导致崩溃
5.推送的 deviceToken 获取到的格式发生变化
6.UISegmentedControl默认样式改变
方法弃用
1.UIWebView 将被禁止提交审核
2.使用 UISearchDisplayController 导致崩溃
3.MPMoviePlayerController 被弃用
工程适配
1.LaunchImage 被弃用
2.iOS13 新生命周期 SceneDelegate
3.蓝牙权限字段更新导致崩溃以及提交审核失败
4.CNCopyCurrentNetworkInfo 使用要求更严格
5.修改APP名称(修改DisplayName值)
SDK 适配
1.使用 @available 导致旧版本 Xcode 编译出错。
其他问题
1.[_LSDefaults sharedInstance]崩溃
一、新特性适配
1.暗黑模式Dark Mode
iOS 13 推出暗黑模式,UIView默认背景色会变成暗黑色。适配暗黑模式的工作量较大,改为强制使用正常模式。
处理方案:在plist文件中增加配置项UIUserInterfaceStyle
,值为Light。
2.苹果登录 Sign In with Apple
关于苹果登录,如果应用没有使用第三方登录,可以不用添加,如果 APP 支持三方登陆(Facbook、Google、微信、QQ、支付宝、微博等),就必须支持苹果登陆,且要放前边。
建议支持使用Apple提供的按钮样式,已经适配各类设备。
2019 年 9 月 12 日 起,提交到 App Store 的新应用必须按照应用审核指南中的标准进行接入;现有应用和应用更新必须也在 2020 年 4 月前完成接入。
二、API 适配
1. 模态视图弹出方式改变
在 iOS 13 UIModalPresentationStyle 枚举的定义中,苹果新加了一个枚举值:
typedef NS_ENUM(NSInteger, UIModalPresentationStyle) {
...
UIModalPresentationAutomatic API_AVAILABLE(ios(13.0)) = -2,
}
新的交互方式默认为下拉dismiss,且导航栏会留白空出;
如果需要点击取消按钮才消失界面的,需要适配;
如果你完全接受苹果的这个默认效果,那就不需要去修改任何代码。
如果需要做成全屏显示的界面,需要手动设置弹出样式:
- (UIModalPresentationStyle)modalPresentationStyle {
return UIModalPresentationFullScreen;
}
如果,之前代码已经设置了modalPresentationStyle的值,那你也不会有这个影响。对于想要找回原来默认交互的同学,直接设置如下即可:
在presentViewController之前加入这个代码:
[self presentViewController:nav animated:YES completion:nil];
nav.modalPresentationStyle = UIModalPresentationFullScreen;
2.私有方法 KVC 被禁用
iOS13后不再允许valueForKey、setValue:forKey: 等方法获取或设置私有属性,虽然编译可以通过,但是在运行时会直接崩溃或不起作用。
👇下面的方法都没有用了哦
// UITextField 的 _placeholderLabel
[textField setValue:[UIColor blackColor] forKeyPath:@"_placeholderLabel.textColor"];
// UISearchBar 的 _searchField
[searchBar valueForKey:@"_searchField"];
解决方案一:富文本设置
UITextField有一个attributedPlaceholder属性,占位符富文本,和UILabel等控件的富文本设置一样,可以设置文字颜色,尺寸等。
UITextField *textField = [[UITextField alloc]init];
textField.placeholder = @"我是占位符";
[self.view addSubview:textField];
NSMutableAttributedString *attribute_placeholder = [[NSMutableAttributedString alloc]initWithString:textField.placeholder];
[attribute_placeholder addAttribute:NSForegroundColorAttributeName
value:[UIColor colorWithWhite:1 alpha:0.2]
range:NSMakeRange(0, textField.placeholder.length)];
[attribute_placeholder addAttribute:NSFontAttributeName
value:[UIFont systemFontOfSize:14]
range:NSMakeRange(0, textField.placeholder.length)];
[searchField setAttributedPlaceholder:attribute_placeholder];
解决方案二:用runtime处理解决TextField的KVC崩溃问题
添加以下代码,老代码不用作任何修改
// 用runtime处理解决TextField的KVC崩溃问题
+ (void)load {
Method origin = class_getInstanceMethod([self class], @selector(valueForKey:));
Method swizzing = class_getInstanceMethod([self class], @selector(swizzing_valueForKey:));
if (class_addMethod([self class], @selector(valueForKey:), method_getImplementation(swizzing), method_getTypeEncoding(swizzing))) {
class_replaceMethod([self class], @selector(swizzing_valueForKey:), method_getImplementation(origin), method_getTypeEncoding(origin));
}
method_exchangeImplementations(origin, swizzing);
}
- (id)swizzing_valueForKey:(NSString *)key {
if ([key isEqualToString:@"_placeholderLabel"]) {
Ivar ivar = class_getInstanceVariable([self class], [key UTF8String]);
id value = object_getIvar(self, ivar);
return value;
} else {
return [self swizzing_valueForKey:key];
}
}
3.searchBar设置textField问题
我们不用再用kvc获取UISearchBar的textField了,因为,iOS13中系统将searchTextField属性暴露出来了,不再是私有属性了,你可以直接调用。
UITextField *searchField = _searchBar.searchTextField;
注意:以上写法在iOS13以下手机运行会崩溃,所以,暂时要写成两种情况的写法,iOS13依然沿用之前的旧写法。
用[[[UIDevice currentDevice] systemVersion] doubleValue] >= 13.0
加以区分
4.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;
}
5. 推送的 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);
}
6. UISegmentedControl默认样式改变
默认样式变为白底黑字,如果设置修改过颜色的话,页面需要修改。
原本设置选中颜色的 tintColor 已经失效,新增了 selectedSegmentTintColor
属性用以修改选中的颜色。
if (@available(iOS 13.0, *)) {
sc.selectedSegmentTintColor = ZLColor(113, 112, 118);
} else {
sc.tintColor = ZLColor(113, 112, 118);
}
iOS12以前设置圆角:
testSegmentedControl.layer.masksToBounds = true
testSegmentedControl.layer.cornerRadius = 10
但是iOS13系统下,完全不起作用,后来找到了方法,可以用重写UISegmentedControl的layoutSubviews方法来设置圆角。
class TestSegmentedControl: UISegmentedControl {
open override func layoutSubviews() {
super.layoutSubviews()
layer.cornerRadius = 10
}
}
这样子我们用TestSegmentedControl作为子类重写父类的layoutSubviews的方法,就又可以设置圆角啦。
如果项目有需求一定要在iOS13系统上实现iOS12以前的风格样式,可以自定义一个category来实现:
@interface UISegmentedControl (Style_OC)
/// UISegmentedControl 将iOS13风格转化成iOS12之前的风格样式
- (void)ensureiOS12Style;
@end
@implementation UISegmentedControl (Style_OC)
- (void)ensureiOS12Style {
// UISegmentedControl has changed in iOS 13 and setting the tint
// color now has no effect.
if (@available(iOS 13, *)) {
UIColor *tintColor = [self tintColor];
UIImage *tintColorImage = [self imageWithColor:tintColor];
// Must set the background image for normal to something (even clear) else the rest won't work
[self setBackgroundImage:[self imageWithColor:self.backgroundColor ? self.backgroundColor : [UIColor clearColor]] forState:UIControlStateNormal barMetrics:UIBarMetricsDefault];
[self setBackgroundImage:tintColorImage forState:UIControlStateSelected barMetrics:UIBarMetricsDefault];
[self setBackgroundImage:[self imageWithColor:[tintColor colorWithAlphaComponent:0.2]] forState:UIControlStateHighlighted barMetrics:UIBarMetricsDefault];
[self setBackgroundImage:tintColorImage forState:UIControlStateSelected|UIControlStateSelected barMetrics:UIBarMetricsDefault];
[self setTitleTextAttributes:@{NSForegroundColorAttributeName: tintColor, NSFontAttributeName: [UIFont systemFontOfSize:13]} forState:UIControlStateNormal];
[self setDividerImage:tintColorImage forLeftSegmentState:UIControlStateNormal rightSegmentState:UIControlStateNormal barMetrics:UIBarMetricsDefault];
self.layer.masksToBounds = YES;
self.layer.borderWidth = 1;
self.layer.borderColor = [tintColor CGColor];
}
}
- (UIImage *)imageWithColor: (UIColor *)color {
CGRect rect = CGRectMake(0.0f, 0.0f, 1.0f, 1.0f);
UIGraphicsBeginImageContext(rect.size);
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetFillColorWithColor(context, [color CGColor]);
CGContextFillRect(context, rect);
UIImage *theImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return theImage;
}
@end
三、方法弃用
1.UIWebView 将被禁止提交审核
在 iOS 13 推出后,苹果在 UIWebView 的说明上将其支持的系统范围定格在了 iOS 2 ~ iOS 12。目前,如果开发者将包含 UIWebView api 的应用更新上传到 App Store 审核后,其将会收到包含 ITMS-90809 信息的回复邮件,提示你在下一次提交时将应用中 UIWebView 的 api 移除。
解决方案:
用 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.'
另外说一下,在 iOS 13 中终于可以获取直接获取搜索的文本框:
_searchBar.searchTextField.text = @"search";
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.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 进行审批。
解决方案:
使用 LaunchScreen.storyboard 设置启动页,弃用 LaunchImage。
2. iOS13 新生命周期 SceneDelegate
Xcode 11 创建的工程在低版本设备上运行黑屏
使用 Xcode 11 创建的工程,运行设备选择 iOS 13.0 以下的设备,运行应用时会出现黑屏。这是因为 Xcode 11 默认是会创建通过 UIScene 管理多个 UIWindow 的应用,工程中除了 AppDelegate 外会多一个 SceneDelegate:
增加了 #import "SceneDelegate.h"
原来的 #import "AppDelegate.h" 没有了window
要想维持原状,需要在AppDelegate的头文件里,添加window,并且删掉Info.plist里的新增键值对
@interface AppDelegate : UIResponder
@property (strong, nonatomic) UIWindow *window;
@end
删掉以下键值对
Application Scene Manifest:2times
Main storyboard file base name:Main
3. 蓝牙权限字段更新导致崩溃以及提交审核失败
在 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 的邮件,并提示审核不通过。
解决方案
官网文档也有说明,就是在 Info.plist 中把两个字段都加上。
For deployment targets earlier than iOS 13, add both `NSBluetoothAlwaysUsageDescription` and `NSBluetoothPeripheralUsageDescription` to your app’s Information Property List file.
4. CNCopyCurrentNetworkInfo 使用要求更严格
从 iOS 12 开始,CNCopyCurrentNetworkInfo 函数需要开启 Access WiFi Information
的功能后才会返回正确的值。在 iOS 13 中,这个函数的使用要求变得更严格,根据 CNCopyCurrentNetworkInfo 文档说明,应用还需要符合下列三项条件中的至少一项才能得到正确的值:
- 使用 Core Location 的应用, 并获得定位服务权限。
- 使用 NEHotspotConfiguration 来配置 WiFi 网络的应用。
- 目前正处于启用状态的 VPN 应用。
苹果作出这项改变主要为了保障用户的安全,因为根据 MAC 地址容易推算出用户当前所处的地理位置。同样,蓝牙设备也具有 MAC 地址,所以苹果也为蓝牙添加了新的权限,可见蓝牙上这一点。
解决方案:
根据应用需求,添加三项要求其中一项。可以选择第一项获取定位权限,因为添加的成本不会太大,只需要用户允许应用使用定位服务即可。
5. 修改APP名称(修改DisplayName值)
在Xcode创建项目时默认的
project.pbxproj
中的所有PRODUCT_NAME = "$(TARGET_NAME)"
。在Xcode11.0之前如果修改
DisplayName
时只是修改info.plist中的Bundle display name
值,但是在Xcode11.0中修改该值则会把project.pbxproj
中的一个PRODUCT_NAME
改为修改后值,如果在项目中通过[NSBundle mainBundle] infoDictionary]取kCFBundleExecutableKey
的就会有影响,并且对Build Settings
中的Packaing
中的一些名称有影响。
五、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
六、其他问题
1. [_LSDefaults sharedInstance]崩溃
遇到以下崩溃问题
+[_LSDefaults sharedInstance]: unrecognized selector sent to class 0x1dd8f5d40
解决方案:
可以尝试pod updatepod
库,暂时可以解决问题。
iOS13的API的变动和适配问题,在工作中可能会有不同的问题和方式出现,会持续更新呦~