跨 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
的 NetworkObserver
init
中,我们使用 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
之外,你还可以访问
isExpensive
isConstrained
除了使用 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
的 ConnectivityObserver
init
中,我们使用 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
之外,你还可以访问
activationState
isReachable
isPairedAppInstalled
isPaired
此外,还有一组发布者 (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 文件。