IOS13适配攻略

1. KVC访问私有属性

2. 模态弹窗ViewController 默认样式改变

3. 黑暗模式的适配

4. LaunchImage即将废弃

5. 新增一直使用蓝牙的权限申请

6. Sign With Apple

7. 推送Device Token适配

8. UIKit 控件变化

9. StatusBar新增样式

10. 部分崩溃

 

1. KVC访问私有属性

 此次iOS 13系统升级,影响范围最广的应属KVC访问修改私有属性了,直接禁止开发者获取或直接设置私有属性。而KVC的初衷是容许开发者经过Key名直接访问修改对象的属性值,为其中最典型的 UITextField 的 _placeholderLabelUISearchBar 的 _searchField。 形成影响:在iOS 13下App闪退 错误代码:html

// 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"];

解决方案:ios

一、简单粗暴去掉下划线_api

一、if ([UIDevice currentDevice].systemVersion.floatValue >= 13.0) {
   [mobileNum setValue:[UIFont boldSystemFontOfSize:13] forKeyPath:@"placeholderLabel.font"];
}

二、解决方案:  使用 NSMutableAttributedString 富文原本替代KVC访问 UITextField 的 _placeholderLabel网络

textField.attributedPlaceholder = [[NSAttributedString alloc] initWithString:@"placeholder" attributes:@{NSForegroundColorAttributeName: [UIColor darkGrayColor], NSFontAttributeName: [UIFont systemFontOfSize:13]}];

三、能够添加。Category 本人以为麻烦就不写了app

 

2. 模态弹窗 ViewController 默认样式改变

 模态弹窗属性 UIModalPresentationStyle 在 iOS 13 下默认被设置为 UIModalPresentationAutomatic新特性,展现样式更为炫酷,同时可用下拉手势关闭模态弹窗。 若原有模态弹出 ViewController 时都已指定模态弹窗属性,则能够无视该改动。 若想在 iOS 13 中继续保持原有默认模态弹窗效果。能够经过 runtime 的 Method Swizzling 方法交换来实现。ide

模态全局设置函数

若是视差效果的样式能够接受的话,就不须要修改;若是须要改回全屏显示的界面,须要手动设置弹出样式:工具

- (UIModalPresentationStyle)modalPresentationStyle {
    return UIModalPresentationFullScreen;
}

3. 黑暗模式的适配布局

 针对黑暗模式的推出,Apple官方推荐全部三方App尽快适配。目前并无强制App进行黑暗模式适配。所以黑暗模式适配范围如今可采用如下三种策略:字体

  • 全局关闭黑暗模式
  • 指定页面关闭黑暗模式
  • 全局适配黑暗模式

3.1. 全局关闭黑暗模式

方案一:在项目 Info.plist 文件中,添加一条内容,Key为 User Interface Style,值类型设置为String并设置为 Light 便可。

方案二:代码强制关闭黑暗模式,将当前 window 设置为 Light 状态。

if(@available(iOS 13.0,*)){ 

    self.window.overrideUserInterfaceStyle = UIUserInterfaceStyleLight; 

}

3.2 指定页面关闭黑暗模式

 从Xcode 十一、iOS 13开始,UIViewController与View新增属性 overrideUserInterfaceStyle,若设置View对象该属性为指定模式,则强制该对象以及子对象以指定模式展现,不会跟随系统模式改变。

  • 设置 ViewController 该属性, 将会影响视图控制器的视图以及子视图控制器都采用该模式
  • 设置 View 该属性, 将会影响视图及其全部子视图采用该模式
  • 设置 Window 该属性, 将会影响窗口中的全部内容都采用该样式,包括根视图控制器和在该窗口中显示内容的全部控制器

 

3.3 全局适配黑暗模式

适配黑暗模式,主要从两方面入手:图片资源适配与颜色适配


图片资源适配

 打开图片资源管理库 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);

  • 这两个方法要求传一个block,block会返回一个 UITraitCollection 类
  • 当系统在黑暗模式与正常模式切换时,会触发block回调 示例代码:
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...
    }
    }

 

 

4. LaunchImage即将废弃

使用 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 为占满屏幕,

 

5. 新增一直使用蓝牙的权限申请

 在iOS13以前,无需权限提示窗便可直接使用蓝牙,但在iOS 13下,新增了使用蓝牙的权限申请。最近一段时间上传IPA包至App Store会收到如下提示。

解决方案:只须要在 Info.plist 里增长如下条目:

<key>NSBluetoothAlwaysUsageDescription</key> 
<string>这里输入使用蓝牙来作什么</string>`

6. Sign With Apple

 在iOS 13系统中,Apple要求提供第三方登陆的App也要支持「Sign With Apple」,具体实践参考 iOS Sign With Apple实践

7. 推送Device Token适配

适配方案: 目的是要将系统返回 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;
}

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);
}

 

8. UIKit 控件变化

主要仍是参照了Apple官方的 UIKit 修改文档声明。iOS 13 Release Notes
 

8.1. UITableView

iOS 13下设置 cell.contentView.backgroundColor 会直接影响 cell 自己 selected 与 highlighted 效果。 建议不要对 contentView.backgroundColor 修改,而对 cell 自己进行设置。

8.2. UITabbar

Badge 文字大小变化

 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];
}

8.2. UITabBarItem

加载gif需设置 scale 比例

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);
  }
复制代码

TabBarItem选中颜色异常

 在 iOS 13下设置 tabbarItem 字体选中状态的颜色,在push到其它 ViewController 再返回时,选中状态的 tabbarItem 颜色会变成默认的蓝色。

设置 tabbar 的 tintColor 属性为本来选中状态的颜色便可。

self.tabBar.tintColor = [UIColor redColor];
复制代码

8.3. 新增 Diffable DataSource

 在 iOS 13下,对 UITableView 与 UICollectionView 新增了一套 Diffable DataSource API。为了更高效地更新数据源刷新列表,避免了原有粗暴的刷新方法 - (void)reloadData,以及手动调用控制列表刷新范围的api,很容易出现计算不许确形成 NSInternalInconsistencyException 而引起App crash。 api 官方连接

9. StatusBar新增样式

 StatusBar 新增一种样式,默认的 default 由以前的黑色字体,变为根据系统模式自动选择展现 lightContent 或者 darkContent

10. 部分崩溃

 

1. UISearchBar 黑线处理致使崩溃

以前为了处理搜索框的黑线问题,一般会遍历 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;
    }
}

2. 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;
        }
    }
}
复制代码

然而,这种作法在 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;
        }
    }
}

 

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

4. 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 做为视频播放控件。

5._LSDefaults 崩溃
 

本人也不清楚是什么缘由  建个分类坐下方法替换就能够了,目前尚未发现有什么问题
建立一个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];
}