跨 Apple 平台的响应式通信库。
为了更方便地在响应式用户界面中使用,尤其是在 SwiftUI 和 Combine 中,我创建了一个库,它抽象并映射了常见的连接 API。特别是在我的 Heartwitch 应用中,我映射了 WatchConnectivity 和 Network 的功能,以跟踪用户连接到互联网的能力,以及他们的 iPhone 通过 WatchConnectivity 连接到他们的 Apple Watch 的能力。
以下是此库目前已实现的功能
Swift Package Manager 是 Apple 的去中心化依赖管理器,用于将库集成到你的 Swift 项目中。现在它已与 Xcode 13 完全集成。
要使用 SPM 将 SundialKit 集成到你的项目中,请在你的 Package.swift 文件中指定它
let package = Package(
...
dependencies: [
.package(url: "https://github.com/brightdigit/SundialKit.git", from: "0.2.0")
],
targets: [
.target(
name: "YourTarget",
dependencies: ["SundialKit", ...]),
...
]
)
过去,Reachability 或 AFNetworking 被用于判断设备的网络连接。SundialKit 使用 Network 框架来监听连接的变化,提供所有可用的信息。
SundialKit 提供了一个 NetworkObserver,允许你监听与网络相关的各种发布者 (publisher)。如果你特别使用 SwiftUI,这将特别有用。使用 SwiftUI,你可以创建一个 ObservableObject,其中包含一个 NetworkObserver
import SwiftUI
import SundialKit
class NetworkConnectivityObject : ObservableObject {
// our NetworkObserver
let connectivityObserver = NetworkObserver()
// our published property for pathStatus initially set to `.unknown`
@Published var pathStatus : PathStatus = .unknown
init () {
// set the pathStatus changes to our published property
connectivityObserver
.pathStatusPublisher
.receive(on: DispatchQueue.main)
.assign(to: &self.$pathStatus)
}
// need to start listening
func start () {
self.connectivityObserver.start(queue: .global())
}
}
有 3 个重要的部分
connectivityObserver 的 NetworkObserverinit 中,我们使用 Combine 监听发布者 (publisher) 并将每个新的 pathStatus 存储到我们的 @Published 属性。start 方法,用于开始监听 NetworkObserver。因此,对于我们的 SwiftUI View,我们需要在 onAppear 时 start 监听,并可以在 View 中使用 pathStatus 属性
struct NetworkObserverView: View {
@StateObject var connectivityObject = NetworkConnectivityObject()
var body: some View {
// Use the `message` property to display text of the `pathStatus`
Text(self.connectivityObject.pathStatus.message).onAppear{
// start the NetworkObserver
self.connectivityObject.start()
}
}
}
除了 pathStatus 之外,你还可以访问
isExpensiveisConstrained除了使用 NWPathMonitor,你还可以通过实现 NetworkPing 来设置定期 ping。这是一个调用 ipify API 以验证是否存在 IP 地址的示例
struct IpifyPing : NetworkPing {
typealias StatusType = String?
let session: URLSession
let timeInterval: TimeInterval
public func shouldPing(onStatus status: PathStatus) -> Bool {
switch status {
case .unknown, .unsatisfied:
return false
case .requiresConnection, .satisfied:
return true
}
}
static let url : URL = .init(string: "https://api.ipify.org")!
func onPing(_ closure: @escaping (String?) -> Void) {
session.dataTask(with: IpifyPing.url) { data, _, _ in
closure(data.flatMap{String(data: $0, encoding: .utf8)})
}.resume()
}
}
接下来,在我们的 ObservableObject 中,我们可以创建一个 NetworkObserver 来使用它
@Published var nwObject = NetworkObserver(ping:
// use the shared `URLSession` and check every 10.0 seconds
IpifyPing(session: .shared, timeInterval: 10.0)
)
除了网络连接,SundialKit 还提供了一个更简单的响应式接口到 WatchConnectivity。这包括
isReachable、isInstalled 等。WatchConnectivity 友好的字典。我们先来谈谈 WatchConnectivity 状态是如何工作的。
使用 WatchConnectivity,有各种属性可以告诉你设备之间的连接状态。这是一个类似于使用 isReachable 的 pathStatus 的示例
import SwiftUI
import SundialKit
class WatchConnectivityObject : ObservableObject {
// our ConnectivityObserver
let connectivityObserver = ConnectivityObserver()
// our published property for isReachable initially set to false
@Published var isReachable : Bool = false
init () {
// set the isReachable changes to our published property
connectivityObserver
.isReachablePublisher
.receive(on: DispatchQueue.main)
.assign(to: &self.$isReachable)
}
func activate () {
// activate the WatchConnectivity session
try! self.connectivityObserver.activate()
}
}
同样,有 3 个重要的部分
connectivityObserver 的 ConnectivityObserverinit 中,我们使用 Combine 监听发布者 (publisher) 并将每个新的 isReachable 存储到我们的 @Published 属性。activate 方法,用于激活 WatchConnectivity 的会话。因此,对于我们的 SwiftUI View,我们需要在 onAppear 时 activate 会话,并可以在 View 中使用 isReachable 属性
struct WatchConnectivityView: View {
@StateObject var connectivityObject = WatchConnectivityObject()
var body: some View {
Text(
connectivityObject.isReachable ?
"Reachable" : "Not Reachable"
)
.onAppear{
self.connectivityObject.activate()
}
}
}
除了 isReachable 之外,你还可以访问
activationStateisReachableisPairedAppInstalledisPaired此外,还有一组发布者 (publisher),用于在 iPhone 和配对的 Apple Watch 之间发送、接收和回复消息。
要通过我们的 ConnectivityObserver 发送和接收消息,我们可以访问两个属性
ConnectivityObserver/messageReceivedPublisher - 用于监听消息ConnectivityObserver/sendingMessageSubject - 用于发送消息SundialKit 使用 [String:Any] 字典来发送和接收消息,该字典使用类型别名 ConnectivityMessage。让我们扩展之前的 WatchConnectivityObject 并使用这些属性
class WatchConnectivityObject : ObservableObject {
// our ConnectivityObserver
let connectivityObserver = ConnectivityObserver()
// our published property for isReachable initially set to false
@Published var isReachable : Bool = false
// our published property for the last message received
@Published var lastReceivedMessage : String = ""
init () {
// set the isReachable changes to our published property
connectivityObserver
.isReachablePublisher
.receive(on: DispatchQueue.main)
.assign(to: &self.$isReachable)
// set the lastReceivedMessage based on the dictionary's _message_ key
connectivityObserver
.messageReceivedPublisher
.compactMap({ received in
received.message["message"] as? String
})
.receive(on: DispatchQueue.main)
.assign(to: &self.$lastReceivedMessage)
}
func activate () {
// activate the WatchConnectivity session
try! self.connectivityObserver.activate()
}
func sendMessage(_ message: String) {
// create a dictionary with the message in the message key
self.connectivityObserver.sendingMessageSubject.send(["message" : message])
}
}
我们现在可以使用更新后的 WatchConnectivityObject 创建一个简单的 SwiftUI View
struct WatchMessageDemoView: View {
@StateObject var connectivityObject = WatchMessageObject()
@State var message : String = ""
var body: some View {
VStack{
Text(connectivityObject.isReachable ? "Reachable" : "Not Reachable").onAppear{
self.connectivityObject.activate()
}
TextField("Message", text: self.$message)
Button("Send") {
self.connectivityObject.sendMessage(self.message)
}
Text("Last received message:")
Text(self.connectivityObject.lastReceivedMessage)
}
}
}
我们甚至可以使用 MessageDecoder 抽象 ConnectivityMessage。为此,我们需要创建一个实现 Messagable 的特殊类型
struct Message : Messagable {
internal init(text: String) {
self.text = text
}
static let key: String = "_message"
enum Parameters : String {
case text
}
init?(from parameters: [String : Any]?) {
guard let text = parameters?[Parameters.text.rawValue] as? String else {
return nil
}
self.text = text
}
func parameters() -> [String : Any] {
return [
Parameters.text.rawValue : self.text
]
}
let text : String
}
实现 Messagable 有三个要求
Messagable/init(from:) - 尝试基于字典创建对象,如果无效则返回 nilMessagable/parameters() - 返回一个包含重建对象所需的所有参数的字典Messagable/key - 返回一个字符串,用于标识类型并且对于 MessageDecoder 是唯一的现在我们已经实现了 Messagable,我们可以在我们的 WatchConnectivityObject 中使用它
class WatchConnectivityObject : ObservableObject {
// our ConnectivityObserver
let connectivityObserver = ConnectivityObserver()
// create a `MessageDecoder` which can decode our new `Message` type
let messageDecoder = MessageDecoder(messagableTypes: [Message.self])
// our published property for isReachable initially set to false
@Published var isReachable : Bool = false
// our published property for the last message received
@Published var lastReceivedMessage : String = ""
init () {
// set the isReachable changes to our published property
connectivityObserver
.isReachablePublisher
.receive(on: DispatchQueue.main)
.assign(to: &self.$isReachable)
connectivityObserver
// get the ``ConnectivityReceiveResult/message`` part of the ``ConnectivityReceiveResult``
.map(\.message)
// use our `messageDecoder` to call ``MessageDecoder/decode(_:)``
.compactMap(self.messageDecoder.decode)
// check it's our `Message`
.compactMap{$0 as? Message}
// get the `text` property
.map(\.text)
.receive(on: DispatchQueue.main)
// set it to our published property
.assign(to: &self.$lastReceivedMessage)
}
func activate () {
// activate the WatchConnectivity session
try! self.connectivityObserver.activate()
}
func sendMessage(_ message: String) {
// create a dictionary using ``Messagable/message()``
self.connectivityObserver.sendingMessageSubject.send(Message(text: message).message())
}
}
此代码根据 MIT 许可证分发。有关更多信息,请参阅 LICENSE 文件。