iOS 13 适配之旅(上)

前段时间官方开放了 iOS 13.0 的公测版本,不得不说,今年的 iOS 带给开发者的 “惊喜” 还真是蛮多的呀,不多说了,直接进入主题,开始进行新系统适配工作吧。首先,来看看 iOS 13 给开发者带来的变化:

  • Dark Model 黑暗模式来袭
  • Apple ID 快捷登录
  • SF Symbols 系统内置图标库
  • KVC 使用限制
  • PresentViewController 默认跳转交互方式更改
  • 3D Touch 按压力度变化
  • UISegmentedControl 默认样式改变
  • CNCopyCurrentNetworkInfo 变化
  • UITabbar 层次发生改变,无法通过设置 shadowImage 去掉上面的线
  • App 启动过程中,部分 View 可能无法实时获取到 Frame
  • UIActivityIndicatorView style 变更
  • UIStatusBar style 变更

Dark Model

这应该是 iOS 13 中最磨人的一个适配项了。
Dark Model 适配主要有以下几点: 1. UIColor 2. UIImage 3. UIBlurEffect
并且苹果规定: iOS 13.0 正式版发布以后,所有项目**必须强制适配**该功能,适配任务刻不容缓。

1. UIColor 适配

iOS 13 中,UIColor 不再是一种颜色,而是一个动态颜色。在 Light Mode 和 Dark Mode 可以分别设置不同的颜色。以下是官方提供的一些动态色:

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
@property (class, nonatomic, readonly) UIColor *systemIndigoColor       API_AVAILABLE(ios(13.0), tvos(13.0)) API_UNAVAILABLE(watchos);
@property (class, nonatomic, readonly) UIColor *systemBrownColor API_AVAILABLE(ios(13.0), tvos(13.0)) API_UNAVAILABLE(watchos);
@property (class, nonatomic, readonly) UIColor *systemGray2Color API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(tvos, watchos);
@property (class, nonatomic, readonly) UIColor *systemGray3Color API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(tvos, watchos);
@property (class, nonatomic, readonly) UIColor *systemGray4Color API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(tvos, watchos);
@property (class, nonatomic, readonly) UIColor *systemGray5Color API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(tvos, watchos);
@property (class, nonatomic, readonly) UIColor *systemGray6Color API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(tvos, watchos);
@property (class, nonatomic, readonly) UIColor *labelColor API_AVAILABLE(ios(13.0), tvos(13.0)) API_UNAVAILABLE(watchos);
@property (class, nonatomic, readonly) UIColor *secondaryLabelColor API_AVAILABLE(ios(13.0), tvos(13.0)) API_UNAVAILABLE(watchos);
@property (class, nonatomic, readonly) UIColor *tertiaryLabelColor API_AVAILABLE(ios(13.0), tvos(13.0)) API_UNAVAILABLE(watchos);
@property (class, nonatomic, readonly) UIColor *quaternaryLabelColor API_AVAILABLE(ios(13.0), tvos(13.0)) API_UNAVAILABLE(watchos);
@property (class, nonatomic, readonly) UIColor *linkColor API_AVAILABLE(ios(13.0), tvos(13.0)) API_UNAVAILABLE(watchos);
@property (class, nonatomic, readonly) UIColor *placeholderTextColor API_AVAILABLE(ios(13.0), tvos(13.0)) API_UNAVAILABLE(watchos);
@property (class, nonatomic, readonly) UIColor *separatorColor API_AVAILABLE(ios(13.0), tvos(13.0)) API_UNAVAILABLE(watchos);
@property (class, nonatomic, readonly) UIColor *opaqueSeparatorColor API_AVAILABLE(ios(13.0), tvos(13.0)) API_UNAVAILABLE(watchos);
@property (class, nonatomic, readonly) UIColor *systemBackgroundColor API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(tvos, watchos);
@property (class, nonatomic, readonly) UIColor *secondarySystemBackgroundColor API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(tvos, watchos);
@property (class, nonatomic, readonly) UIColor *tertiarySystemBackgroundColor API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(tvos, watchos);
@property (class, nonatomic, readonly) UIColor *systemGroupedBackgroundColor API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(tvos, watchos);
@property (class, nonatomic, readonly) UIColor *secondarySystemGroupedBackgroundColor API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(tvos, watchos);
@property (class, nonatomic, readonly) UIColor *tertiarySystemGroupedBackgroundColor API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(tvos, watchos);
@property (class, nonatomic, readonly) UIColor *systemFillColor API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(tvos, watchos);
@property (class, nonatomic, readonly) UIColor *secondarySystemFillColor API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(tvos, watchos);
@property (class, nonatomic, readonly) UIColor *tertiarySystemFillColor API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(tvos, watchos);
@property (class, nonatomic, readonly) UIColor *quaternarySystemFillColor API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(tvos, watchos);

系统提供的动态 UIColor 样式和之前方法一样,不需要其他的适配操作。但在实际开发过程,系统提供的这些颜色还远远不够,因此我们需要创建更多的动态颜色。

在 iOS 13 中 UIColor 增加了两个初始化方法,使用以下方法可以创建动态 UIColor。

1
2
+ (UIColor *)colorWithDynamicProvider:(UIColor * (^)(UITraitCollection *traitCollection))dynamicProvider API_AVAILABLE(ios(13.0), tvos(13.0)) API_UNAVAILABLE(watchos);
- (UIColor *)initWithDynamicProvider:(UIColor * (^)(UITraitCollection *traitCollection))dynamicProvider API_AVAILABLE(ios(13.0), tvos(13.0)) API_UNAVAILABLE(watchos);
  • 这两个方法要求传一个 block 进去
  • 当系统在 LightMode 和 DarkMode 之间相互切换时就会触发此回调
  • 这个 block 会返回一个 UITraitCollection 类
  • 我们需要使用其属性 userInterfaceStyle,它是一个枚举类型,会告诉我们当前是 LightMode 还是DarkMode
1
2
3
4
5
typedef NS_ENUM(NSInteger, UIUserInterfaceStyle) {
UIUserInterfaceStyleUnspecified,
UIUserInterfaceStyleLight,
UIUserInterfaceStyleDark,
} API_AVAILABLE(tvos(10.0)) API_AVAILABLE(ios(12.0)) API_UNAVAILABLE(watchos);

实际使用代码演示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
+ (UIColor *)darkWhiteColor {
UIColor *color = [UIColor whiteColor];
#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 130000
if (@available(iOS 13.0, *)) {
color = [UIColor colorWithDynamicProvider:^UIColor * _Nonnull(UITraitCollection * _Nonnull trainCollection) {
if ([trainCollection userInterfaceStyle] == UIUserInterfaceStyleDark) {
return [UIColor blackColor];
}
return [UIColor whiteColor];
}];
}
#endif
return color;
}

2. UIImage 适配

  • 打开 Assets.xcassets
  • 打开右侧工具栏,点击最后一栏,找到 Appearances,选择 Any,Dark
  • 将两种模式下不同的图片资源都拖进去

或者,你可以这样写:

1
2
3
4
5
6
7
+ (UIImage *)imageWithNamed:(NSString *)name drakImageWithName:(NSString *)drakName {
UIImage *resultImage = [UIImage imageNamed:name];
if (UITraitCollection.currentTraitCollection.userInterfaceStyle == UIUserInterfaceStyleDark) {
resultImage = [UIImage imageNamed:drakName];
}
return resultImage;
}

3. UIBlurEffect 适配

iOS 13 中,UIBlurEffect 增加了几个新的枚举值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
UIBlurEffectStyleSystemUltraThinMaterial        API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(watchos, tvos),
UIBlurEffectStyleSystemThinMaterial API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(watchos, tvos),
UIBlurEffectStyleSystemMaterial API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(watchos, tvos),
UIBlurEffectStyleSystemThickMaterial API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(watchos, tvos),
UIBlurEffectStyleSystemChromeMaterial API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(watchos, tvos),

UIBlurEffectStyleSystemUltraThinMaterialLight API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(watchos, tvos),
UIBlurEffectStyleSystemThinMaterialLight API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(watchos, tvos),
UIBlurEffectStyleSystemMaterialLight API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(watchos, tvos),
UIBlurEffectStyleSystemThickMaterialLight API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(watchos, tvos),
UIBlurEffectStyleSystemChromeMaterialLight API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(watchos, tvos),

UIBlurEffectStyleSystemUltraThinMaterialDark API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(watchos, tvos),
UIBlurEffectStyleSystemThinMaterialDark API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(watchos, tvos),
UIBlurEffectStyleSystemMaterialDark API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(watchos, tvos),
UIBlurEffectStyleSystemThickMaterialDark API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(watchos, tvos),
UIBlurEffectStyleSystemChromeMaterialDark API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(watchos, tvos)

其中,最上面的 5 项是会根据系统模式改变而自动而改变的(类似于动态 UIColor),后面的则是单一的模式。

实际使用代码演示

1
2
3
4
5
6
UIBlurEffect *effect = [UIBlurEffect effectWithStyle:UIBlurEffectStyleLight];
#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 130000
if (@available(iOS 13.0, *)) {
effect = [UIBlurEffect effectWithStyle:UIBlurEffectStyleSystemMaterial];
}
#endif

4. 其他

获取当前模式

1
2
3
4
5
if (UITraitCollection.currentTraitCollection.userInterfaceStyle == UIUserInterfaceStyleDark) {
NSLog(@"DarkMode");
} else {
NSLog(@"LightMode");
}

监听模式切换

重写方法:

1
2
3
4
5
6
7
8
- (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection {
[super traitCollectionDidChange:previousTraitCollection];
UIImage *resultImage = [UIImage imageNamed:@“name”];
if (UITraitCollection.currentTraitCollection.userInterfaceStyle == UIUserInterfaceStyleDark) {
resultImage = [UIImage imageNamed:@“drakName”];
}
self.imageView.image = resultImage;
}

如何在模式切换时打印日志

在 Arguments 中的 Arguments Passed On Launch 里面添加下面这行命令。

UITraitCollectionChangeLoggingEnabled YES

Sign In with Apple

Sign In with Apple 也是 iOS 13 中的一个特色,或许是苹果爸爸眼红了三方登录市场,要求所有的 App 凡是有微信、微博、Facebook、Google 等第三方平台登陆的功能,都**必须强制增加** Sign In with Apple,并且要排在前面,在一屏内必须能发现其位置,不可通过滑动界面才能看到。Apple 为开发者提供了三种样式,更多资料见官方文档这里就不再过多介绍。

使用 Sign In With Apple 的流程为:

  1. 设置 ASAuthorizationAppleIDButton 相关布局,添加相应地授权处理;

  2. 获取授权码;

  3. 验证;

  4. 处理 Sign In With Apple 授权状态变化;

注意事项

  • 和往常一样,需要在苹果后台打开 Sign In with Apple 选项,并且重新生成 Profiles 配置文件,并安装到 Xcode 11
  • 在 Xcode 11 的 Signing & Capabilities中 添加 Sign In With Apple

如图所示:

开发工作

  1. 导入 #import <AuthenticationServices/AuthenticationServices.h>
  2. 遵循 ASAuthorizationControllerPresentationContextProvidingASAuthorizationControllerDelegate 协议
1
2
3
4
5
6
if (@available(iOS 13.0, *)) { 
ASAuthorizationAppleIDButton *appleIDBtn = [ASAuthorizationAppleIDButton buttonWithType:ASAuthorizationAppleIDButtonTypeDefault style:ASAuthorizationAppleIDButtonStyleWhite]; appleIDBtn.frame = CGRectMake(30, 180, self.view.bounds.size.width - 60, 60);
appleBtn.cornerRadius = 22.f;
[appleIDBtn addTarget:self action:@selector(handleAuthorizationAppleIDButtonPress:) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:appleIDBtn];
}

系统为我们提供了三种样式,分别是白底黑字无边框、白底黑字有边框、黑底白字无边框:

  • ASAuthorizationAppleIDButtonStyleWhite,
  • ASAuthorizationAppleIDButtonStyleWhiteOutline,
  • ASAuthorizationAppleIDButtonStyleBlack,
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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 130000

- (void)handleAuthorizationAppleIDButtonPress:(ASAuthorizationAppleIDButton *)sender API_AVAILABLE(ios(13.0)) {

if (@available(iOS 13.0, *)) {
// 基于用户的Apple ID授权用户,生成用户授权请求的一种机制
ASAuthorizationAppleIDProvider *appleIDProvider = [[ASAuthorizationAppleIDProvider alloc] init];
// 创建新的AppleID 授权请求
ASAuthorizationAppleIDRequest *appleIDRequest = [appleIDProvider createRequest];
// 在用户授权期间请求的联系信息
appleIDRequest.requestedScopes = @[ASAuthorizationScopeFullName, ASAuthorizationScopeEmail];
// 由ASAuthorizationAppleIDProvider创建的授权请求 管理授权请求的控制器
ASAuthorizationController *authorizationController = [[ASAuthorizationController alloc] initWithAuthorizationRequests:@[appleIDRequest]];
// 设置授权控制器通知授权请求的成功与失败的代理
authorizationController.delegate = self;
// 设置提供 展示上下文的代理,在这个上下文中 系统可以展示授权界面给用户
authorizationController.presentationContextProvider = self;
// 在控制器初始化期间启动授权流
[authorizationController performRequests];
}
}


#pragma mark - delegate

//授权成功地回调
- (void)authorizationController:(ASAuthorizationController *)controller didCompleteWithAuthorization:(ASAuthorization *)authorization API_AVAILABLE(ios(13.0)) {
NSLog(@"授权完成:::%@", authorization.credential);
NSLog(@"%s", __FUNCTION__);
NSLog(@"%@", controller);
NSLog(@"%@", authorization);

if ([authorization.credential isKindOfClass:[ASAuthorizationAppleIDCredential class]]) {
ASAuthorizationAppleIDCredential *appleIDCredential = authorization.credential;
NSString *user = appleIDCredential.user;
NSString *familyName = appleIDCredential.fullName.familyName;
NSString *givenName = appleIDCredential.fullName.givenName;
NSString *email = appleIDCredential.email;
// NSData *identityToken = appleIDCredential.identityToken;
// NSData *authorizationCode = appleIDCredential.authorizationCode;

NSLog(@"user -> %@, familyName -> %@, givenName -> %@, email -> %@", user, familyName, givenName, email);

} else if ([authorization.credential isKindOfClass:[ASPasswordCredential class]])
{
// 用户登录使用现有的密码凭证
ASPasswordCredential *passwordCredential = authorization.credential;
// 密码凭证对象的用户标识 用户的唯一标识
NSString *user = passwordCredential.user;
// 密码凭证对象的密码
NSString *password = passwordCredential.password;

NSLog(@"user -> %@, password -> %@", user, password);

} else
{
NSLog(@"授权信息均不符");
}
}

// 授权失败的回调
- (void)authorizationController:(ASAuthorizationController *)controller didCompleteWithError:(NSError *)error API_AVAILABLE(ios(13.0)) {

NSLog(@"Handle error:%@", error);
NSString *errorMsg = nil;
switch (error.code) {
case ASAuthorizationErrorCanceled:
errorMsg = @"用户取消了授权请求";
break;
case ASAuthorizationErrorFailed:
errorMsg = @"授权请求失败";
break;
case ASAuthorizationErrorInvalidResponse:
errorMsg = @"授权请求响应无效";
break;
case ASAuthorizationErrorNotHandled:
errorMsg = @"未能处理授权请求";
break;
case ASAuthorizationErrorUnknown:
errorMsg = @"授权请求失败未知原因";
break;

default:
break;
}

NSMutableString *mStr = [NSMutableString string];
[mStr appendString:@"\n"];
[mStr appendString:errorMsg];
[mStr appendString:@"\n"];
NSLog(@"error ->>>> %@", mStr);
}

// 告诉代理应该在哪个window 展示内容给用户
- (ASPresentationAnchor)presentationAnchorForAuthorizationController:(ASAuthorizationController *)controller API_AVAILABLE(ios(13.0)) {
return self.view.window;
}

#endif

在授权登录成功回调中,我们可以拿到以下几类数据

  • UserID: 苹果用户唯一标识符,该值在同一个开发者账号下的所有 App 下是一样的,开发者可以用该唯一标识符与自己后台系统的账号体系绑定起来(这与国内的微信、QQ、微博等第三方登录流程基本一致)
  • Verification data: Identity token, code,验证数据,用于传给开发者后台服务器,然后开发者服务器再向苹果的身份验证服务端验证,本次授权登录请求数据的有效性和真实性,详见 Sign In with Apple REST API
  • Account information: 苹果用户信息,包括全名、邮箱等,注意:如果用户登录时拒绝提供真实的邮箱账号,苹果会生成虚拟的邮箱账号
  • 验证: 关于验证的这一步,需要传递授权码给自己的服务端,自己的服务端调用苹果 API 去校验授权码 Generate and validate tokens。如果验证成功,可以根据 userIdentifier 判断账号是否已存在,若存在,则返回自己账号系统的登录态,若不存在,则创建一个新的账号,并返回对应的登录状态给 App.

监听授权状态变化

监听授权状态改变,并且做出相应处理。授权状态有:

  • ASAuthorizationAppleIDProviderCredentialRevoked 授权状态失效(用户停止使用AppID 登录App)
  • ASAuthorizationAppleIDProviderCredentialAuthorized 已授权(已使用AppleID 登录过App)
  • ASAuthorizationAppleIDProviderCredentialNotFound 授权凭证缺失(可能是使用AppleID 登录过App)

处理改变有 2 种处理方式,一种是通过通知的方式,另一种是监听当前的 appleIDCredential.user 的授权状态。

  1. 监听 appleIDCredential.user 的授权状态
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
38
#pragma mark - Private functions
//! 观察授权状态
- (void)observeAuthticationState {

if (@available(iOS 13.0, *)) {
ASAuthorizationAppleIDProvider *appleIDProvider = [ASAuthorizationAppleIDProvider new];
// 注意 存储用户标识信息需要使用钥匙串来存储 这里笔者简单期间 使用NSUserDefaults 做的简单示例
NSString *userIdentifier = [[NSUserDefaults standardUserDefaults] valueForKey:QiShareCurrentIdentifier];

if (userIdentifier) {
NSString* __block errorMsg = nil;
// 在回调中返回用户的授权状态
[appleIDProvider getCredentialStateForUserID:userIdentifier completion:^(ASAuthorizationAppleIDProviderCredentialState credentialState, NSError * _Nullable error) {
switch (credentialState) {
// 苹果证书的授权状态
case ASAuthorizationAppleIDProviderCredentialRevoked:
// 苹果授权凭证失效
errorMsg = @"苹果授权凭证失效";
break;
case ASAuthorizationAppleIDProviderCredentialAuthorized:
// 苹果授权凭证状态良好
errorMsg = @"苹果授权凭证状态良好";
break;
case ASAuthorizationAppleIDProviderCredentialNotFound:
// 未发现苹果授权凭证
errorMsg = @"未发现苹果授权凭证";
// 可以引导用户重新登录
break;
}
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"SignInWithApple授权状态变化情况");
NSLog(@"%@", errorMsg);
});
}];

}
}
}

建议这部分代码可以放到 AppDelegate 的 - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions 中,判断是否需要展示出登录控制器.

  1. 使用通知的方式检测是否授权应用支持 Sign In With Apple 变化情况。如下的代码可以根据自己的业务场景去考虑放置的位置。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 添加苹果登录的状态通知
- (void)observeAppleSignInState {
if (@available(iOS 13.0, *)) {
NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
[center addObserver:self selector:@selector(handleSignInWithAppleStateChanged:) name:ASAuthorizationAppleIDProviderCredentialRevokedNotification object:nil];
}
}

// 观察SignInWithApple状态改变
- (void)handleSignInWithAppleStateChanged:(id)noti {
NSLog(@"%s", __FUNCTION__);
NSLog(@"%@", noti);
}

- (void)dealloc {
if (@available(iOS 13.0, *)) {
[[NSNotificationCenter defaultCenter] removeObserver:self name:ASAuthorizationAppleIDProviderCredentialRevokedNotification object:nil];
}
}