iOS13适配指南

iOS13系统上线后,我遇到了哪些问题?

新系统的发布,xcode的第一时间更新带来的问题

11月6日,除iOS13.3与iPadOS13.3开发者预览版Beta1外,苹果面向开发人员发布了Xcode11.2.1紧急更新。Xcode是用于开发iOS、macOS、watchOS和tvOS应用程序的工具。本次更新距Xcode 11.2发布仅一周时间。

  • 苹果表示,在早期版本的iOS、iPadOS或tvOS运行时,使用UITextView的应用程序可能会崩溃。最新的Xcode 11.2.1解决了这一问题。

  • 在我用商店下载的xcode11.2打包上架时候,苹果会拒绝并要求升级到11.2.1以上版本,并提示遭遇这一问题的开发人员应尽快更新Xcode。新版本目前在Apple Developer网站上可用,并很快将在Mac App Store上提供。(这句话扯到什么地步呢,也就是在我从开发者网站下载bete版本以后的几天,我都没发现Mac App store有更新 11.2.1)。

    谈谈适配问题

    1. 支持的机型
  • 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层面

2. Dark(黑夜模式)

  • iOS 13 推出暗黑模式,UIKit 提供新的系统颜色和 api 来适配不同颜色模式,xcassets 对素材适配也做了调整,官方具体适配可见: Implementing Dark Mode on iOS

  • iOS13-适配夜间模式/深色外观(Dark Mode)怎么动态改变颜色.

3. 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 上提到正式发布时要求加上,具体发布时间还没确定。但苹果同样举出了几种特例的情况:

  • 仅使用公司内部账号登陆。
  • 教育或者企业应用,需要使用现有的教育和企业帐号登录。
  • 应用需要使用政府或者行业背景的共名身份系统或者 电子ID 进行登录。

4. 模态弹出默认交互改变(必须适配,强制要求)

  • 在 iOS 13 中此枚举值直接成为了模态弹出的默认值,因此 presentViewController 方式打开视图是如下的视差效果,默认是下滑返回。

添加

iOS13下仍然可以做到全屏弹出,这里需要UI决定采用哪种样式:

1
2
//和以前一样全屏幕铺满状态,ios13里面默认会是-2,以前是0
nav.modalPresentationStyle = UIModalPresentationFullScreen;
  • 有一点注意的是,ctr的生命周期方法调用情况会改变,假设有a,b两个ctr,在a中present出b:
    全屏present时(UIModalPresentationFullScreen)的方法调用顺序:

    1
    2
    3
    4
    a---viewWillDisappear:
    b---viewWillAppear:
    b---viewDidAppear:
    a---viewDidDisappear:
    • dissmiss时的方法调用顺序:
      1
      2
      3
      4
      b---viewWillDisappear:
      a---viewWillAppear:
      a---viewDidAppear:
      b---viewDidDisappear:
      非全屏presnet时(UIModalPresentationPageSheet)的方法调用顺序:
    1
    2
    b---viewWillAppear:
    b---viewDidAppear:

    dissmiss时的方法调用顺序:

    1
    2
    b---viewWillDisappear:
    b---viewDidDisappear:

    *可以看出,以UIModalPresentationPageSheet的方式来present/dismiss时,分别少调用了a的两个方法,如果之前在这个位置有相关的逻辑代码,比如网络请求,UI刷新,要注意

5. UISegmentedControl 默认样式改变(必须适配,强制要求)

  • 默认样式变为白底黑字,如果设置修改过颜色的话,页面需要修改。
    添加
    原本设置选中颜色的 tintColor 已经失效,新增了 selectedSegmentTintColor 属性用以修改选中的颜色。

6. Web的适配,参考链接:

代码层面

1. 私有方法 KVC 不允许使用(必须适配,强制要求)

  • 在 iOS 13 中不再允许使用 valueForKey、setValue:forKey: 等方法获取或设置私有属性,虽然编译可以通过,但是在运行时会直接崩溃,并提示一下崩溃信息:
1
2
// 使用的私有方法
[_textField setValue:[UIColor redColor] forKeyPath:@"_placeholderLabel.textColor"];
1
2
// 崩溃提示信息
*** Terminating app due to uncaught exception 'NSGenericException', reason: 'Access to UITextField's _placeholderLabel ivar is prohibited. This is an application bug'

解决方案:

1
2
// 替换的方案
_textField.attributedPlaceholder = [[NSAttributedString alloc] initWithString:@"输入"attributes:@{NSForegroundColorAttributeName: [UIColor redColor]}];

2. 推送的 deviceToken 获取到的格式发生变化(必须适配,强制要求)

  • 原本可以直接将 NSData 类型的 deviceToken 转换成 NSString 字符串,然后替换掉多余的符号即可:
1
2
3
4
5
6
7
- (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 转换成的字符串变成了:

    1
    {length = 32, bytes = 0xd7f9fe34 69be14d1 fa51be22 329ac80d ... 5ad13017 b8ad0736 }
  • 需要进行一次数据格式处理,参考友盟的做法,可以适配新旧系统,获取方式如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    #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 渲染失败,然后直接崩溃,崩溃信息如下:

    1
    *** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Missing or detached view for search bar layout'
    • 解决办法是设置 UISearchBarBackground 的 layer.contents 为 nil:
      1
      2
      3
      4
      5
      6
      7
      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 会直接导致崩溃,崩溃信息如下:
1
*** 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 中终于可以获取直接获取搜索的文本框:
    1
    _searchBar.searchTextField.text = @“search";

    5. MPMoviePlayerController 被弃用

  • 在 iOS 9 之前播放视频可以使用 MediaPlayer.framework 中的MPMoviePlayerController类来完成,它支持本地视频和网络视频播放。但是在 iOS 9 开始被弃用,如果在 iOS 13 中继续使用的话会直接抛出异常:
    1
    *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'MPMoviePlayerController is no longer available. Use AVPlayerViewController in AVKit.'

    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 的头文件加上:

1
@property (strong, nonatomic) UIWindow *window;

8. 使用 @available 导致旧版本 Xcode 编译出错。(必须)

在 Xcode 11 的 SDK 工程的代码里面使用了 @available 判断当前系统版本,打出来的包放在 Xcode 10 中编译,会出现一下错误:

1
2
3
4
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,那么需要避开此方法,通过获取系统版本来进行判断:

1
2
3
if ([UIDevice currentDevice].systemVersion.floatValue >= 13.0) {
...
}

另外,在 Xcode 10 上打开 SDK 工程也应该可以正常编译,这就需要加上编译宏进行处理:

1
2
3
4
5
6
7
#ifndef __IPHONE_13_0
#define __IPHONE_13_0 130000
#endif

#if __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_13_0
...
#endif

9. NSAttributedString优化

  • 对于UILabel、UITextField、UITextView,在设置NSAttributedString时也要考虑适配Dark Mode,否则在切换模式时会与背景色融合,造成不好的体验/推荐的做法:
    1
    2
    3
    // 添加一个NSForegroundColorAttributeName属性
    NSDictionary *dic = @{NSFontAttributeName:[UIFont systemFontOfSize:16],NSForegroundColorAttributeName:[UIColor labelColor]};
    NSAttributedString *str = [[NSAttributedString alloc] initWithString:@"富文本文案" attributes:dic];

    10. 废弃UIWebView(必须)

  • UIWebView在12.0就已经被废弃,部分APP使用webview时, 审核被拒
    目前提交苹果应用市场(App Store)会反馈以下邮件提示:
    1
    2
    ITMS-90809: Deprecated API Usage - Apple will stop accepting submissions of apps that use UIWebView APIs .
    See developer.apple.com/documentati… for more information.
    查找哪些SDK包含了 UIWebView
    1
    find . -type f | grep -e ".a" -e ".framework" | xargs grep -s UIWebView
    目前常用的SDK中,已知更新移除 UIWebView 的版本有:
  • QQ登录 (v3.3.6)
  • ShareSDK (v4.3.2)
  • 极验证 (v0.12.5)
  • 新浪微博(v3.2.5)
  • 微信开放平台 (v1.8.6.1)

暂未更新移除 UIWebView 版本的 SDK :

11. WKWebView 中测量页面内容高度的方式变更

  • iOS 13以前 document.body.scrollHeight iOS 13中 document.documentElement.scrollHeight 两者相差55 应该是浏览器定义高度变了

12. 蓝牙权限需要申请

  • CBCentralManager,iOS13以前,使用蓝牙时可以直接用,不会出现权限提示,iOS13后,再使用就会提示了。 在info.plist里增加

    1
    2
    <key>NSBluetoothAlwaysUsageDescription</key> 
    <string>我们要一直使用您的蓝牙</string>
  • 在iOS13中,蓝牙变成了和位置,通知服务等同样的可以针对单个app授权的服务。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    - (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;
    }

    13. CNCopyCurrentNetworkInfo

  • iOS13 以后只有开启了 Access WiFi Information capability,才能获取到 SSID 和 BSSID wi-fi or wlan 相关使用变更

最近收到了苹果的邮件,说获取WiFi SSID的接口CNCopyCurrentNetworkInfo 不再返回SSID的值。不仔细看还真会被吓一跳,对物联网的相关APP简直是炸弹。仔细看邮件还好说明了可以先获取用户位置权限才能返回SSID。
注意:目本身已经打开位置权限,则可以直接获取

14. MJExtension 问题

  • Stack overflow in +[NSObject(Property) mj_properties] 升级 MJExtension 至 3.1.0版本以上,弃用老方法,涉及到所有方法添加 mj_ 前缀。