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 (第七代)
一、UI层面
1.Dark Mode - 审核强制要求适配黑夜模式。
2.Sign In with Apple - 提供第三方登录
3.模态弹出默认交互改变(必须)
4.UISegmentedControl 默认样式改变(必须)
5.h5的适配,内嵌WebView,一些图片路径,文件路径,不用写绝对路径,直接写文件名字即可
二、代码层面
1.私有方法 KVC 不允许使用:valueForKey、setValue:forKey:(必须)
2.推送的 deviceToken 获取到的格式发生变化(必须)
3.UISearchBar 黑线处理导致崩溃
4.使用 UISearchDisplayController 导致崩溃
5.模态弹出默认交互改变(必须)
6.MPMoviePlayerController 在iOS13被弃用
7.LaunchImage 被弃用(必须)
8.Xcode 11 创建的工程在低版本设备上运行黑屏
9.使用 @available 导致旧版本 Xcode 编译出错。(必须)
10.textfield.leftview(必须)
11.NSAttributedString优化
12.TabBar红点偏移
13.废弃UIWebView(必须)
14.WKWebView 中测量页面内容高度的方式变更
15.使用MJExtension 中处理NSNull的不同(必须)
16.StatusBar 与之前版本不同(必须)
17.UIActivityIndicatorView(必须)
18.蓝牙权限需要申请
19.CNCopyCurrentNetworkInfo
20.下面这种获取网络状态用到KVC的方法会发生崩溃
21.MPRemoteCommandCenter 音频后台播放控制器
一、UI层面
注:必须适配的点以“(必须)”标出,有的是看项目需求,大家可忽略
1.Dark Mode - 审核强制要求适配黑夜模式。
iOS 13 推出暗黑模式,UIKit 提供新的系统颜色和 api 来适配不同颜色模式,xcassets 对素材适配也做了调整,官方具体适配可见: Implementing Dark Mode on iOS。
适配方案:
![](https://img.haomeiwen.com/i10389949/772ce089a12dcf4b.png)
参考链接:
https://mp.weixin.qq.com/s/qliFbqRdkkE30vslojfJCA
https://juejin.im/post/5cf6276be51d455a68490b26
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 上提到正式发布时要求加上,具体发布时间还没确定。
3.模态弹出默认交互改变(必须)
在 iOS 13 中此枚举值直接成为了模态弹出的默认值,因此 presentViewController 方式打开视图是如下的视差效果,默认是下滑返回。
![](https://img.haomeiwen.com/i10389949/17b874cf86afde08.gif)
iOS13下仍然可以做到全屏弹出,运行代码发现presentViewController和之前弹出的样式不一样
会出现这种情况是主要是因为我们之前对UIViewController里面的一个属性,即modalPresentationStyle(该属性是控制器在模态视图时将要使用的样式)没有设置需要的类型。在iOS13中modalPresentationStyle的默认改为UIModalPresentationAutomatic,而在之前默认是UIModalPresentationFullScreen。
ViewController *vc = [[ViewController alloc] init];
vc.title = @"presentVC";
UINavigationController *nav = [[UINavigationController alloc] initWithRootViewController:vc];
nav.modalPresentationStyle = UIModalPresentationFullScreen;
[self.window.rootViewController presentViewController:nav animated:YES completion:nil];
注意:如果你某个控制器想使用卡片模式,需要注意你这个控制器底部是否有控件。卡片式的底部的控件容易被遮挡。
比方说TZPhotoPickerController 这个常用的开源相册控件,当选择照片时底部的确定按钮就被遮挡,无法选中.
4.UISegmentedControl 默认样式改变(必须)
默认样式变为白底黑字,如果设置修改过颜色的话,页面需要修改。
![](https://img.haomeiwen.com/i10389949/cbe15928781f0d68.png)
原本设置选中颜色的 tintColor 已经失效,新增了 selectedSegmentTintColor 属性用以修改选中的颜色。
Web Content适配
if ( @available(iOS 13.0, *)) {
self.segmentedControl.selectedSegmentTintColor = [UIColor clearColor];
}else
{
self.segmentedControl.tintColor = [UIColor clearColor];
}
5.h5的适配,参考链接:
内嵌WebView,一些图片路径,文件路径,不用写绝对路径,直接写文件名字即可
https://blog.csdn.net/u012413955/article/details/92198556
二、代码层面
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]}];
解决方案二:去掉keypath中的“_”
如果需要修改UISearchBar的placeholder,需要获取其searchTextField,可用category实现:
@implementation UISearchBar (SearchTextField)
- (UITextField *)atu_searchTextField{
if ([UIDevice currentDevice].systemVersion.floatValue >= 13.0) {
//判断Xcode版本
#ifdef __IPHONE_13_0
return self.searchTextField;
#else
return [self valueForKey:@"searchTextField"];
#endif
}
//尝试过遍历subviews来找到,但是subviews中并不包含searchField!没有找到更好的办法
return [self valueForKey:@"searchField"];
}
@end
大家可以集思广益
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);
}
3.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;
}
}
4.使用 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";
5.模态弹出默认交互改变(必须)
在iOS13中运行代码发现presentViewController和之前弹出的样式不一样。是因为之前对UIViewController里面的一个属性,即modalPresentationStyle(该属性是控制器在模态视图时将要使用的样式)没有设置需要的类型。在iOS13中modalPresentationStyle的默认改为UIModalPresentationAutomatic,而在之前默认是UIModalPresentationFullScreen。
注意:如果你某个控制器想使用卡片模式,需要注意你这个控制器底部是否有控件。卡片式的底部的控件容易被遮挡。比方说TZPhotoPickerController 这个常用的开源相册控件。当选择照片时底部的确定按钮就被遮挡。无法选中
如果需要做成全屏显示的界面,需要手动设置弹出样式:
- (UIModalPresentationStyle)modalPresentationStyle {
return UIModalPresentationFullScreen;
}
//或者在present之前:
ctr.modalPresentationStyle = UIModalPresentationFullScreen;
[self presentViewController:ctr animated:animated completion:completion];
如果项目里面有大量的位置使用了presentViewController,然后你不想每个位置都显式的手动修改,推荐这篇文章https://juejin.im/post/5d5f96866fb9a06b0517f78c
里面的方法,原理是:
通过runtime修改ctr.modalPresentationStyle的默认值为UIModalPresentationFullScreen(原默认值为UIModalPresentationPageSheet)如果需要禁止自动修改默认值
ctr.LL_automaticallySetModalPresentationStyle = NO;
不太多的话还是建议老老实实的一个个手动修改,以免以后官方api再次变动。
有一点注意的是,ctr的生命周期方法调用情况会改变,假设有a,b两个ctr,在a中present出b:
全屏present时(UIModalPresentationFullScreen)的方法调用顺序:
a---viewWillDisappear:
b---viewWillAppear:
b---viewDidAppear:
a---viewDidDisappear:
dissmiss时的方法调用顺序:
b---viewWillDisappear:
a---viewWillAppear:
a---viewDidAppear:
b---viewDidDisappear:
非全屏presnet时(UIModalPresentationPageSheet)的方法调用顺序:
b---viewWillAppear:
b---viewDidAppear:
dissmiss时的方法调用顺序:
b---viewWillDisappear:
b---viewDidDisappear:
可以看出,以UIModalPresentationPageSheet的方式来present/dismiss时,分别少调用了a的两个方法,如果之前在这个位置有相关的逻辑代码,比如网络请求,UI刷新,要注意
6.MPMoviePlayerController 在iOS13被弃用
在 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。
7.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 进行审批。
8.Xcode 11 创建的工程在低版本设备上运行黑屏
使用 Xcode 11 创建的工程,运行设备选择 iOS 13.0 以下的设备,运行应用时会出现黑屏。这是因为 Xcode 11 默认是会创建通过 UIScene 管理多个 UIWindow 的应用,工程中除了 AppDelegate 外会多一个 SceneDelegate.
![](https://img.haomeiwen.com/i10389949/3bd5a28b99637789.png)
这是为了 iPadOS 的多进程准备的,也就是说 UIWindow 不再是 UIApplication 中管理。但是旧版本根本没有 UIScene,因此解决方案就是在 AppDelegate 的头文件加上:
@property (strong, nonatomic) UIWindow *window;
9.使用 @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
10.textfield.leftview(必须)
如下方式,直接给 textfield.leftView 赋值一个 UILabel 对象,他的宽高会被 sizeToFit,而不是创建时的值。
// left view label
UILabel *phoneLabel = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, 63, 50)];
phoneLabel.text = @"手机号";
phoneLabel.font = [UIFont systemFontOfSize:16];
// set textfield left view
self.textfieldName.leftView = phoneLabel;
如所看到,实际leftview的width为59,height为19:
![](https://img.haomeiwen.com/i10389949/46755dfdbbed9e42.png)
解决方法:嵌套一个UIView
// label
UILabel *phoneLabel = [[UILabel alloc] init];
phoneLabel.text = @"手机号";
phoneLabel.font = [UIFont systemFontOfSize:16];
[phoneLabel sizeToFit];
phoneLabel.centerY = 50/2.f;
// left view
UIView *leftView = [[UIView alloc] initWithFrame:(CGRect){0, 0, 63, 50}];
[leftView addSubview:phoneLabel];
// set textfield left view
self.textfieldName.leftView = leftView;
11.NSAttributedString优化
对于UILabel、UITextField、UITextView,在设置NSAttributedString时也要考虑适配Dark Mode,否则在切换模式时会与背景色融合,造成不好的体验
不建议的做法
NSDictionary *dic = @{NSFontAttributeName:[UIFont systemFontOfSize:16]};
NSAttributedString *str = [[NSAttributedString alloc] initWithString:@"富文本文案" attributes:dic];
推荐的做法
// 添加一个NSForegroundColorAttributeName属性
NSDictionary *dic = @{NSFontAttributeName:[UIFont systemFontOfSize:16],NSForegroundColorAttributeName:[UIColor labelColor]};
NSAttributedString *str = [[NSAttributedString alloc] initWithString:@"富文本文案" attributes:dic];
12.TabBar红点偏移
如果之前有通过TabBar上图片位置来设置红点位置,在iOS13上会发现显示位置都在最左边去了。遍历UITabBarButton的subViews发现只有在TabBar选中状态下才能取到UITabBarSwappableImageView,解决办法是修改为通过UITabBarButton的位置来设置红点的frame
13.废弃UIWebView(必须)
UIWebView在12.0就已经被废弃,部分APP使用webview时, 审核被拒
14.WKWebView 中测量页面内容高度的方式变更
iOS 13以前 document.body.scrollHeight
iOS 13中 document.documentElement.scrollHeight 两者相差55 应该是浏览器定义高度变了
15.使用MJExtension 中处理NSNull的不同(必须)
这个直接会导致Crash的在将服务端数据字典转换为模型时,如果遇到服务端给的数据为NSNull时, mj_JSONObject,其中 class_copyPropertyList方法得到的属性里,多了一种EFSQLBinding类型的东西,而且属性数量也不准确, 那就没办法了, 我只能改写这个方法了,这个组件没有更新的情况下,写了一个方法swizzling掉把当遇到 NSNull时,直接转为nil了。
有人问这个方法怎么实现,其实目前我们项目在ios13下面还没遇到这种情况,发一个之前项目处理null的方法,在项目里面写一个NSObject的分类,添加下面的方法就可以了。大家请根据项目情况、数据格式修改下这个方法,MJ库里面自己会进行替换的:
- (id)mj_newValueFromOldValue:(id)oldValue property:(MJProperty *)property {
//为了解决json字符串先赋值给oc字典后,类型转换crash问题,如:
//json->oldValue:0
//model中值为NSString类型
//如果先将json转为dic,dic中对应value值为NSNumber类型,则向oldValue发送isEqualToString消息会crash
id tempValue = oldValue;
if ([property.type.code isEqualToString:@"NSString"]) {
tempValue = [NSString stringWithFormat:@"%@", tempValue];
if ([tempValue isKindOfClass:[NSNull class]] || tempValue == nil || [tempValue isEqual:[NSNull null]] || [tempValue isEqualToString:@"(null)"] || [tempValue isEqualToString:@"(\n)"] ) {
return @"";
}
}
if ([property.type.code isEqualToString:@"NSNumber"]) {
// tempValue = [NSNumber numberWithFloat:[tempValue floatValue]];
if ([tempValue isKindOfClass:[NSNull class]] || tempValue == nil || [tempValue isEqual:[NSNull null]] || [tempValue isEqualToString:@"(null)"] || [tempValue isEqualToString:@"(\n)"] ) {
return @0;
}
}
return tempValue;
}
16.StatusBar 与之前版本不同(必须)
目前状态栏也增加了一种模式,由之前的两种,变成了三种, 其中default由之前的黑色内容,变成了会根据系统模式,自动选择当前展示lightContent还是darkContent。
public enum UIStatusBarStyle : Int {
case `default` // Automatically chooses light or dark content based on the user interface style
@available(iOS 7.0, *)
case lightContent // Light content, for use on dark backgrounds
@available(iOS 13.0, *)
case darkContent // Dark content, for use on light backgrounds
}
我们在使用的时候,就可以重写preferredStatusBarStyle的get方法:
override var preferredStatusBarStyle: UIStatusBarStyle{
get{
return .lightContent
}
}
17.UIActivityIndicatorView(必须)
之前的 UIActivityIndicatorView 有三种 style 分别为 whiteLarge, white 和 gray,现在全部废弃。
增加两种 style 分别为 medium 和 large,指示器颜色用 color 属性修改。
18.蓝牙权限需要申请
CBCentralManager,iOS13以前,使用蓝牙时可以直接用,不会出现权限提示,iOS13后,再使用就会提示了。 在info.plist里增加
<key>NSBluetoothAlwaysUsageDescription</key>
<string>我们要一直使用您的蓝牙</string>
在iOS13中,蓝牙变成了和位置,通知服务等同样的可以针对单个app授权的服务。
- (NSString*) getWifiSsid {
if (@available(iOS 13.0, *)) {
//用户明确拒绝,可以弹窗提示用户到设置中手动打开权限
if ([CLLocationManager authorizationStatus] == kCLAuthorizationStatusDenied) {
NSLog(@"User has explicitly denied authorization for this application, or location services are disabled in Settings.");
//使用下面接口可以打开当前应用的设置页面
//[[UIApplication sharedApplication] openURL:[NSURL URLWithString:UIApplicationOpenSettingsURLString]];
return nil;
}
CLLocationManager* cllocation = [[CLLocationManager alloc] init];
if(![CLLocationManager locationServicesEnabled] || [CLLocationManager authorizationStatus] == kCLAuthorizationStatusNotDetermined){
//弹框提示用户是否开启位置权限
[cllocation requestWhenInUseAuthorization];
usleep(50);
//递归等待用户选选择
return [self getWifiSsidWithCallback:callback];
}
}
NSString *wifiName = nil;
CFArrayRef wifiInterfaces = CNCopySupportedInterfaces();
if (!wifiInterfaces) {
return nil;
}
NSArray *interfaces = (__bridge NSArray *)wifiInterfaces;
for (NSString *interfaceName in interfaces) {
CFDictionaryRef dictRef = CNCopyCurrentNetworkInfo((__bridge CFStringRef)(interfaceName));
if (dictRef) {
NSDictionary *networkInfo = (__bridge NSDictionary *)dictRef;
NSLog(@"network info -> %@", networkInfo);
wifiName = [networkInfo objectForKey:(__bridge NSString *)kCNNetworkInfoKeySSID];
CFRelease(dictRef);
}
}
CFRelease(wifiInterfaces);
return wifiName;
}
19.CNCopyCurrentNetworkInfo
iOS13 以后只有开启了 Access WiFi Information capability,才能获取到 SSID 和 BSSID wi-fi or wlan 相关使用变更
最近收到了苹果的邮件,说获取WiFi SSID的接口CNCopyCurrentNetworkInfo 不再返回SSID的值。不仔细看还真会被吓一跳,对物联网的相关APP简直是炸弹。仔细看邮件还好说明了可以先获取用户位置权限才能返回SSID。
注意:目本身已经打开位置权限,则可以直接获取
20.下面这种获取网络状态用到KVC的方法会发生崩溃
UIApplication *app = [UIApplication sharedApplication];
NSArray *children = nil;
// 不能用 [[self deviceVersion] isEqualToString:@"iPhone X"] 来判断,因为模拟器不会返回 iPhone X
id statusBar = [app valueForKeyPath:@"statusBar"];
if ([statusBar isKindOfClass:NSClassFromString(@"UIStatusBar_Modern")]) {
children = [[[statusBar valueForKey:@"statusBar"] valueForKey:@"foregroundView"] subviews];
} else {
children = [[statusBar valueForKey:@"foregroundView"] subviews];
}
NSString *state = [[NSString alloc] init];
int netType = 0;
for (id child in children) {
if ([child isKindOfClass:NSClassFromString(@"UIStatusBarDataNetworkItemView")]) {
//获取到状态栏
netType = [[child valueForKeyPath:@"dataNetworkType"] intValue];
switch (netType) {
case 0:
state = @"none";
break;
case 1:
state = @"2G";
break;
case 2:
state = @"3G";
break;
case 3:
state = @"4G";
break;
case 5:
state = @"WIFI";
break;
default:
break;
}
}
}
21.MPRemoteCommandCenter 音频后台播放控制器
- (void)addTarget:(id)target action:(SEL)action; 在iOS 13中selector 一定要返回MPRemoteCommandHandlerStatus,要不然会闪退。
// Target-action style for adding handlers to commands.
// Actions receive an MPRemoteCommandEvent as the first parameter.
// Targets are not retained by addTarget:action:, and should be removed from the
// command when the target is deallocated.
//
// Your selector should return a MPRemoteCommandHandlerStatus value when
// possible. This allows the system to respond appropriately to commands that
// may not have been able to be executed in accordance with the application's
// current state.
- (void)addTarget:(id)target action:(SEL)action;
- (void)removeTarget:(id)target action:(nullable SEL)action;
- (void)removeTarget:(nullable id)target;
网友评论