一直使用 MonkeyKing 作为分享模块的底层实现,但 微信 SDK 1.8.6.1 和 QQ SDK 3.3.6 开始改用 Universal Links 进行处理且校验请求合法性,而 MonkeyKing 在较晚的时候才提供支持。
当时急着上线,微信和 QQ 直接改用官方 SDK,微博还是保留 MonkeyKing 的处理。从那之后我就在想,官方闭源也好,三方开源也罢,能快速接入上线才是最重要的。如果分享组件能提供一键切换的能力,那就方便了。除此之外,也受 nixzhu 的鼓励,萌生了写 NBus 的想法。
前言
一开始用的名字是「Bus」,但已经被其他人在 CocoaPods 注册了,因此库名改成了「NBus」,除此之外还是用「Bus」。
Bus 采用了注册 Handler 的机制,由 Handler 实现细节,Bus 只作为派发任务的管理者。这样做的好处是新增或者修改默认实现变得十分简单。
目前有 share
/ oauth
/ openURL
/ openUserActivity
四个方法,用于实现分享、登录以及回调。
Handler
1 2 3 4
| public protocol HandlerType {
var isInstalled: Bool { get } }
|
Handler 由 HandlerType
协议所约束, 声明了 isInstalled
属性,用于判断 Handler 是否安装对应 App。
在此之上根据具体功能分为 ShareHandlerType
/ OauthHandlerType
/ OpenURLHandlerType
/ OpenUserActivityHandlerType
四个子协议,同时还有一个 LogHandlerProxyType
用于记录日志。
ShareHandlerType
1 2 3 4 5 6 7 8 9 10 11 12 13
| public protocol ShareHandlerType: HandlerType {
var endpoints: [Endpoint] { get }
func share( message: MessageType, to endpoint: Endpoint, options: [Bus.ShareOptionKey: Any], completionHandler: @escaping Bus.ShareCompletionHandler )
func canShare(to endpoint: Endpoint) -> Bool }
|
1 2 3 4 5 6
| extension ShareHandlerType {
public func canShare(to endpoint: Endpoint) -> Bool { endpoints.contains(endpoint) } }
|
Bus 通过判断 Handler 预先声明的 endpoints
是否包含本次分享的 endpoint
进行派发任务。
OauthHandlerType
1 2 3 4 5 6 7 8 9 10 11
| public protocol OauthHandlerType: HandlerType {
var platform: Platform { get }
func oauth( options: [Bus.OauthOptionKey: Any], completionHandler: @escaping Bus.OauthCompletionHandler )
func canOauth(with platform: Platform) -> Bool }
|
1 2 3 4 5 6
| extension OauthHandlerType {
public func canOauth(with platform: Platform) -> Bool { self.platform == platform } }
|
Bus 通过判断 Handler 预先声明的 platform
是否等于本次登录的 platform
进行派发任务。
OpenURLHandlerType
1 2 3 4 5 6 7 8
| public protocol OpenURLHandlerType: HandlerType {
var appID: String { get }
func openURL(_ url: URL)
func canOpenURL(_ url: URL) -> Bool }
|
1 2 3 4 5 6
| extension OpenURLHandlerType {
public func canOpenURL(_ url: URL) -> Bool { appID == url.scheme } }
|
Bus 通过判断 Handler 预先声明的 appID
是否等于本次打开 URL 的 scheme
进行派发任务。
OpenUserActivityHandlerType
1 2 3 4 5 6 7 8
| public protocol OpenUserActivityHandlerType: HandlerType {
var universalLink: URL { get }
func openUserActivity(_ userActivity: NSUserActivity)
func canOpenUserActivity(_ userActivity: NSUserActivity) -> Bool }
|
1 2 3 4 5 6 7 8
| extension OpenUserActivityHandlerType {
public func canOpenUserActivity(_ userActivity: NSUserActivity) -> Bool { let lhs = userActivity.webpageURL?.absoluteString ?? "" let rhs = universalLink.absoluteString return lhs.hasPrefix(rhs) } }
|
Bus 通过判断 Handler 预先声明的 universalLink
是否是本次打开 NSUserActivity 的 webpageURL
的前缀进行派发任务。
LogHandlerProxyType
1 2 3 4 5 6 7 8 9 10 11
| public protocol LogHandlerProxyType: HandlerType {
var logHandler: Bus.LogHandler { get }
func log( _ message: String, file: String, function: String, line: UInt ) }
|
1 2 3 4 5 6 7 8 9 10 11
| extension LogHandlerProxyType {
public func log( _ message: String, file: String = #file, function: String = #function, line: UInt = #line ) { logHandler(message, file, function, line) } }
|
LogHandlerProxyType
协议声明了 logHandler
属性以及 log
函数用于记录日志。这个协议只是用来约束命名,可以不用实现。
自定义日志记录行为,可以用 delegate 或者 block,这里为了方便选择了 block。只需要在初始化 Handler 时修改 logHandler
,即可把日志记录转到自有的日志系统。
Endpoint
1 2 3 4 5 6 7 8 9 10
| public struct Endpoint: RawRepresentable, Hashable {
public typealias RawValue = String
public let rawValue: Self.RawValue
public init(rawValue: Self.RawValue) { self.rawValue = rawValue } }
|
Endpoint
是一个只有 rawValue
的结构体类型,同时实现了 Hashable
协议。它表示的是分享行为的目标。
Endpoints
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| public enum Endpoints {
public enum QQ {
public static let friend = Endpoint(rawValue: "com.nuomi1.bus.endpoint.qq.friend")
public static let timeline = Endpoint(rawValue: "com.nuomi1.bus.endpoint.qq.timeline") }
public enum Wechat {
public static let friend = Endpoint(rawValue: "com.nuomi1.bus.endpoint.wechat.friend")
public static let timeline = Endpoint(rawValue: "com.nuomi1.bus.endpoint.wechat.timeline")
public static let favorite = Endpoint(rawValue: "com.nuomi1.bus.endpoint.wechat.favorite") } }
|
通过枚举类型的嵌套,把各个 endpoint 放到 Endpoints
集中访问。如果把这些常量直接放到 Endpoint
,不容易做平台的区分,这里通过中间的枚举类型分开了不同平台。同时枚举类型没有显式的初始化方法,在代码提示中可以避免不必要的展示。写法参考了 Combine 的 Publishers。
1 2 3 4 5 6 7 8 9 10
| public struct Platform: RawRepresentable, Hashable {
public typealias RawValue = String
public let rawValue: Self.RawValue
public init(rawValue: Self.RawValue) { self.rawValue = rawValue } }
|
Platform
是一个只有 rawValue
的结构体类型,同时实现了 Hashable
协议。它表示的是登录行为的平台。
1 2 3 4 5 6
| public enum Platforms {
public static let qq = Platform(rawValue: "com.nuomi1.bus.platform.qq")
public static let wechat = Platform(rawValue: "com.nuomi1.bus.platform.wechat") }
|
与 Endpoints
类似,把各个 platform 放到 Platforms
集中访问。平台下面没有子平台,因此不需要嵌套来区分。
Message
1 2 3 4 5 6 7 8 9 10
| public struct Message: RawRepresentable, Hashable {
public typealias RawValue = String
public let rawValue: Self.RawValue
public init(rawValue: Self.RawValue) { self.rawValue = rawValue } }
|
Message
是一个只有 rawValue
的结构体类型,同时实现了 Hashable
协议。它表示的是分享行为中所分享事物的类型。
Messages
1 2 3 4 5 6
| public enum Messages {
public static let text = Message(rawValue: "com.nuomi1.bus.message.text")
public static let image = Message(rawValue: "com.nuomi1.bus.message.image") }
|
与 Endpoints
类似,把各个 message 放到 Messages
集中访问。
MessageType
1 2 3 4
| public protocol MessageType {
var identifier: Message { get } }
|
1 2 3 4 5 6 7 8
| public protocol MediaMessageType: MessageType {
var title: String? { get }
var description: String? { get }
var thumbnail: Data? { get } }
|
MessageType
协议声明了 identifier
属性用于区别分享事物的类型。MediaMessageType
是 MessageType
的子协议,额外声明了 title
/ description
/ thumbnail
等共有属性。
TextMessage
1 2 3 4 5 6
| public struct TextMessage: MessageType {
public let identifier = Messages.text
public let text: String }
|
TextMessage
是一个实现了 MessageType
协议的结构体类型。它表示的是分享事物为纯文本。
ImageMessage
1 2 3 4 5 6 7 8 9 10 11 12
| public struct ImageMessage: MediaMessageType {
public let identifier = Messages.image
public let data: Data
public let title: String?
public let description: String?
public let thumbnail: Data? }
|
ImageMessage
是一个实现了 MediaMessageType
协议的结构体类型。它表示的是分享事物为图片。
Messages-func
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 Messages {
public static func text( text: String ) -> TextMessage { TextMessage( text: text ) }
public static func image( data: Data, title: String? = nil, description: String? = nil, thumbnail: Data? = nil ) -> ImageMessage { ImageMessage( data: data, title: title, description: description, thumbnail: thumbnail ) } }
|
Messages
除了各个 message,还定义了静态函数来初始化各个分享事物,并且把非必须的属性自动赋值 nil 方便初始化。
Bus
share
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
| extension Bus {
public struct ShareOptionKey: RawRepresentable, Hashable {
public typealias RawValue = String
public let rawValue: Self.RawValue
public init(rawValue: Self.RawValue) { self.rawValue = rawValue } }
public typealias ShareCompletionHandler = (Result<Void, Bus.Error>) -> Void
public func share( message: MessageType, to endpoint: Endpoint, options: [Bus.ShareOptionKey: Any] = [:], completionHandler: @escaping ShareCompletionHandler ) { let handlers = self.handlers.compactMap { $0 as? ShareHandlerType }
guard let handler = handlers.first(where: { $0.canShare(to: endpoint) }) else { assertionFailure() completionHandler(.failure(.missingHandler)) return }
handler.share( message: message, to: endpoint, options: options, completionHandler: completionHandler ) } }
|
ShareOptionKey
是实现了 Hashable
协议的结构体类型,作用是传递数据时做标识符。
ShareCompletionHandler
是分享回调的函数签名,分享不需要处理返回值,因此 Success
被设计为 Void
。
在 share
中,先找出实现了 ShareHandlerType
协议的 handlers,再向各个 handler 询问是否可以处理本次分享行为。找不到任何 handler 就执行 assertionFailure
提示调用方应注册所需的 Handler,找到就给对应的 Handler 派发任务。
oauth
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
| extension Bus {
public struct OauthOptionKey: RawRepresentable, Hashable {
public typealias RawValue = String
public let rawValue: Self.RawValue
public init(rawValue: Self.RawValue) { self.rawValue = rawValue } }
public struct OauthInfoKey: RawRepresentable, Hashable {
public typealias RawValue = String
public let rawValue: Self.RawValue
public init(rawValue: Self.RawValue) { self.rawValue = rawValue } }
public typealias OauthCompletionHandler = (Result<[OauthInfoKey: String], Bus.Error>) -> Void
public func oauth( with platform: Platform, options: [Bus.OauthOptionKey: Any] = [:], completionHandler: @escaping OauthCompletionHandler ) { let handlers = self.handlers.compactMap { $0 as? OauthHandlerType }
guard let handler = handlers.first(where: { $0.canOauth(with: platform) }) else { assertionFailure() completionHandler(.failure(.missingHandler)) return }
handler.oauth( options: options, completionHandler: completionHandler ) } }
|
在 oauth
中,处理同 share
方法,但是登录需要返回登录凭据和用户信息,因此 OauthCompletionHandler
的 Success
被设计为 [OauthInfoKey: String]
。
openURL
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| extension Bus {
public func openURL(_ url: URL) -> Bool { let handlers = self.handlers.compactMap { $0 as? OpenURLHandlerType }
guard let handler = handlers.first(where: { $0.canOpenURL(url) }) else { return false }
handler.openURL(url) return true } }
|
在 openURL
中,处理同 share
方法,但是不需要执行 assertionFailure
,因为这个方法一般在 AppDelegate
的 application(_:open:options:)
方法中使用 ||
调用,无法处理 url
是正常情况。
openUserActivity
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| extension Bus {
public func openUserActivity(_ userActivity: NSUserActivity) -> Bool { let handlers = self.handlers.compactMap { $0 as? OpenUserActivityHandlerType }
guard let handler = handlers.first(where: { $0.canOpenUserActivity(userActivity) }) else { return false }
handler.openUserActivity(userActivity) return true } }
|
在 openUserActivity
中,处理同 openURL
方法,在 AppDelegate
的 application(_:continue:restorationHandler:)
方法中使用 ||
调用。
总结
Bus 中大量使用带 rawValue
的结构体类型用于标识符。虽然本质上都是字符串,但实际代表的事物却不一样,应加以区分。另一方面,如果使用 String
作为别名,那么实现 description
来描述具体的常量将是个灾难,不得不为每一个别名分别实现 xxxDescription
,现在只需要给各个结构体单独实现 CustomStringConvertible
协议即可。
Bus 中还大量使用枚举类型作为常量集合的入口,这样既可以统一管理,也方便调用方增加自己的常量。如果直接使用枚举类型,除了修改源码,没法增加常量。
限于篇幅,没有逐一介绍 Bus 的源码,感兴趣的可以到 NBus 查看,顺便给个 star 或者 fork。
参考