这次iOS 13系统升级,影响范围最广的应属KVC访问修改私有属性了,直接禁止开发者获取或直接设置私有属性。而KVC的初衷是允许开发者通过Key名直接访问修改对象的属性值,为其中最典型的 UITextField
的 _placeholderLabel
、UISearchBar
的 _searchField
。 造成影响:在iOS 13下App闪退 错误代码:
// placeholderLabel私有属性访问
[textField setValue:[UIColor redColor] forKeyPath:@"_placeholderLabel.textColor"];
[textField setValue:[UIFont boldSystemFontOfSize:16] forKeyPath:@"_placeholderLabel.font"];
// searchField私有属性访问
UISearchBar *searchBar = [[UISearchBar alloc] init];
UITextField *searchTextField = [searchBar valueForKey:@"_searchField"];
解决方案:
1、简单粗暴去掉下划线_
1、if ([UIDevice currentDevice].systemVersion.floatValue >= 13.0) {
[mobileNum setValue:[UIFont boldSystemFontOfSize:13] forKeyPath:@"placeholderLabel.font"];
}
2、解决方案: 使用 NSMutableAttributedString
富文本来替代KVC访问 UITextField
的 _placeholderLabel
textField.attributedPlaceholder = [[NSAttributedString alloc] initWithString:@"placeholder" attributes:@{NSForegroundColorAttributeName: [UIColor darkGrayColor], NSFontAttributeName: [UIFont systemFontOfSize:13]}];
3、可以添加。Category 本人觉得麻烦就不写了
模态弹窗属性 UIModalPresentationStyle
在 iOS 13 下默认被设置为 UIModalPresentationAutomatic
新特性,展示样式更为炫酷,同时可用下拉手势关闭模态弹窗。 若原有模态弹出 ViewController 时都已指定模态弹窗属性,则可以无视该改动。 若想在 iOS 13 中继续保持原有默认模态弹窗效果。可以通过 runtime 的 Method Swizzling
方法交换来实现。
模态全局设置
如果视差效果的样式可以接受的话,就不需要修改;如果需要改回全屏显示的界面,需要手动设置弹出样式:
- (UIModalPresentationStyle)modalPresentationStyle {
return UIModalPresentationFullScreen;
}
3. 黑暗模式的适配
针对黑暗模式的推出,Apple官方推荐所有三方App尽快适配。目前并没有强制App进行黑暗模式适配。因此黑暗模式适配范围现在可采用以下三种策略:
方案一:在项目 Info.plist
文件中,添加一条内容,Key为 User Interface Style
,值类型设置为String并设置为 Light
即可。
方案二:代码强制关闭黑暗模式,将当前 window 设置为 Light 状态。
if(@available(iOS 13.0,*)){
self.window.overrideUserInterfaceStyle = UIUserInterfaceStyleLight;
}
从Xcode 11、iOS 13开始,UIViewController与View新增属性 overrideUserInterfaceStyle
,若设置View对象该属性为指定模式,则强制该对象以及子对象以指定模式展示,不会跟随系统模式改变。
适配黑暗模式,主要从两方面入手:图片资源适配与颜色适配
图片资源适配
打开图片资源管理库 Assets.xcassets
,选中需要适配的图片素材item,打开最右侧的 Inspectors 工具栏,找到 Appearances 选项,并设置为 Any, Dark
模式,此时会在item下增加Dark Appearance,将黑暗模式下的素材拖入即可。关于黑暗模式图片资源的加载,与正常加载图片方法一致。
iOS 13开始UIColor变为动态颜色,在Light Mode与Dark Mode可以分别设置不同颜色。 若UIColor色值管理,与图片资源一样存储于 Assets.xcassets
中,同样参照上述方法适配。 若UIColor色值并没有存储于 Assets.xcassets
情况下,自定义动态UIColor时,在iOS 13下初始化方法增加了两个方法
+ (UIColor *)colorWithDynamicProvider:(UIColor * (^)(UITraitCollection *))dynamicProvider API_AVAILABLE(ios(13.0), tvos(13.0)) API_UNAVAILABLE(watchos); - (UIColor *)initWithDynamicProvider:(UIColor * (^)(UITraitCollection *))dynamicProvider API_AVAILABLE(ios(13.0), tvos(13.0)) API_UNAVAILABLE(watchos);
UIColor *dynamicColor = [UIColor colorWithDynamicProvider:^UIColor * _Nonnull(UITraitCollection * _Nonnull trainCollection) {
if ([trainCollection userInterfaceStyle] == UIUserInterfaceStyleLight) {
return [UIColor whiteColor];
} else {
return [UIColor blackColor];
}
}];
[self.view setBackgroundColor:dynamicColor];
监听模式的切换
当需要监听系统模式发生变化并作出响应时,需要用到 ViewController 以下函数
// 注意:参数为变化前的traitCollection,改函数需要重写 - (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection; // 判断两个UITraitCollection对象是否不同 - (BOOL)hasDifferentColorAppearanceComparedToTraitCollection:(UITraitCollection *)traitCollection;
示例代码:
- (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection {
[super traitCollectionDidChange:previousTraitCollection];
// trait has Changed?
if ([self.traitCollection hasDifferentColorAppearanceComparedToTraitCollection:previousTraitCollection]) {
// do something...
}
}
使用 LaunchImage 设置启动图,需要提供各类屏幕尺寸的启动图适配,这种方式随着各类设备尺寸的增加,增加了额外不必要的工作量。为了解决 LaunchImage 带来的弊端,iOS 8引入了 LaunchScreen 技术,因为支持 AutoLayout + SizeClass,所以通过 LaunchScreen 就可以简单解决适配当下以及未来各种屏幕尺寸。 Apple官方已经发出公告,2020年4月开始,所有使用iOS 13 SDK 的App都必须提供 LaunchScreen。 创建一个 LaunchScreen 也非常简单 (1)New Files创建一个 LaunchScreen,在创建的 ViewController 下 View 中新建一个 Image,并配置 Image 的图片 (2)调整 Image 的 frame 为占满屏幕,
在iOS13之前,无需权限提示窗即可直接使用蓝牙,但在iOS 13下,新增了使用蓝牙的权限申请。最近一段时间上传IPA包至App Store会收到以下提示。
解决方案:只需要在 Info.plist
里增加以下条目:
NSBluetoothAlwaysUsageDescription
这里输入使用蓝牙来做什么 `
在iOS 13系统中,Apple要求提供第三方登录的App也要支持「Sign With Apple」,具体实践参考 iOS Sign With Apple实践
适配方案: 目的是要将系统返回 NSData
类型数据转换成字符串,再传给推送服务方。-(void)description;
本身是用于为类调试提供相关的打印信息,严格来说,不应直接从该方法获取数据并应用于正式环境中。将 NSData
转换成 HexString
,即可满足适配需求。
- (NSString *)getHexStringForData:(NSData *)data {
NSUInteger length = [data length];
char *chars = (char *)[data bytes];
NSMutableString *hexString = [[NSMutableString alloc] init];
for (NSUInteger i = 0; i < length; i++) {
[hexString appendString:[NSString stringWithFormat:@"%0.2hhx", chars[i]]];
}
return hexString;
}
原本可以直接将 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
- (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);
}
主要还是参照了Apple官方的 UIKit 修改文档声明。iOS 13 Release Notes
iOS 13下设置 cell.contentView.backgroundColor
会直接影响 cell 本身 selected 与 highlighted 效果。 建议不要对 contentView.backgroundColor
修改,而对 cell
本身进行设置。
iOS 13之后,Badge 字体默认由13号变为17号。 建议在初始化 TabbarController 时,显示 Badge 的 ViewController 调用 setBadgeTextAttributes:forState:
方法
if (@available(iOS 13, *)) {
[viewController.tabBarItem setBadgeTextAttributes:@{NSFontAttributeName: [UIFont systemFontOfSize:13]} forState:UIControlStateNormal];
[viewController.tabBarItem setBadgeTextAttributes:@{NSFontAttributeName: [UIFont systemFontOfSize:13]} forState:UIControlStateSelected];
}
NSData *data = [NSData dataWithContentsOfFile:path];
CGImageSourceRef gifSource = CGImageSourceCreateWithData(CFBridgingRetain(data), nil);
size_t gifCount = CGImageSourceGetCount(gifSource);
CGImageRef imageRef = CGImageSourceCreateImageAtIndex(gifSource, i,NULL);
// iOS 13之前
UIImage *image = [UIImage imageWithCGImage:imageRef]
// iOS 13之后添加scale比例(该imageView将展示该动图效果)
UIImage *image = [UIImage imageWithCGImage:imageRef scale:image.size.width / CGRectGetWidth(imageView.frame) orientation:UIImageOrientationUp];
CGImageRelease(imageRef);
iOS 13下不需要调整 imageInsets
,图片会自动居中显示,因此只需要针对iOS 13之前的做适配即可。
if (IOS_VERSION < 13.0) {
viewController.tabBarItem.imageInsets = UIEdgeInsetsMake(5, 0, -5, 0);
}
复制代码
在 iOS 13下设置 tabbarItem 字体选中状态的颜色,在push到其它 ViewController 再返回时,选中状态的 tabbarItem 颜色会变成默认的蓝色。
设置 tabbar 的 tintColor 属性为原本选中状态的颜色即可。
self.tabBar.tintColor = [UIColor redColor];
复制代码
在 iOS 13下,对 UITableView 与 UICollectionView 新增了一套 Diffable DataSource API。为了更高效地更新数据源刷新列表,避免了原有粗暴的刷新方法 - (void)reloadData
,以及手动调用控制列表刷新范围的api,很容易出现计算不准确造成 NSInternalInconsistencyException 而引发App crash。 api 官方链接
StatusBar 新增一种样式,默认的 default 由之前的黑色字体,变为根据系统模式自动选择展示 lightContent 或者 darkContent
之前为了处理搜索框的黑线问题,通常会遍历 searchBar 的 subViews,找到并删除 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;
}
}
从 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;
}
}
}
在 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
的组合方案。
在 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
作为视频播放控件。
本人也不清楚是什么原因 建个分类坐下方法替换就可以了,目前还没有发现有什么问题
创建一个NSObject的分类
代码如下
#import "NSObject+Extend.h"
@implementation NSObject (Extend)
+ (void)load{
SEL originalSelector = @selector(doesNotRecognizeSelector:);
SEL swizzledSelector = @selector(sw_doesNotRecognizeSelector:);
Method originalMethod = class_getClassMethod(self, originalSelector);
Method swizzledMethod = class_getClassMethod(self, swizzledSelector);
if(class_addMethod(self, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod))){
class_replaceMethod(self, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
}else{
method_exchangeImplementations(originalMethod, swizzledMethod);
}
}
+ (void)sw_doesNotRecognizeSelector:(SEL)aSelector {
//处理 _LSDefaults 崩溃问题
if([[self description] isEqualToString:@"_LSDefaults"] && (aSelector == @selector(sharedInstance))){
//冷处理...
return;
}
[self sw_doesNotRecognizeSelector:aSelector];
}