diff --git a/Package.swift b/Package.swift index 0ae8f1e..0f8c03e 100644 --- a/Package.swift +++ b/Package.swift @@ -5,7 +5,7 @@ let package = Package( name: "WatchSync", platforms: [ .iOS(.v13), - .watchOS(.v4), + .watchOS(.v6), ], products: [ .library(name: "WatchSync", targets: ["WatchSync"]), diff --git a/README.md b/README.md index 852f800..d798476 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # ⌚️WatchSync -WatchConnectivity wrapper with typed messages, better error handling, and simplified subscription APIs. +WatchConnectivity wrapper with typed messages, better error handling, and simplified subscription APIs. It contains learnings from building [Pinnacle Climb Log](https://pinnacleclimb.com/). ## Example @@ -12,8 +12,8 @@ Create a new message type that conforms to the `SyncableMessage` protocol. Uses import WatchSync struct MyMessage: SyncableMessage { - var myString: String? - var myDate: Date? + let myString: String? + let myDate: Date? } ``` @@ -33,21 +33,22 @@ WatchSync.shared.sendMessage(["test": "message"]) { result in } ``` +🛫 `WatchSync` will send the message using realtime messaging if the other device is `reachable`, otherwise it will fall back on `transferUserInfo` to ensure it is delivered. If it is sent using realtime messaging you will receive a `delivered` event. + +🗒️ The message is compressed to reduce the likelihood of running into a `WCErrorCodePayloadTooLarge` error. + ### Subscribe to new messages Listen for changes from the paired device (iOS or watchOS) ```swift -class ViewController: UIViewController { - var subscriptionToken: SubscriptionToken? - - override func viewDidLoad() { - super.viewDidLoad() - - subscriptionToken = WatchSync.shared.subscribeToMessages(ofType: MyMessage.self) { myMessage in - print(String(describing: myMessage.myString), String(describing: myMessage.myDate)) - } - } +struct MyView: View { + var body: some View { + Text("Hello") + .onReceive(WatchSync.shared.publisher(for: MyMessage.self)) { message in + print(message.myString, message.myDate) + } + } } ``` @@ -61,28 +62,16 @@ WatchSync.shared.update(applicationContext: ["test": "context"]) { result in ### Subscribe to application context updates ```swift -appDelegateObserver = - -class ViewController: UIViewController { - var subscriptionToken: SubscriptionToken? - - override func viewDidLoad() { - super.viewDidLoad() - - subscriptionToken = WatchSync.shared.subscribeToApplicationContext { applicationContext in - print(applicationContext) - } - } +struct MyView: View { + var body: some View { + Text("Hello") + .onReceive(WatchSync.shared.applicationContextPublisher) { applicationContext in + print(applicationContext) + } + } } ``` -## How it works - -* If the paired device is reachable, `WatchSync` will try to send using an interactive message with `session.sendMessage()`. -* If the paired device is unreachable, it will fall back to using `sendUserInfo()` instead. -* All messages conforming to `SyncableMessage` will be JSON serialized to reduce the size of the payload. This is to reduce the likelyhood of running into a `WCErrorCodePayloadTooLarge` error. -* For interactive messages it uses the `replyHandler` for delivery acknowledgments. - ## Installation & Setup In your `AppDelegate` (iOS) and `ExtensionDelegate` (watchOS) under `applicationDidFinishLaunching` you will need to activate the Watch Connectivity session. diff --git a/Sources/WatchSync/WatchSync.swift b/Sources/WatchSync/WatchSync.swift index 690b03d..410952b 100644 --- a/Sources/WatchSync/WatchSync.swift +++ b/Sources/WatchSync/WatchSync.swift @@ -6,6 +6,7 @@ // Copyright © 2018 Ten Minute Wait. All rights reserved. // +import Combine import Foundation import Gzip import WatchConnectivity @@ -26,6 +27,8 @@ open class WatchSync: NSObject { private var activationCallback: ((Error?) -> Void)? + private let messagePublisher = PassthroughSubject() + /// Loop through these when processing an incoming message /// /// I would prefer to use a Set here but not sure not sure how @@ -62,10 +65,53 @@ open class WatchSync: NSObject { /// Observe messages of Type (Recommended) /// /// - Parameters: + /// - for: Message type that conforms to the `WatchSyncable` protocol + public func publisher(for messageType: T.Type) -> AnyPublisher { + if !registeredMessageTypes.contains(where: { watchSyncableType -> Bool in + if watchSyncableType == T.self { + return true + } + return false + }) { + registeredMessageTypes.append(messageType) + } + return messagePublisher + .compactMap { $0 as? T } + .eraseToAnyPublisher() + } + + private let rawMessageInternalPublisher = PassthroughSubject<[String: Any], Never>() + + /// Observe messages for all data that is not a `WatchSyncable` message + public var rawMessagePublisher: AnyPublisher<[String: Any], Never> { + rawMessageInternalPublisher + .eraseToAnyPublisher() + } + + private let applicationContextInternalPublisher = PassthroughSubject<[String: Any], Never>() + + /// Observe application context + public var applicationContextPublisher: AnyPublisher<[String: Any], Never> { + applicationContextInternalPublisher + .eraseToAnyPublisher() + } + + private let fileTransferInternalPublisher = PassthroughSubject() + + /// Observe file transfers + public var fileTransferPublisher: AnyPublisher { + fileTransferInternalPublisher + .eraseToAnyPublisher() + } + + /// Observe messages of Type + /// + /// - Parameters: /// - ofType: Message that conforms to `WatchSyncable` protocol /// - queue: Queue to call the callback on. Defaults to `.main` /// - callback: Closure to be called when receiving a message /// - Returns: `SubscriptionToken` store this for as long as you would like to receive messages + @available(*, deprecated, message: "Please use `publisher(for:)` instead.") public func subscribeToMessages(ofType: T.Type, on queue: DispatchQueue = DispatchQueue.main, callback: @escaping SyncableMessageListener) -> SubscriptionToken { let subscription = SyncableMessageSunscription(callback: callback, dispatchQueue: queue) @@ -99,12 +145,13 @@ open class WatchSync: NSObject { return SubscriptionToken(object: rawSubscription) } - /// Observer application context, also called immediately with the most recently received context + /// Observe application context, also called immediately with the most recently received context /// /// - Parameters: /// - queue: Queue to call the callback on. Defaults to `.main` /// - callback: Closure to be called when receiving an application context /// - Returns: `SubscriptionToken` store this for as long as you would like to application contexts + @available(*, deprecated, message: "Please use `applicationContextPublisher` instead.") public func subscribeToApplicationContext(on queue: DispatchQueue = DispatchQueue.main, callback: @escaping ApplicationContextListener) -> SubscriptionToken { let rawSubscription = ApplicationContextSubscription(callback: callback, dispatchQueue: queue) @@ -119,6 +166,7 @@ open class WatchSync: NSObject { return SubscriptionToken(object: rawSubscription) } + @available(*, deprecated, message: "Please use `fileTransferPublisher` instead.") public func subscribeToFileTransfers(on queue: DispatchQueue = DispatchQueue.main, callback: @escaping FileTransferListener) -> SubscriptionToken { let rawSubscription = FileTransferSubscription(callback: callback, dispatchQueue: queue) @@ -367,6 +415,10 @@ open class WatchSync: NSObject { } subscription.callCallback(watchSyncableMessage) } + + DispatchQueue.main.async { + self.messagePublisher.send(watchSyncableMessage) + } } // If there are no message types found, just give the raw payload back if !foundMessage { @@ -377,6 +429,10 @@ open class WatchSync: NSObject { } subscription.callCallback(message) } + + DispatchQueue.main.async { + self.rawMessageInternalPublisher.send(message) + } } } } @@ -384,7 +440,7 @@ open class WatchSync: NSObject { extension WatchSync: WCSessionDelegate { // MARK: Watch Activation, multiple devices can be paired and swapped with the phone - public func session(_: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) { + public func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) { var error = error if error == nil, activationState != .activated { // We should hopefully never end up in this state, if activationState @@ -392,6 +448,10 @@ extension WatchSync: WCSessionDelegate { error = CouldNotActivateError() } activationCallback?(error) + + DispatchQueue.main.async { + self.applicationContextInternalPublisher.send(session.applicationContext) + } } #if os(iOS) @@ -470,6 +530,10 @@ extension WatchSync: WCSessionDelegate { } subscription.callCallback(applicationContext) } + + DispatchQueue.main.async { + self.applicationContextInternalPublisher.send(applicationContext) + } } /// Entrypoint for received file transfers, use `subscribeToFileTransfer` to receive these @@ -481,6 +545,10 @@ extension WatchSync: WCSessionDelegate { } subscription.callCallback(file) } + + DispatchQueue.main.async { + self.fileTransferInternalPublisher.send(file) + } } public func session(_: WCSession, didFinish fileTransfer: WCSessionFileTransfer, error: Error?) {