NBus 之 WechatSDKHandler

微信 SDK 的接入是最简单的,本 Handler 基于微信 SDK 1.8.7.1 实现。


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
34
35
36
37
38
39
40
41
42
43
44
45
46
public class WechatSDKHandler {

public let endpoints: [Endpoint] = [
Endpoints.Wechat.friend,
Endpoints.Wechat.timeline,
Endpoints.Wechat.favorite,
]

public let platform: Platform = Platforms.wechat

public var isInstalled: Bool {
WXApi.isWXAppInstalled()
}

private var shareCompletionHandler: Bus.ShareCompletionHandler?
private var oauthCompletionHandler: Bus.OauthCompletionHandler?

public let appID: String
public let universalLink: URL

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

private var helper: Helper!

public init(appID: String, universalLink: URL) {
self.appID = appID
self.universalLink = universalLink

helper = Helper(master: self)

#if DEBUG
WXApi.startLog(by: .detail) { [weak self] message in
self?.log(message)
}
#endif

WXApi.registerApp(
appID,
universalLink: universalLink.absoluteString
)
}
}

调用 WXApi.registerApp(_:universalLink:) 注册微信 SDK,调用 WXApi.startLog(by:logBlock:) 记录日志。Helper 处理微信的回调,相关内容下文会进行说明。

LogHandlerProxyType

1
extension WechatSDKHandler: LogHandlerProxyType {}

声明 WechatSDKHandler 遵循 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
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
extension WechatSDKHandler: ShareHandlerType {

public func share(
message: MessageType,
to endpoint: Endpoint,
options: [Bus.ShareOptionKey: Any] = [:],
completionHandler: @escaping Bus.ShareCompletionHandler
) {
guard isInstalled else {
completionHandler(.failure(.missingApplication))
return
}

guard canShare(message: message.identifier, to: endpoint) else {
completionHandler(.failure(.unsupportedMessage))
return
}

shareCompletionHandler = completionHandler

let request = SendMessageToWXReq()
request.scene = Int32(scene(endpoint).rawValue)

let mediaMessage = WXMediaMessage()

if let message = message as? MediaMessageType {
mediaMessage.title = message.title ?? ""
mediaMessage.description = message.description ?? ""
mediaMessage.thumbData = message.thumbnail
}

switch message {
case let message as TextMessage:
request.text = message.text
request.bText = true

case let message as ImageMessage:
let imageObject = WXImageObject()
imageObject.imageData = message.data

mediaMessage.mediaObject = imageObject

case let message as AudioMessage:
let audioObject = WXMusicObject()
audioObject.musicUrl = message.link.absoluteString
audioObject.musicDataUrl = message.dataLink?.absoluteString ?? ""

mediaMessage.mediaObject = audioObject

case let message as VideoMessage:
let videoObject = WXVideoObject()
videoObject.videoUrl = message.link.absoluteString

mediaMessage.mediaObject = videoObject

case let message as WebPageMessage:
let webPageObject = WXWebpageObject()
webPageObject.webpageUrl = message.link.absoluteString

mediaMessage.mediaObject = webPageObject

case let message as FileMessage:
let fileObject = WXFileObject()
fileObject.fileData = message.data
fileObject.fileExtension = message.fileExtension

mediaMessage.mediaObject = fileObject

case let message as MiniProgramMessage:
let miniProgramObject = WXMiniProgramObject()
miniProgramObject.webpageUrl = message.link.absoluteString
miniProgramObject.userName = message.miniProgramID
miniProgramObject.path = message.path
miniProgramObject.miniProgramType = miniProgramType(message.miniProgramType)
miniProgramObject.hdImageData = message.thumbnail

mediaMessage.mediaObject = miniProgramObject

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

request.message = mediaMessage

WXApi.send(request) { result in
if !result {
completionHandler(.failure(.invalidMessage))
}
}
}

private func canShare(message: Message, to endpoint: Endpoint) -> Bool {
switch endpoint {
case Endpoints.Wechat.friend:
return true
case Endpoints.Wechat.timeline:
return ![Messages.file, Messages.miniProgram].contains(message)
case Endpoints.Wechat.favorite:
return ![Messages.miniProgram].contains(message)
default:
assertionFailure()
return false
}
}

private func scene(_ endpoint: Endpoint) -> WXScene {
switch endpoint {
case Endpoints.Wechat.friend:
return WXSceneSession
case Endpoints.Wechat.timeline:
return WXSceneTimeline
case Endpoints.Wechat.favorite:
return WXSceneFavorite
default:
assertionFailure()
return WXSceneSession
}
}

private func miniProgramType(_ miniProgramType: MiniProgramMessage.MiniProgramType) -> WXMiniProgramType {
switch miniProgramType {
case .release:
return .release
case .test:
return .test
case .preview:
return .preview
}
}
}

在分享流程中:

  1. 调用 isInstalled 判断是否安装微信,没有则提前退出。
  2. 调用 canShare(message:to:) 判断 Endpoint 是否支持此类型 Message。例如 Endpoints.Wechat.timeline 朋友圈不支持 Messages.file 文件和 Messages.miniProgram 小程序。
  3. 创建 SendMessageToWXReq 请求,调用 scene(_:) 根据 Endpoint 创建 WXScene
  4. 创建 WXMediaMessage 多媒体消息,如果 message 遵循 MediaMessageType 协议,设置 title / description / thumbData
  5. 判断 message 的具体类型,设置 requestmediaMessage 的相关属性。例如 TextMessage 需要设置 request.bText = trueMiniProgramMessage 需要调用 miniProgramType(_:) 根据 MiniProgramMessage.MiniProgramType 创建 WXMiniProgramType
  6. 调用 WXApi.send(_: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
25
26
27
28
29
30
31
32
33
34
35
36
37
extension WechatSDKHandler: OauthHandlerType {

public func oauth(
options: [Bus.OauthOptionKey: Any] = [:],
completionHandler: @escaping Bus.OauthCompletionHandler
) {
guard isInstalled else {
completionHandler(.failure(.missingApplication))
return
}

oauthCompletionHandler = completionHandler

let request = SendAuthReq()
request.scope = "snsapi_userinfo"

let viewController = (options[OauthOptionKeys.viewController] as? UIViewController) ?? UIViewController()

WXApi.sendAuthReq(
request,
viewController: viewController,
delegate: helper
) { result in
if !result {
completionHandler(.failure(.unknown))
}
}
}
}

extension WechatSDKHandler {

public enum OauthOptionKeys {

public static let viewController = Bus.OauthOptionKey(rawValue: "com.nuomi1.bus.wechatSDKHandler.viewController")
}
}

在登录流程中:

  1. 调用 isInstalled 判断是否安装微信,没有则提前退出。
  2. 创建 SendAuthReq 请求。scope 在微信 SDK 中没有对应的常量,使用 snsapi_userinfo 硬编码。
  3. options 中获取 viewController,没有则创建一个空白的。
  4. 调用 WXApi.sendAuthReq(_:viewController:delegate:completion:) 拉起微信登录。

OpenURLHandlerType

1
2
3
4
5
6
extension WechatSDKHandler: OpenURLHandlerType {

public func openURL(_ url: URL) {
WXApi.handleOpen(url, delegate: helper)
}
}

调用 WXApi.handleOpen(_:delegate:) 处理 URL Scheme 回调。

OpenUserActivityHandlerType

1
2
3
4
5
6
extension WechatSDKHandler: OpenUserActivityHandlerType {

public func openUserActivity(_ userActivity: NSUserActivity) {
WXApi.handleOpenUniversalLink(userActivity, delegate: helper)
}
}

调用 WXApi.handleOpenUniversalLink(_:delegate:) 处理 NSUserActivity 回调。

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

fileprivate class Helper: NSObject, WXApiDelegate {

weak var master: WechatSDKHandler?

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

func onReq(_ req: BaseReq) {
assertionFailure("\(req)")
}

func onResp(_ resp: BaseResp) {
switch resp {
case let response as SendMessageToWXResp:
switch response.errCode {
case WXSuccess.rawValue:
master?.shareCompletionHandler?(.success(()))
default:
master?.shareCompletionHandler?(.failure(.unknown))
}
case let response as SendAuthResp:
switch (response.errCode, response.code) {
case let (WXSuccess.rawValue, code):
let parameters = [
OauthInfoKeys.code: code,
]
.compactMapContent()

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

extension WechatSDKHandler {

public enum OauthInfoKeys {

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

创建 Helper 处理微信的回调:

  1. WechatSDKHandler 强持有 Helper,所以 Helpermaster 声明为 weak 避免循环引用。
  2. 处理分享回调时,返回 SendMessageToWXResp,调用 response.errCode 判断是否为 WXSuccess.rawValue,调用 master?.shareCompletionHandler?(.success(())) 完成分享回调。
  3. 处理登录回调时,返回 SendAuthResp,调用 response.errCoderesponse.code 判断成功且 code 不为空时,调用 master?.oauthCompletionHandler?(.success(parameters)) 完成登录回调。

总结

本文通过封装微信 SDK 做出 WechatSDKHandler,实现微信的登录和分享功能。同时使用了独立的内部类 Helper 来处理微信的回调,保持 WechatSDKHandler 的整洁。