{"id":17269569,"url":"https://github.com/kanecheshire/communicator","last_synced_at":"2025-05-07T17:50:16.468Z","repository":{"id":22950785,"uuid":"97765267","full_name":"KaneCheshire/Communicator","owner":"KaneCheshire","description":"Communication between iOS and watchOS apps just got a whole lot better.","archived":false,"fork":false,"pushed_at":"2023-12-14T05:46:43.000Z","size":239,"stargazers_count":152,"open_issues_count":13,"forks_count":20,"subscribers_count":6,"default_branch":"main","last_synced_at":"2025-05-05T16:58:58.734Z","etag":null,"topics":["communication","ios","messaging","watchconnectivity","watchos"],"latest_commit_sha":null,"homepage":"","language":"Swift","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/KaneCheshire.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2017-07-19T22:06:50.000Z","updated_at":"2025-04-29T04:12:52.000Z","dependencies_parsed_at":"2022-08-07T10:16:27.864Z","dependency_job_id":"ceb6fc32-f138-474e-8e60-cedaa204d0a8","html_url":"https://github.com/KaneCheshire/Communicator","commit_stats":{"total_commits":73,"total_committers":4,"mean_commits":18.25,"dds":0.1095890410958904,"last_synced_commit":"3d2b6c9f07902de7e87a5e9af2cff280de7981c9"},"previous_names":[],"tags_count":11,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/KaneCheshire%2FCommunicator","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/KaneCheshire%2FCommunicator/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/KaneCheshire%2FCommunicator/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/KaneCheshire%2FCommunicator/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/KaneCheshire","download_url":"https://codeload.github.com/KaneCheshire/Communicator/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":252931391,"owners_count":21827104,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["communication","ios","messaging","watchconnectivity","watchos"],"created_at":"2024-10-15T08:16:47.031Z","updated_at":"2025-05-07T17:50:16.444Z","avatar_url":"https://github.com/KaneCheshire.png","language":"Swift","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Communicator\n\n- [Introduction](#introduction)\n- [Quick start](#quick-start)\n- [Usage](#usage)\n  - [`Communicator`](#communicator-1)\n  - [`ImmediateMessage`](#immediatemessage)\n  - [`InteractiveImmediateMessage`](#interactiveimmediatemessage)\n  - [`GuaranteedMessage`](#guaranteedmessage)\n  - [`Blob`](#blob)\n  - [`Context`](#context)\n  - [`WatchState`](#watchstate)\n  - [`PhoneState`](#phonestate)\n  - [`ComplicationInfo`](#complicationinfo)\n- [Example](#example)\n- [Requirements](#requirements)\n- [Installation](#installation)\n  - [SPM](#swift-package-manager)\n  - [Cocoapods](#cocoapods)\n- [Author](#author)\n- [License](#license)\n\n## Introduction\n\nSending 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, some of the API calls are quite similar and it's not really clear which is needed and for what purpose.\n\n`Communicator` tries to clear all this up, handles a lot of stuff for you, and it's extremely easy to use.\n\n`Communicator` supports watch switching out-the-box, uses closures rather than delegate functions,\nand allows multiple places in your app to react to messages and events.\n\n## Quick start\n\nEach app gets its own shared `Communicator` object to use which handles all the underlying session stuff:\n\n```swift\nCommunicator.shared\n```\n\nUsage between the two platforms is essentially identical.\n\nHere's how you send a simple message with `Communicator`:\n\n```swift\nlet message = ImmediateMessage(identifier: \"1234\", content: [\"messageKey\" : \"This is some message content!\"])\nCommunicator.shared.send(message)\n```\n\nThis will try to send a message to the counterpart immediately. If the receiving app is not appropriately reachable, the message sending will fail, but you can query this any time:\n\n```swift\nswitch Communicator.shared.currentReachability {\n  case .immediateMessaging: Communicator.shared.send(message)\n  default: break\n}\n```\n\nOn the other device you register as an observer for new messages as early on as possible in your app's launch cycle:\n\n```swift\nImmediateMessage.observe { message in\n  guard message.identifier == \"1234\" else { return }\n  print(\"Message received!\", message)\n}\n```\n\nYou can observe these messages from anywhere in your app and filter out the ones you don't care about. Anything that can change or be received in `Communicator`, including `Reachability` and `WatchState`, is observable using the same syntax, just calling `observe` on the type you want to observe:\n\n```swift\nReachability.observe { reachability in\n  print(\"Reachability changed!\", reachability)\n}\n```\n\nAdditionally, you can unobserve at any time:\n\n```swift\nlet observation = Reachability.observe { _ in }\n/// ...\nReachability.unobserve(observation)\n```\n\n`Communicator` can also transfer `GuaranteedMessage`s, data `Blob`s and also sync `Context`s.\n\n`GuaranteedMessage`s are similar to `ImmediateMessage`s and `InteractiveImmediateMessage`s, in that they have an identifier, but they don't support reply handlers and can be sent when the reachability state is at least `.backgroundOnly`, and will continue to transfer even if your app is terminated during transfer.\n\n`Blob`s are perfect for sending larger amounts of data (`WatchConnectivity` will reject large data in any other message type), can be sent when the reachability state is at least `.backgroundOnly`, and will continue to transfer even if your app is terminated during transfer.\n\nYou 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. Sending or receiving a `Context` overwrites any previously sent `Context`, which you can query any time with `Communicator.shared.mostRecentlySentContext` and `Communicator.shared.mostRecentlyReceivedContext`\n\nLastly, you can update your watchOS complication from your iOS app 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.\n\nIf you have transfers available, your watch app is woken up in the background to process the `ComplicationInfo`.\n\n\u003e **NOTE:** You app must have a complication added to the user's _active_ watch face to be able to\nwake your watch up in the background, and the number of transfers available must not be 0.\n\n## Usage\n\n### Communicator\n\nEach app has its own shared `Communicator` object which it should use to communicate with the counterpart app.\n\n```swift\nCommunicator.shared\n```\n\nThe APIs between iOS and watchOS are almost identical.\n\nThe first time you access the `.shared` instance, `Communicator` will do what it needs to in order to activate the underlying session and report any received messages/data etc.\n\nThis means you should access the shared instance as early on as possible in your app's lifecycle, but also observe any changes as soon as possible to avoid losing data:\n\n```swift\nReachability.observe { reachability in\n  // Handle reachability change\n}\nImmediateMessage.observe { message in\n  // Handle immediate message\n}\nGuaranteedMessage.observe { message in\n  // Handle guaranteed message\n}\n```\n\n\u003e **NOTE:** Observing any type will impliclty access the `.shared` instance, so you only need to observe things for `Communicator` to activate the underlying session.\n\n### Querying the current reachability\n\nBefore sending any messages or data you should check the current reachability of the counterpart\napp. This can change as the user switches watches, installs your app or backgrounds your app.\n\nAdditionally, since watchOS 6, it's possible to install a watch app without installing the iOS app,\nwhich Communicator takes into account.\n\nYou can query the current reachability at any time:\n\n```swift\nlet reachability = Communicator.shared.currentReachability\n```\n\nYou can also observe and react to reachability changes:\n\n```swift\nReachability.observe { reachability in\n  // Handle reachability change\n}\n```\n\nDifferent types of communication require a different minimum level of reachability.\nI.e. `ImmediateMessage` and `InteractiveImmediateMessage` require `.immediatelyReachable`,\nbut `GuaranteedMessage`, `Blob`, `Context`, and `ComplicationInfo` require at least `.backgroundOnly`\n(although can still be sent when `.immediatelyReachable`).\n\n### Querying the current activation state\n\nYou can query the current activation state of Communicator at any time:\n\n```swift\nlet state = Communicator.shared.currentState\n```\n\nYou can also observe state changes:\n\n```swift\nCommunicator.State.observe { state in\n // Handle new state\n}\n```\n\nThe state can change as the user switches watches. Generally, you won't need to use this state and\ninstead should query the reachability, which takes into account whether the counterpart app is currently installed.\n\n### Querying the current state of the counterpart device\n\nYou can query the state of the user's paired watch at any time:\n\n```swift\nlet watchState = Communicator.shared.currentWatchState\n```\n\nYou can also observe state changes:\n\n```swift\nWatchState.observe { state in\n // Handle new state\n}\n```\n\nThe watch state provides information like whether the watch is paired, your app is installed,\na complication is added to the active watch face, and more.\n\nAdditionally, you can query the state of the iPhone from the watchOS app, since iOS 6 users\ncan install your watch app without installing the iOS app:\n\n```swift\nlet phoneState = Communicator.shared.currentPhoneState\n```\n\nAnd like all other states you can observe changes:\n\n```swift\nPhoneState.observe { state in\n  // Handle new state\n}\n```\n\n### `ImmediateMessage`\n\nAn `ImmediateMessage` is a simple object comprising of an identifier string of your choosing, and a JSON dictionary as content.\n\nThe 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 _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.\n\nThis is how you create a simple `ImmediateMessage`:\n\n```swift\nlet content: Content = [\"TotalDistanceTravelled\" : 10000.00]\nlet message = ImmediateMessage(identifier: \"JourneyComplete\", content: json)\n```\n\nAnd this is how you send it:\n\n```swift\nCommunicator.shared.send(message) { error in\n  // Handle error\n}\n```\n\nThis works well for rapid, interactive communication between two devices, but is limited to small amounts of data and will fail if either of the devices becomes unreachable during communication.\n\nIf you send this from watchOS it will also wake up your iOS app in the background if it needs to so long as the current `Reachability` is `.immediatelyReachable`.\n\nOn the receiving device you listen for new messages:\n\n```swift\nImmediateMessage.observe { message in\n  if message.identifier == \"JourneyComplete\" {\n    // Handle message\n  }\n}\n```\n\n\u003e **NOTE:** The value of `Communicator.currentReachability` must be `.immediatelyReachable` otherwise an error will occur which you can catch by assigning an error handler when sending the message.\n\n### `InteractiveImmediateMessage`\n\nAn `InteractiveImmediateMessage` is similar to a regular `ImmediateMessage` but it additionally takes\na reply handler that you _must_ execute yourself on the receiving device. Once you execute the handler\non the receiving device, it is called by the system on the sending device.\n\nThis provides a means for extremely fast communication between devices, but like an `ImmediateMessage`,\nthe reachability must be `.immediatelyReachable` during both the send and the reply.\n\nOn the sending device, send the message:\n\n```swift\nlet message = InteractiveImmediateMessage(identifier: \"message\", content: [\"hello\": \"world\"])\nCommunicator.shared.send(message) { error in\n\n}\n```\n\nAnd on the receiving device, listen for the message and execute the reply handler:\n\n```swift\nInteractiveImmediateMessage.observe { message in\n  guard message.identifier == \"message\" else { return }\n  let replyMessage = ImmediateMessage(\"identifier\", content: [\"reply\": \"message\"])\n  message.reply(replyMessage)\n}\n```\n\nLike an `ImmediateMessage`, if you send this from your watch app the system will wake your iOS app\nup in the background if needed, so long as the current reachability is `.immediatelyReachable`.\n\n### `GuaranteedMessage`\n\nYou can also choose to send a message using the \"guaranteed\" method. `GuaranteedMessage`s 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:\n\n```swift\nlet content: Content = [\"CaloriesBurnt\" : 400.00]\nlet message = GuaranteedMessage(identifier: \"WorkoutComplete\", content: content)\nCommunicator.shared.send(message) { result in\n  // Handle success or failure\n}\n```\n\nBecause 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, i.e. in your `AppDelegate` or `ExtensionDelegate`:\n\n```swift\nGuaranteedMessage.observe { message in\n  if message.identifier == \"CaloriesBurnt\" {\n    let content = message.content\n    // Handle message\n  }\n}\n```\n\n\u003e **NOTE:** On watchOS, receiving a `GuaranteedMessage` while in the background can cause the system to generate a `WKWatchConnectivityRefreshBackgroundTask`. If you assign this to the `Communicator`'s `task` property, `Communicator` will automatically handle ending the task for you at the right time.\n\nThe value of `Communicator.currentReachability` must not be `.notReachable` otherwise an error will occur.\n\n### `Blob`\n\nA `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.\n\nThis is how you create a `Blob`:\n\n```swift\nlet largeData: Data = getJourneyHistoryData()\nlet blob = Blob(identifier: \"JourneyHistory\", content: largeData)\n```\n\nAnd this is how you transfer it to the other device:\n\n```swift\nCommunicator.shared.transfer(blob: blob) { result in\n  // Handle success or failure\n}\n```\n\nBecause 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.\n\nOn the receiving device you listen for new `Blob`s. Because these `Blob`s 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 `Blob`s as soon as possible, i.e. in the `AppDelegate` or `ExtensionDelegate`:\n\n```swift\nBlob.observe { blob in\n  if blob.identifier == \"JourneyHistory\" {\n    let JourneyHistoryData: Data = blob.content\n    // ... do something with the data ... //\n  }\n}\n```\n\nAdditionally, you can also attach some metadata to a `Blob`, by passing in a dictionary of plist values when creating the `Blob`:\n\n```swift\nlet metadata = [\"DateGenerated\": Date()]\nlet blobWithMetadata = Blob(identifier: \"JourneyHistory\", content: data, metadata: metadata)\n```\n\nAnd then on the receiving device you can query the metadata on the received `Blob`:\n\n```swift\nBlob.observe { blob in\n    print(blob.metadata)\n}\n```\n\n\u003e **NOTE:** On watchOS, receiving a `Blob` while in the background can cause the system to generate a `WKWatchConnectivityRefreshBackgroundTask`. If you assign this to the `Communicator`'s `task` property, `Communicator` will automatically handle ending the task for you at the right time.\n\nThe value of `Communicator.currentReachability` must not be `.notReachable` otherwise an error will occur.\n\n### `Context`\n\nA `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.\n\nA `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:\n\n```swift\nlet content: Content = [\"ShowTotalDistance\" : true]\nlet context = Context(content: content)\ndo {\n  try Communicator.shared.sync(context)\n} catch {\n  // Handle error\n}\n```\n\nYou can also query the last sent context from either device:\n\n```swift\nlet context = Communicator.shared.mostRecentlySentContext\n```\n\nOn the receiving device you listen for new `Context`s:\n\n```swift\nContent.observe { context in\n  if let shouldShowTotalDistance = context.content[\"ShowTotalDistance\"] as? Bool {\n    print(\"Show total distance setting changed: \\(shouldShowTotalDistance)\")\n  }\n}\n```\n\nYou can also query the last received context from either device:\n\n```swift\nlet context = Communicator.shared.mostRecentlyReceivedContext\n```\n\n\u003e **NOTE:** On watchOS, receiving a `Context` while in the background can cause the system to generate a `WKWatchConnectivityRefreshBackgroundTask`. If you assign this to the `Communicator`'s `task` property, `Communicator` will automatically handle ending the task for you at the right time.\n\nThe value of `Communicator.currentReachability` must not be `.notReachable` otherwise an error will be thrown.\n\n### `WatchState`\n\n`WatchState` is one of the only iOS-only elements of `Communicator`. It provides some information\nabout the current state of the user's paired watch or watches, like whether a complication has been enabled\nor whether the watch app has been installed.\n\nYou can observe any changes in the `WatchState` on iOS:\n\n```swift\nWatchState.observe { state in\n  // Handle watch state\n}\n```\n\nYou can also query the current `WatchState` at any time from the iOS `Communicator`:\n\n```swift\nlet watchState = Communicator.shared.currentWatchState\n```\n\nYou can use `WatchState` retrieve a URL which points to a directory on the iOS device specific to the currently paired watch.\n\nYou 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 or unpairs their watch.\n\n### `PhoneState`\n\n`PhoneState` is similar to the `WatchState` but is queried from the watch's side instead.\n\nSince watchOS 6, users can install watch apps without installing the iOS app, and you can\nuse `PhoneState` to determine this.\n\n### `ComplicationInfo`\n\nA `ComplicationInfo` can only be sent from an iOS device, and can only be received on a watchOS device.\nIts purpose is to wake the watchOS app in the background to process the data and update its complication. At the time of writing your iOS app 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.\n\nJust like a `Context`, a `ComplicationInfo` has no identifier and its content is a JSON dictionary:\n\n```swift\nlet content: Content = [\"NumberOfStepsWalked\" : 1000]\nlet complicationInfo = ComplicationInfo(content: content)\n```\n\nAnd you send it from the iOS app like this:\n\n```swift\nCommunicator.shared.transfer(complicationInfo) { result in\n  // Handle success or failure\n}\n```\n\nUpon successful transfer, the `success` case in the `result` provides the remaining complication\nupdates available that day.\n\nOn the watchOS side you observe new `ComplicationInfo`s 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`:\n\n```swift\nComplicationInfo.observe { complicationInfo in\n  // Handle update\n}\n```\n\nThe value of `Communicator.currentReachability` must not be `.notReachable` otherwise an error will be thrown.\n\n\u003e **NOTE:** On watchOS, receiving a `ComplicationInfo` while in the background can cause the system to generate a `WKWatchConnectivityRefreshBackgroundTask`. If you assign this to the `Communicator`'s `task` property, `Communicator` will automatically handle ending the task for you at the right time.\n\n## Example\n\nTo run the example project, clone the repo, and run `pod install` from the Example directory first.\n\nThe watchOS and iOS example apps set up observers for new `Message`s, `Blob`s, reachability changes etc and prints out any\nchanges to the console. They set up these observers early on in the app, which is recommended for state changes and\nobservers of things that may have transferred while the app was terminated, like `Blob`s.\n\nTry running each target and seeing the output when you interact with the buttons.\n\n## Requirements\n\nCommunicator relies on `WatchConnectivity`, Apple's framework for communicating between iOS and watchOS apps, but has no external dependencies.\n\nCommunicator requires iOS 10.0 and newer and watchOS 3.0 and newer.\n\n## Installation\n\n### Swift Package Manager\n\nCommunicator supports SPM, simply add Communicator as a package dependency in Xcode 11 or newer.\n\n### Cocoapods\n\nAdd the following line to your Podfile and then run `pod install` in Terminal:\n\n```ruby\npod \"Communicator\"\n```\n\n## Author\n\nKane Cheshire, [@kanecheshire](https://twitter.com/kanecheshire)\n\n## License\n\nCommunicator is available under the MIT license. See the LICENSE file for more info.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkanecheshire%2Fcommunicator","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fkanecheshire%2Fcommunicator","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkanecheshire%2Fcommunicator/lists"}