Latest3.1.0
Homepagehttps://github.com/KaneCheshire/Communicator
LicenseMIT
Platformsios 9.3, watchos 2.2
DependenciesTABObserverSet
FrameworksWatchConnectivity
Authors

CI Status
Version
License
Platform

Introduction

Sending messages and data between watchOS and iOS apps is possible thanks to Apple’s work on WatchConnectivity, however there are a lot of delegate callbacks to work with, and some of the API calls are quite similar and it’s not really clear which is needed and for what purpose.

Communicator means you don’t have to spend any time writing a cross-platform wrapper around WatchConnectivity and is extremely easy to use.

Each app gets its own shared Communicator object to use which handles all the underlying session stuff:

Communicator.shared

Usage between the two platforms is essentially identical, so you can use it in a shared framework with no workarounds.

Here’s how you send a simple message with Communicator.

let message = ImmediateMessage(identifier: "1234", content: ["messageKey" : "This is some message content!"])
try? Communicator.shared.send(immediateMessage: message)

This will try to send a message to the counterpart immediately. If the underlying session is not active, the try will fail and Communicator will throw an error you can catch if you want.

On the other device you register as an observer for new messages:

Communicator.shared.immedateMessageReceivedObservers.add { message in
    if message.identifier == "1234" {
        print("Message received: (message.content)")
    }
}

You can observe these messages from anywhere in your app and filter out the ones you don’t care about.

Communicator can also transfer Blobs and sync Contexts.

Blobs are perfect for sending larger amounts of data (WatchConnectivity will reject large data in any other message type), and will continue to transfer even if your app
is terminated during transfer.

You can use a Context to keep things in sync between devices, which makes it perfect for preferences. Contexts are not suitable for messaging or sending large data.

Lastly, you can update your watchOS complication from your iOS by transferring a ComplicationInfo. You get a limited number of ComplicationInfo transfers a day, and you can easily query the remaining number of transfers available by getting the currentWatchState object and inspecting the numberOfComplicationInfoTransfersAvailable property.

Usage

Communicator

Each app has its own shared Communicator object which it should use to communicate with the counterpart app.

Communicator.shared

The APIs between iOS and watchOS are almost identical, so
you can use Communicator anywhere, including in a shared iOS-watchOS framework.

Communicator uses ObserverSets to notify observers/listeners when events occur, like an ImmediateMessage being received, or the activation state of the underlying session changing.

You can query the current reachability of the counterpart app at any time using the currentReachability propery.

ImmediateMessage

An ImmediateMessage is a simple object comprising of an identifier string of your choosing, and a JSON dictionary as content.

The keys of the JSON dictionary must be strings, and the values must be plist-types. That means anything you can save to UserDefaults; String, Int, Data etc. You also cannot send large amounts of data between devices using a ImmediateMessage because the system will reject it. Instead, use a Blob for sending large amounts of data.

This is how you create a simple ImmediateMessage:

let json: JSONDictionary = ["TotalDistanceTravelled" : 10000.00]
let message = ImmediateMessage(identifier: "JourneyComplete", content: json) // Optionally pass in a reply handler here

And this is how you send it:

try? Communicator.shared.send(immediateMessage: message)

This works well for rapid communication between two devices, but is limited to small amounts of data and will fail if either of the devices becomes unreachable during communication.

If you send this from watchOS it will also wake up your iOS app in the background if it needs to.

You can also assign a replyHandler to the message. On the sending device, this replyHandler is executed by the system when the receiving device executes it on its end.

On the receiving device you listen for new messages, check the identifier and then execute the replyHandler (if you assigned one when creating the message), which will allow the system on the sending device to execute the replyHandler there. The replyHandler also expects a JSON dictionary just like the content of the message.

Communicator.shared.immediateMessageReceivedObservers.add { message in
    if message.identifier == "JourneyComplete" {
      let replyJSON: JSONDictionary = ["JourneyProcessed" : true]
      message.replyHandler?(replyJSON) // Optional because you might not have set one on the sender's side
    }
}

The value of Communicator.currentReachability must be .immediateMessage otherwise an error will be thrown.

GuaranteedMessage

You can also choose to send a message using the guaranteed method. GuaranteedMessages don’t have a reply handler because messages can be queued while the receiving device is not currently receiving messages, meaning they’re queued until the session is next created:

let json: JSONDictionary = ["CaloriesBurnt" : 400.00]
let message = GuaranteedMessage(identifier: "WorkoutComplete", content: json)
try? Communicator.shared.send(guaranteedMessage: message)

Because the messages are queued, they could be received in a stream on the receiving device when it’s able to process them. You should make sure your observers are set up as soon as possible to avoid missing any messages.

Communicator.shared.guaranteedMessageReceivedObservers.add { message in
    if message.identifier == "CaloriesBurnt" {
      let content = message.content
      // Handle message
    }
}

On watchOS, receiving a "guaranteed" GuaranteedMessage while in the background can cause the system to generate a WKWatchConnectivityRefreshBackgroundTask which you are responsible for handling in your ExtensionDelegate.

Blob

A Blob is very similar to a GuaranteedMessage but is better suited to sending larger bits of data. A Blob is created with an identifier but instead of assigning a JSON dictionary as the content, you assign pure Data instead.

This is how you create a Blob:

let largeData: Data = getJourneyHistoryData()
let blob = Blob(identifier: "JourneyHistory", content: largeData)

And this is how you transfer it to the other device:

try? Communicator.shared.transfer(blob: blob)

Because a Blob can be much larger than a Message, it might take significantly longer to send. The system handles this, and continues to send it even if the sending device becomes unreachable before it has completed.

On the receiving device you listen for new Blobs. Because these Blobs can often be queued waiting for the session to start again, Communicator will often notify observers very early on. This makes it a good idea to start observing for Blobs as soon as possible, like in the AppDelegate or ExtensionDelegate.

Communicator.shared.blobReceivedObservers.add { blob in
    if blob.identifier == "JourneyHistory" {
      let JourneyHistoryData: Data = blob.content
      // ... do something with the data ... //
    }
}

You can also assign a completion handler when creating a Blob, which will give you an error if an error was detected by the system.

On watchOS, receiving a Blob while in the background can cause the system to generate a WKWatchConnectivityRefreshBackgroundTask which you are responsible for handling in your ExtensionDelegate.

The value of Communicator.currentReachability must not be .notReachable otherwise an error will be thrown.

Context

A Context is a very lightweight object. A Context can be sent and received by either device, and the system stores the last sent/received Context that you can query at any time. This makes it ideal for syncing lightweight things like preferences between devices.

A Context has no identifier, and simply takes a JSON dictionary as content. Like an ImmediateMessage, this content must be primitive types like String, Int, Data etc, and must not be too large or the system will reject it:

let json: JSONDictionary = ["ShowTotalDistance" : true]
let context = Context(content: json)
try? Communicator.shared.sync(context: context)

On the receiving device you listen for new Contexts:

Communicator.shared.contextUpdatedObservers.add { context in
  if let shouldShowTotalDistance = context.content["ShowTotalDistance"] as? Bool {
    print("Show total distance setting changed: (shouldShowTotalDistance)")
  }
}

On watchOS, receiving a Context while in the background can cause the system to generate a WKWatchConnectivityRefreshBackgroundTask which you are responsible for handling in your ExtensionDelegate.

The value of Communicator.currentReachability must not be .notReachable otherwise an error will be thrown.

WatchState

WatchState is one of the only iOS-only elements of Communicator. It provides some information
about the current state of the user’s paired watch or watches, like whether a complication has been enabled
or whether the watch app has been installed.

You can observe any changes in the WatchState using the shared communicator object on iOS:

Communicator.shared.watchStateUpdatedObservers.add { watchState in
   print("Watch state changed: (watchState)")
}

You can also query the current WatchState at any time from the iOS Communicator:

let watchState = Communicator.shared.currentWatchState

A WatchState will let you check if the user has a paired watch:

watchState.isPaired

Whether the currently paired watch has your watch app installed:

watchState.isWatchAppInstalled

Whether the currently paired watch has one of your complications enabled:

watchState.isComplicationEnabled

The number of complication info transfers available today (returns 0 if isComplicationEnabled returns false):

watchState.numberOfComplicationInfoTransfersAvailable // This will be -1 on anything older than iOS 10

And also, you can query a URL which points to a directory on the iOS device specific to the currently paired watch.
You can use this directory to store things specific to that watch, which you don’t want associated with the user’s other watches. This directory (and anything in it) is automatically deleted by the system if the user uninstalls your watchOS app.

watchState.watchSpecificDirectoryURL

ComplicationInfo

A ComplicationInfo can only be sent from an iOS device, and can only be received on a watchOS device.
Its purpose is to wake the watchOS app to process the data and update its complication. At the time of writing
your iOS can do this 50 times a day, and you can query the currentWatchState of the shared Communicator object
on iOS to find out how many remaining updates you have left.

Just like a Context, a ComplicationInfo has no identifier and its content is a JSON dictionary:

let json: JSONDictionary = ["NumberOfStepsWalked" : 1000]
let complicationInfo = ComplicationInfo(content: json)

And you send it from the iOS app like this:

try? Communicator.shared.transfer(complicationInfo: complicationInfo)

On the watchOS side you observe new ComplicationInfos being received. Just like other transfers that may happen
in the background, it’s a good idea to observe these early on, like in the ExtensionDelegate:

Communicator.shared.complicationInfoReceivedObservers.add { complicationInfo in
    print("Received complication info: (complicationInfo)")
    // ... update your complications ... //
}

You are responsible for handling and completing any WKWatchConnectivityRefreshBackgroundTask handed to your ExtensionDelegate as a result of transferring a ComplicationInfo, which is why observing these updates in the ExtensionDelegate is recommended.

The value of Communicator.currentReachability must not be .notReachable otherwise an error will be thrown.

Example

To run the example project, clone the repo, and run pod install from the Example directory first.

The watchOS and iOS example apps set up observers for new Messages, Blobs, reachability changes etc and prints out any
changes to the console. They set up these observers early on in the app, which is recommended for state changes and
observers of things that may have transferred while the app was terminated, like Blobs.

Each app has some simple buttons which kick off sending a Message (with a reply handler), transferring a Blob and syncing a Context. Try running each target and seeing the output when you interact with the buttons.

Requirements

Communicator relies on WatchConnectivity, Apple’s framework for communicating between iOS and watchOS apps,
and also heavily relies on TABObserverSet as an external dependency.

Communicator requires iOS 9.3 and newer and watchOS 2.2 and newer.

Installation

Communicator is available through CocoaPods. To install
it, simply add the following line to your Podfile and then run pod install in Terminal:

pod "Communicator"

Author

Kane Cheshire, @kanecheshire

License

Communicator is available under the MIT license. See the LICENSE file for more info.

Latest podspec

{
    "name": "Communicator",
    "version": "3.1.0",
    "summary": "Communication between iOS and watchOS apps just got a whole lot better.",
    "description": "Sending messages and data between watchOS and iOS appsnis possible thanks to Apple's work on `WatchConnectivity`,nbut there are a lot of delegate callbacks to work with,nsome of the API calls are similar, and it's not reallynclear which is needed for what purpose.nn`Communicator` means you don't have to spend any time writing a cross-platform wrapper around `WatchConnectivity` and is extremely easy to use.nnEach app gets its own shared `Communicator` object to use which handles all the underlying session stuff:nn```swiftnCommunicator.sharedn```nnUsage between the two platforms is identical, so you cannuse it in a shared framework with few workarounds.nnHere's how you send a simple message with Communicator.nn```swiftnlet message = ImmediateMessage(identifier: "1234", content: ["messageKey" : "This is some message content!"])ntry? Communicator.shared.send(immediateMessage: message)n```nnThis will try to send a message to the counterpart immediately. If the underlying session is not active, the `try` will fail and Communicator will `throw` an error you can catch if you want.nnOn the other device you register as an observer for new messages:nn```swiftnCommunicator.shared.immediateMessageReceivedObservers.add { message inn    if message.identifier == "1234" {n        print("Message received: (message.content)")n    }n}n```nnThe great thing about using this style of observing means that you can observe these messages from anywhere in your app and filter out the ones you don't care about.nn`Communicator` can also transfer `Blob`s and sync `Context`s.nn`Blob`s are perfect for sending larger amounts of data (`WatchConnectivity` will reject large data in other types of messages), and will continue to transfer even if your appnis terminated during transfer.nnYou can use a `Context` to keep things in sync between devices, which makes it perfect for preferences. `Context`s are not suitable for messaging or sending large data.",
    "homepage": "https://github.com/KaneCheshire/Communicator",
    "license": {
        "type": "MIT",
        "file": "LICENSE"
    },
    "authors": {
        "Kane Cheshire": "[email protected]"
    },
    "source": {
        "git": "https://github.com/KaneCheshire/Communicator.git",
        "tag": "3.1.0"
    },
    "social_media_url": "https://twitter.com/kanecheshire",
    "platforms": {
        "ios": "9.3",
        "watchos": "2.2"
    },
    "source_files": "Communicator/Classes/**/*",
    "frameworks": "WatchConnectivity",
    "dependencies": {
        "TABObserverSet": [
            "2.1.0"
        ]
    }
}

Pin It on Pinterest

Share This