NBus 之 SystemHandler

第一方支持不要求额外 SDK,处理的时候判断一下系统版本即可。


General

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
public class SystemHandler {

public let endpoints: [Endpoint] = [
Endpoints.System.activity,
]

public let platform: Platform = Platforms.system

public var isInstalled: Bool {
true
}

private var oauthCompletionHandler: Bus.OauthCompletionHandler?

public var logHandler: Bus.LogHandler = { message, _, _, _ in
#if DEBUG
print(message)
#endif
}

private var boxHelper: Any!

@available(iOS 13.0, *)
private var helper: Helper {
boxHelper as! Helper
}

public init() {
if #available(iOS 13.0, *) {
boxHelper = Helper(master: self)
}
}
}

使用 Helper 处理系统的回调,相关内容下文会进行说明。通过 Apple 登录 功能要求 iOS 13.0+,所以使用 boxHelper: Any! 来持有,另外提供 helper 计算属性快速访问。

LogHandlerProxyType

1
extension SystemHandler: LogHandlerProxyType {}

声明 SystemHandler 遵循 LogHandlerProxyType 协议。

ShareHandlerType

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
100
101
extension SystemHandler: ShareHandlerType {

public func share(
message: MessageType,
to endpoint: Endpoint,
options: [Bus.ShareOptionKey: Any] = [:],
completionHandler: @escaping Bus.ShareCompletionHandler
) {
guard
let presentingViewController = options[ShareOptionKeys.presentingViewController] as? UIViewController
?? UIApplication.shared.keyWindow?.rootViewController
else {
assertionFailure()
completionHandler(.failure(.unknown))
return
}

var activityItems: [Any?] = []

if let message = message as? MediaMessageType {
activityItems.append(message.title)
activityItems.append(message.description)
}

switch message {
case let message as TextMessage:
activityItems.append(message.text)

case let message as ImageMessage:
activityItems.append(message.data)

case let message as AudioMessage:
activityItems.append(message.link)

case let message as VideoMessage:
activityItems.append(message.link)

case let message as WebPageMessage:
activityItems.append(message.link)

case let message as FileMessage:
activityItems.append(message.data)

case let message as MiniProgramMessage:
activityItems.append(message.link)

default:
completionHandler(.failure(.unsupportedMessage))
return
}

let activityViewController = UIActivityViewController(
activityItems: activityItems.compactMap { $0 },
applicationActivities: nil
)

activityViewController.completionWithItemsHandler = { _, result, _, error in
switch (result, error) {
case (_, _?):
completionHandler(.failure(.unknown))
case (true, _):
completionHandler(.success(()))
case (false, _):
completionHandler(.failure(.userCancelled))
}
}

if let popoverPresentationController = activityViewController.popoverPresentationController {
guard
let sourceView = options[ShareOptionKeys.sourceView] as? UIView
else {
assertionFailure()
completionHandler(.failure(.unknown))
return
}

popoverPresentationController.sourceView = sourceView

if let sourceRect = options[ShareOptionKeys.sourceRect] as? CGRect {
popoverPresentationController.sourceRect = sourceRect
}
}

presentingViewController.present(
activityViewController,
animated: true
)
}
}

extension SystemHandler {

public enum ShareOptionKeys {

public static let presentingViewController = Bus.ShareOptionKey(rawValue: "com.nuomi1.bus.systemHandler.presentingViewController")

public static let sourceView = Bus.ShareOptionKey(rawValue: "com.nuomi1.bus.systemHandler.sourceView")

public static let sourceRect = Bus.ShareOptionKey(rawValue: "com.nuomi1.bus.systemHandler.sourceRect")
}
}

在分享流程中:

  1. options 中获取 presentingViewController,没有则使用 keyWindow.rootViewController,都获取不到就提前退出。
  2. 创建 activityItems 待分享内容,如果 message 遵循 MediaMessageType 协议,添加 title / description
  3. 判断 message 的具体类型,添加消息的主要内容。
  4. 创建 activityViewController,调用 activityItems.compactMap { $0 } 过滤空数据,设置 completionWithItemsHandler 分享回调。
  5. popoverPresentationController 存在时,设置 sourceView / sourceRect
  6. 调用 presentingViewController.present(_:animated:completion: 拉起系统分享。

OauthHandlerType

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
extension SystemHandler: OauthHandlerType {

public func oauth(
options: [Bus.OauthOptionKey: Any] = [:],
completionHandler: @escaping Bus.OauthCompletionHandler
) {
guard #available(iOS 13.0, *) else {
completionHandler(.failure(.unknown))
return
}

oauthCompletionHandler = completionHandler

let provider = ASAuthorizationAppleIDProvider()

let request = provider.createRequest()
request.requestedScopes = [.email, .fullName]

let controller = ASAuthorizationController(authorizationRequests: [request])
controller.delegate = helper

controller.performRequests()
}
}

在登录流程中:

  1. 调用 #available(iOS 13.0, *) 判断是否可以使用 通过 Apple 登录 功能,不可以则提前退出。
  2. 创建 ASAuthorizationAppleIDProvider 提供者,调用 createRequest() 创建请求,设置 requestedScopes[.email, .fullName]
  3. 创建 ASAuthorizationController 控制器,设置 delegatehelper
  4. 调用 controller.performRequests() 拉起 Apple 登录。

Helper

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
extension SystemHandler {

@available(iOS 13.0, *)
fileprivate class Helper: NSObject, ASAuthorizationControllerDelegate {

weak var master: SystemHandler?

required init(master: SystemHandler) {
self.master = master
}

func authorizationController(
controller: ASAuthorizationController,
didCompleteWithAuthorization authorization: ASAuthorization
) {
switch authorization.credential {
case let credential as ASAuthorizationAppleIDCredential:
let identityToken = credential.identityToken.flatMap {
String(data: $0, encoding: .utf8)
}

let authorizationCode = credential.authorizationCode.flatMap {
String(data: $0, encoding: .utf8)
}

let parameters = [
OauthInfoKeys.identityToken: identityToken,
OauthInfoKeys.authorizationCode: authorizationCode,
OauthInfoKeys.user: credential.user,
OauthInfoKeys.email: credential.email,
OauthInfoKeys.givenName: credential.fullName?.givenName,
OauthInfoKeys.familyName: credential.fullName?.familyName,
]
.compactMapContent()

if !parameters.isEmpty {
master?.oauthCompletionHandler?(.success(parameters))
} else {
master?.oauthCompletionHandler?(.failure(.unknown))
}
default:
assertionFailure("\(authorization.credential)")
}
}

func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) {
switch error {
case ASAuthorizationError.canceled:
master?.oauthCompletionHandler?(.failure(.userCancelled))
default:
master?.oauthCompletionHandler?(.failure(.unknown))
}
}
}
}

extension SystemHandler {

public enum OauthInfoKeys {

public static let identityToken = Bus.OauthInfoKey(rawValue: "com.nuomi1.bus.systemHandler.identityToken")

public static let authorizationCode = Bus.OauthInfoKey(rawValue: "com.nuomi1.bus.systemHandler.authorizationCode")

public static let user = Bus.OauthInfoKey(rawValue: "com.nuomi1.bus.systemHandler.user")

public static let email = Bus.OauthInfoKey(rawValue: "com.nuomi1.bus.systemHandler.email")

public static let givenName = Bus.OauthInfoKey(rawValue: "com.nuomi1.bus.systemHandler.givenName")

public static let familyName = Bus.OauthInfoKey(rawValue: "com.nuomi1.bus.systemHandler.familyName")
}
}

创建 Helper 处理系统的回调:

  1. SystemHandler 强持有 Helper,所以 Helpermaster 声明为 weak 避免循环引用。
  2. 处理登录回调时,成功则 authorizationController(controller:didCompleteWithAuthorization:) 被调用,返回 ASAuthorizationAppleIDCredentialidentityToken / authorizationCodeData? 类型,需要转为 String?,再获取 user / email / givenName / familyName 等信息,调用 master?.oauthCompletionHandler?(.success(parameters)) 完成登录回调。失败则 authorizationController(controller:didCompleteWithError:) 被调用,调用 master?.oauthCompletionHandler?(.failure(Error)) 完成登录回调。

总结

本文通过封装系统模块做出 SystemHandler,实现系统的登录和分享功能。需要注意在 iPad 环境下需要设置 popoverPresentationController 相关属性,否则无法进行分享。同时一定要尽可能获取 ASAuthorizationAppleIDCredential 的全部数据,例如用户提供的邮箱和姓名,后续与 Apple 服务器校验时,Apple 不会返回用户数据。