Skip to content

Commit

Permalink
Combine
Browse files Browse the repository at this point in the history
  • Loading branch information
nickromano committed Apr 27, 2023
1 parent 1f7d18c commit c887f19
Show file tree
Hide file tree
Showing 3 changed files with 92 additions and 35 deletions.
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ let package = Package(
name: "WatchSync",
platforms: [
.iOS(.v13),
.watchOS(.v4),
.watchOS(.v6),
],
products: [
.library(name: "WatchSync", targets: ["WatchSync"]),
Expand Down
53 changes: 21 additions & 32 deletions README.md
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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?
}
```

Expand All @@ -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)
}
}
}
```

Expand All @@ -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.
Expand Down
72 changes: 70 additions & 2 deletions Sources/WatchSync/WatchSync.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
// Copyright © 2018 Ten Minute Wait. All rights reserved.
//

import Combine
import Foundation
import Gzip
import WatchConnectivity
Expand All @@ -26,6 +27,8 @@ open class WatchSync: NSObject {

private var activationCallback: ((Error?) -> Void)?

private let messagePublisher = PassthroughSubject<any SyncableMessage, Never>()

/// Loop through these when processing an incoming message
///
/// I would prefer to use a Set here but not sure not sure how
Expand Down Expand Up @@ -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<T: SyncableMessage>(for messageType: T.Type) -> AnyPublisher<T, Never> {
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<WCSessionFile, Never>()

/// Observe file transfers
public var fileTransferPublisher: AnyPublisher<WCSessionFile, Never> {
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<T: SyncableMessage>(ofType: T.Type, on queue: DispatchQueue = DispatchQueue.main, callback: @escaping SyncableMessageListener<T>) -> SubscriptionToken {
let subscription = SyncableMessageSunscription<T>(callback: callback, dispatchQueue: queue)

Expand Down Expand Up @@ -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)

Expand All @@ -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)

Expand Down Expand Up @@ -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 {
Expand All @@ -377,21 +429,29 @@ open class WatchSync: NSObject {
}
subscription.callCallback(message)
}

DispatchQueue.main.async {
self.rawMessageInternalPublisher.send(message)
}
}
}
}

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
// isn't activated there should be an error with reason from Apple
error = CouldNotActivateError()
}
activationCallback?(error)

DispatchQueue.main.async {
self.applicationContextInternalPublisher.send(session.applicationContext)
}
}

#if os(iOS)
Expand Down Expand Up @@ -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
Expand All @@ -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?) {
Expand Down

0 comments on commit c887f19

Please sign in to comment.