WatchConnectivitySwift
A modern async/await based convenience package on top of Apple's WatchConnectivity framework
Install / Use
/learn @ts95/WatchConnectivitySwiftREADME
WatchConnectivitySwift
WatchConnectivitySwift is a modern, type-safe Swift library that simplifies communication between iOS and watchOS using Apple's WatchConnectivity framework. Built from the ground up with Swift 6 strict concurrency, async/await, and strong typing, it enables you to write robust, reactive, and testable communication layers between your iPhone and Apple Watch apps.
Features
- Type-safe request/response using protocols with associated types
- Swift 6 strict concurrency compliant with
@MainActorisolation - Automatic retry with configurable retry policies for reliable message delivery
- Fallback delivery strategies (message -> userInfo -> context)
- File transfers with progress tracking and async/await support
- Shared state synchronization via
applicationContext - Session health monitoring with automatic recovery
- SwiftUI integration via
ObservableObjectwith@Publishedproperties - Comprehensive diagnostics via
AsyncStreamevents
Requirements
| Platform | Minimum Version | |----------|-----------------| | iOS | 17.0+ | | watchOS | 10.0+ | | Swift | 6.0+ | | Xcode | 16.0+ |
Installation
Swift Package Manager
Add the following to your Package.swift:
dependencies: [
.package(url: "https://github.com/ts95/WatchConnectivitySwift.git", from: "5.2.0")
]
Or in Xcode: File > Add Package Dependencies, then enter the repository URL.
Quick Start
1. Define Your Request Types (Shared Code)
Create a shared Swift file or framework that both your iOS and watchOS targets can access. This ensures both sides understand the same request/response types.
// 📁 Shared/WatchRequests.swift
// ⚠️ This file must be included in BOTH your iOS and watchOS targets,
// or placed in a shared framework that both targets depend on.
import WatchConnectivitySwift
// Define a request that the Watch sends to the iPhone.
// The iPhone will handle this and return a Recipe.
struct FetchRecipeRequest: WatchRequest {
// The response type that the handler must return
typealias Response = Recipe
let recipeID: String
}
// The response model - must be Codable for serialization over the wire
struct Recipe: Codable, Sendable {
let title: String
let ingredients: [String]
}
2. Register Handlers on iPhone (iOS Target)
On the iOS side, register handlers for requests that the Watch will send. The iPhone acts as the "server" that responds to Watch requests.
// 📁 iOS App/AppCoordinator.swift
// 💡 This code runs ONLY on the iPhone
import WatchConnectivitySwift
@MainActor
class AppCoordinator {
// Create a single WatchConnection instance for your iOS app
let connection = WatchConnection()
func setup() {
// Register a handler for FetchRecipeRequest.
// When the Watch sends this request, this closure is called
// and the returned Recipe is sent back to the Watch.
connection.register(FetchRecipeRequest.self) { request in
// Access your iOS app's data sources here:
// - Core Data, SwiftData, Realm
// - Network APIs, Firestore
// - UserDefaults, Keychain, etc.
return Recipe(
title: "Pasta Carbonara",
ingredients: ["Pasta", "Eggs", "Pancetta", "Parmesan"]
)
}
}
}
3. Send Requests from Watch (watchOS Target)
On the watchOS side, send requests to the iPhone and await the typed response.
// 📁 watchOS App/RecipeViewModel.swift
// 💡 This code runs ONLY on the Apple Watch
import WatchConnectivitySwift
@MainActor
class RecipeViewModel: ObservableObject {
@Published var recipe: Recipe?
@Published var error: Error?
// Create a single WatchConnection instance for your watchOS app
private let connection = WatchConnection()
func loadRecipe(id: String) async {
do {
// Send the request to the iPhone and await the response.
// The iPhone's registered handler will process this and return a Recipe.
recipe = try await connection.send(
FetchRecipeRequest(recipeID: id),
strategy: .messageWithUserInfoFallback // Falls back to queued delivery if unreachable
)
} catch {
self.error = error
}
}
}
Note: Communication can go both ways. The Watch can also register handlers, and the iPhone can send requests to the Watch using the same pattern.
Core Concepts
Request Types
// 📁 Shared/Requests.swift
// ⚠️ Place in shared code accessible by both iOS and watchOS targets
// Standard request expecting a response
struct MyRequest: WatchRequest {
typealias Response = MyResponse
let data: String
}
struct MyResponse: Codable, Sendable {
let result: String
}
// Fire-and-forget request (no response expected)
// Useful for logging, analytics, or notifications where you don't need confirmation
struct LogEventRequest: FireAndForgetRequest {
let eventName: String
}
Delivery Strategies
Choose how messages are delivered based on your reliability needs. WatchConnectivity provides three transport mechanisms with different tradeoffs:
| Transport | Speed | Reliability | Behavior |
|-----------|-------|-------------|----------|
| sendMessage | Instant | May fail | Requires counterpart app to be reachable |
| transferUserInfo | Queued | Guaranteed | Delivers in order when app becomes active |
| applicationContext | Queued | Guaranteed | Only latest value delivered (overwrites pending) |
Available strategies:
// DEFAULT: Best for most use cases
// Instant delivery when possible, queued backup when not
.messageWithUserInfoFallback
// For settings/state where only latest value matters
// If you send 5 updates while offline, only the last one is delivered
.messageWithContextFallback
// For real-time features only (remote control, live updates)
// Fails immediately if counterpart is unreachable
.messageOnly
// For background sync where order matters
// All messages queued and delivered in order, even if app is suspended
.userInfoOnly
// For state sync where only current value matters
// Overwrites any pending value not yet delivered
.contextOnly
When to use each:
| Strategy | Use Case |
|----------|----------|
| messageWithUserInfoFallback | Chat messages, notifications, data requests—anything that must eventually arrive |
| messageWithContextFallback | Settings sync, preferences, status updates where stale values are useless |
| messageOnly | Remote camera shutter, live game controls, time-sensitive actions |
| userInfoOnly | Workout logs, transaction history, audit trails that need ordering |
| contextOnly | Current user state, now-playing info, connection status |
Retry Policies
Configure retry behavior for transient failures:
// Built-in policies
.default // 3 attempts, 10s timeout, 200ms delay between retries
.patient // 5 attempts, 30s timeout, 200ms delay between retries
.none // 1 attempt, no retries
// Custom policy
RetryPolicy(maxAttempts: 4, timeout: .seconds(15))
Shared State
Synchronize state between devices. SharedState uses applicationContext under the hood, which automatically syncs the latest value to the counterpart device.
// 📁 Shared/AppSettings.swift
// ⚠️ The model must be in shared code accessible by both targets
struct AppSettings: Codable, Sendable, Equatable {
var theme: String
var notificationsEnabled: Bool
}
// 📁 iOS App/SettingsManager.swift (or watchOS App/SettingsManager.swift)
// 💡 Use the same pattern on BOTH iOS and watchOS targets.
// Each side creates its own SharedState instance with the same structure.
// Updates from either side automatically sync to the other.
import WatchConnectivitySwift
@MainActor
class SettingsManager {
let sharedSettings: SharedState<AppSettings>
init(connection: WatchConnection) {
// Both iOS and watchOS create a SharedState with the same initial value.
// The library handles syncing updates between devices automatically.
sharedSettings = SharedState(
initialValue: AppSettings(theme: "light", notificationsEnabled: true),
connection: connection // Pass the WatchConnection instance
)
}
func updateTheme(_ theme: String) throws {
// Update locally - the change is automatically pushed to the other device
var settings = sharedSettings.value
settings.theme = theme
try sharedSettings.update(settings)
}
}
### File Transfers
Transfer files between devices with progress tracking. Files are transferred in the background and continue even when your app is suspended.
```swift
// 📁 iOS App/FileTransferManager.swift (or watchOS App/FileTransferManager.swift)
// 💡 File transfers work in both directions between iOS and watchOS
import WatchConnectivitySwift
@MainActor
class FileTransferManager {
private let connection = WatchConnection()
// MARK: - Sending Files
func sendFile(_ fileURL: URL, metadata: [String: Any]? = nil) async throws {
// Start the transfer and get a FileTransfer object for tracking
let transfer = try await connection.transferFile(fileURL, metadata: metadata)
// Option 1: Wait for completion
try await transfer.waitForCompletion()
// Option 2: Track progress
for await progress in transfer.progressUpdates {
