{"id":20499398,"url":"https://github.com/ts95/watchconnectivityswift","last_synced_at":"2026-04-17T18:32:35.946Z","repository":{"id":68302544,"uuid":"592910341","full_name":"ts95/WatchConnectivitySwift","owner":"ts95","description":"A modern async/await based convenience package on top of Apple's WatchConnectivity framework","archived":false,"fork":false,"pushed_at":"2026-02-10T11:36:36.000Z","size":124,"stargazers_count":5,"open_issues_count":0,"forks_count":1,"subscribers_count":1,"default_branch":"main","last_synced_at":"2026-02-10T16:18:41.406Z","etag":null,"topics":["ios","watchconnectivity","watchos","wcsession"],"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/ts95.png","metadata":{"files":{"readme":"README.md","changelog":null,"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,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2023-01-24T19:51:13.000Z","updated_at":"2026-02-10T11:36:39.000Z","dependencies_parsed_at":null,"dependency_job_id":"80e88808-c739-49c8-92e2-6613cdc463b5","html_url":"https://github.com/ts95/WatchConnectivitySwift","commit_stats":null,"previous_names":[],"tags_count":5,"template":false,"template_full_name":null,"purl":"pkg:github/ts95/WatchConnectivitySwift","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ts95%2FWatchConnectivitySwift","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ts95%2FWatchConnectivitySwift/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ts95%2FWatchConnectivitySwift/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ts95%2FWatchConnectivitySwift/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ts95","download_url":"https://codeload.github.com/ts95/WatchConnectivitySwift/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ts95%2FWatchConnectivitySwift/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31940858,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-17T17:29:20.459Z","status":"ssl_error","status_checked_at":"2026-04-17T17:28:47.801Z","response_time":62,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"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":["ios","watchconnectivity","watchos","wcsession"],"created_at":"2024-11-15T18:17:17.343Z","updated_at":"2026-04-17T18:32:35.933Z","avatar_url":"https://github.com/ts95.png","language":"Swift","funding_links":[],"categories":[],"sub_categories":[],"readme":"# WatchConnectivitySwift\n\n![Version](https://img.shields.io/badge/version-5.2.0-blue.svg)\n![Swift](https://img.shields.io/badge/Swift-6.0-orange.svg)\n![iOS](https://img.shields.io/badge/iOS-17.0+-green.svg)\n![watchOS](https://img.shields.io/badge/watchOS-10.0+-green.svg)\n![License](https://img.shields.io/badge/license-MIT-lightgrey.svg)\n\n**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.\n\n---\n\n## Features\n\n- **Type-safe request/response** using protocols with associated types\n- **Swift 6 strict concurrency** compliant with `@MainActor` isolation\n- **Automatic retry** with configurable retry policies for reliable message delivery\n- **Fallback delivery strategies** (message -\u003e userInfo -\u003e context)\n- **File transfers** with progress tracking and async/await support\n- **Shared state synchronization** via `applicationContext`\n- **Session health monitoring** with automatic recovery\n- **SwiftUI integration** via `ObservableObject` with `@Published` properties\n- **Comprehensive diagnostics** via `AsyncStream` events\n\n---\n\n## Requirements\n\n| Platform | Minimum Version |\n|----------|-----------------|\n| iOS      | 17.0+           |\n| watchOS  | 10.0+           |\n| Swift    | 6.0+            |\n| Xcode    | 16.0+           |\n\n---\n\n## Installation\n\n### Swift Package Manager\n\nAdd the following to your `Package.swift`:\n\n```swift\ndependencies: [\n    .package(url: \"https://github.com/ts95/WatchConnectivitySwift.git\", from: \"5.2.0\")\n]\n```\n\nOr in Xcode: File \u003e Add Package Dependencies, then enter the repository URL.\n\n---\n\n## Quick Start\n\n### 1. Define Your Request Types (Shared Code)\n\nCreate 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.\n\n```swift\n// 📁 Shared/WatchRequests.swift\n// ⚠️ This file must be included in BOTH your iOS and watchOS targets,\n//    or placed in a shared framework that both targets depend on.\n\nimport WatchConnectivitySwift\n\n// Define a request that the Watch sends to the iPhone.\n// The iPhone will handle this and return a Recipe.\nstruct FetchRecipeRequest: WatchRequest {\n    // The response type that the handler must return\n    typealias Response = Recipe\n\n    let recipeID: String\n}\n\n// The response model - must be Codable for serialization over the wire\nstruct Recipe: Codable, Sendable {\n    let title: String\n    let ingredients: [String]\n}\n```\n\n### 2. Register Handlers on iPhone (iOS Target)\n\nOn the iOS side, register handlers for requests that the Watch will send. The iPhone acts as the \"server\" that responds to Watch requests.\n\n```swift\n// 📁 iOS App/AppCoordinator.swift\n// 💡 This code runs ONLY on the iPhone\n\nimport WatchConnectivitySwift\n\n@MainActor\nclass AppCoordinator {\n    // Create a single WatchConnection instance for your iOS app\n    let connection = WatchConnection()\n\n    func setup() {\n        // Register a handler for FetchRecipeRequest.\n        // When the Watch sends this request, this closure is called\n        // and the returned Recipe is sent back to the Watch.\n        connection.register(FetchRecipeRequest.self) { request in\n            // Access your iOS app's data sources here:\n            // - Core Data, SwiftData, Realm\n            // - Network APIs, Firestore\n            // - UserDefaults, Keychain, etc.\n            return Recipe(\n                title: \"Pasta Carbonara\",\n                ingredients: [\"Pasta\", \"Eggs\", \"Pancetta\", \"Parmesan\"]\n            )\n        }\n    }\n}\n```\n\n### 3. Send Requests from Watch (watchOS Target)\n\nOn the watchOS side, send requests to the iPhone and await the typed response.\n\n```swift\n// 📁 watchOS App/RecipeViewModel.swift\n// 💡 This code runs ONLY on the Apple Watch\n\nimport WatchConnectivitySwift\n\n@MainActor\nclass RecipeViewModel: ObservableObject {\n    @Published var recipe: Recipe?\n    @Published var error: Error?\n\n    // Create a single WatchConnection instance for your watchOS app\n    private let connection = WatchConnection()\n\n    func loadRecipe(id: String) async {\n        do {\n            // Send the request to the iPhone and await the response.\n            // The iPhone's registered handler will process this and return a Recipe.\n            recipe = try await connection.send(\n                FetchRecipeRequest(recipeID: id),\n                strategy: .messageWithUserInfoFallback  // Falls back to queued delivery if unreachable\n            )\n        } catch {\n            self.error = error\n        }\n    }\n}\n```\n\n\u003e **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.\n\n---\n\n## Core Concepts\n\n### Request Types\n\n```swift\n// 📁 Shared/Requests.swift\n// ⚠️ Place in shared code accessible by both iOS and watchOS targets\n\n// Standard request expecting a response\nstruct MyRequest: WatchRequest {\n    typealias Response = MyResponse\n    let data: String\n}\n\nstruct MyResponse: Codable, Sendable {\n    let result: String\n}\n\n// Fire-and-forget request (no response expected)\n// Useful for logging, analytics, or notifications where you don't need confirmation\nstruct LogEventRequest: FireAndForgetRequest {\n    let eventName: String\n}\n```\n\n### Delivery Strategies\n\nChoose how messages are delivered based on your reliability needs. WatchConnectivity provides three transport mechanisms with different tradeoffs:\n\n| Transport | Speed | Reliability | Behavior |\n|-----------|-------|-------------|----------|\n| `sendMessage` | Instant | May fail | Requires counterpart app to be reachable |\n| `transferUserInfo` | Queued | Guaranteed | Delivers in order when app becomes active |\n| `applicationContext` | Queued | Guaranteed | Only latest value delivered (overwrites pending) |\n\n**Available strategies:**\n\n```swift\n// DEFAULT: Best for most use cases\n// Instant delivery when possible, queued backup when not\n.messageWithUserInfoFallback\n\n// For settings/state where only latest value matters\n// If you send 5 updates while offline, only the last one is delivered\n.messageWithContextFallback\n\n// For real-time features only (remote control, live updates)\n// Fails immediately if counterpart is unreachable\n.messageOnly\n\n// For background sync where order matters\n// All messages queued and delivered in order, even if app is suspended\n.userInfoOnly\n\n// For state sync where only current value matters\n// Overwrites any pending value not yet delivered\n.contextOnly\n```\n\n**When to use each:**\n\n| Strategy | Use Case |\n|----------|----------|\n| `messageWithUserInfoFallback` | Chat messages, notifications, data requests—anything that must eventually arrive |\n| `messageWithContextFallback` | Settings sync, preferences, status updates where stale values are useless |\n| `messageOnly` | Remote camera shutter, live game controls, time-sensitive actions |\n| `userInfoOnly` | Workout logs, transaction history, audit trails that need ordering |\n| `contextOnly` | Current user state, now-playing info, connection status |\n\n### Retry Policies\n\nConfigure retry behavior for transient failures:\n\n```swift\n// Built-in policies\n.default     // 3 attempts, 10s timeout, 200ms delay between retries\n.patient     // 5 attempts, 30s timeout, 200ms delay between retries\n.none        // 1 attempt, no retries\n\n// Custom policy\nRetryPolicy(maxAttempts: 4, timeout: .seconds(15))\n```\n\n### Shared State\n\nSynchronize state between devices. `SharedState` uses `applicationContext` under the hood, which automatically syncs the latest value to the counterpart device.\n\n```swift\n// 📁 Shared/AppSettings.swift\n// ⚠️ The model must be in shared code accessible by both targets\n\nstruct AppSettings: Codable, Sendable, Equatable {\n    var theme: String\n    var notificationsEnabled: Bool\n}\n```\n\n```swift\n// 📁 iOS App/SettingsManager.swift  (or watchOS App/SettingsManager.swift)\n// 💡 Use the same pattern on BOTH iOS and watchOS targets.\n//    Each side creates its own SharedState instance with the same structure.\n//    Updates from either side automatically sync to the other.\n\nimport WatchConnectivitySwift\n\n@MainActor\nclass SettingsManager {\n    let sharedSettings: SharedState\u003cAppSettings\u003e\n\n    init(connection: WatchConnection) {\n        // Both iOS and watchOS create a SharedState with the same initial value.\n        // The library handles syncing updates between devices automatically.\n        sharedSettings = SharedState(\n            initialValue: AppSettings(theme: \"light\", notificationsEnabled: true),\n            connection: connection  // Pass the WatchConnection instance\n        )\n    }\n\n    func updateTheme(_ theme: String) throws {\n        // Update locally - the change is automatically pushed to the other device\n        var settings = sharedSettings.value\n        settings.theme = theme\n        try sharedSettings.update(settings)\n    }\n}\n\n### File Transfers\n\nTransfer files between devices with progress tracking. Files are transferred in the background and continue even when your app is suspended.\n\n```swift\n// 📁 iOS App/FileTransferManager.swift  (or watchOS App/FileTransferManager.swift)\n// 💡 File transfers work in both directions between iOS and watchOS\n\nimport WatchConnectivitySwift\n\n@MainActor\nclass FileTransferManager {\n    private let connection = WatchConnection()\n\n    // MARK: - Sending Files\n\n    func sendFile(_ fileURL: URL, metadata: [String: Any]? = nil) async throws {\n        // Start the transfer and get a FileTransfer object for tracking\n        let transfer = try await connection.transferFile(fileURL, metadata: metadata)\n\n        // Option 1: Wait for completion\n        try await transfer.waitForCompletion()\n\n        // Option 2: Track progress\n        for await progress in transfer.progressUpdates {\n            print(\"Progress: \\(Int(progress * 100))%\")\n        }\n    }\n\n    // MARK: - Receiving Files\n\n    func setupFileReceiving() {\n        // Option 1: Callback-based\n        connection.onFileReceived = { file in\n            // ⚠️ IMPORTANT: Move the file synchronously!\n            // The file will be deleted after this handler returns.\n            let documents = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]\n            let destination = documents.appendingPathComponent(file.fileURL.lastPathComponent)\n            try? FileManager.default.moveItem(at: file.fileURL, to: destination)\n\n            // Access any metadata sent with the file\n            if let id = file.metadata?[\"id\"] as? String {\n                print(\"Received file with ID: \\(id)\")\n            }\n        }\n\n        // Option 2: Async stream\n        Task {\n            for await file in connection.receivedFiles {\n                // Process each received file\n                let destination = documentsDirectory.appendingPathComponent(file.fileURL.lastPathComponent)\n                try? FileManager.default.moveItem(at: file.fileURL, to: destination)\n            }\n        }\n    }\n}\n```\n\n### Session Health Monitoring\n\n```swift\n// 📁 iOS App/ConnectionMonitor.swift  (or watchOS App/ConnectionMonitor.swift)\n// 💡 Use on EITHER or BOTH targets to monitor connection health.\n//    Each device tracks the health of its connection to the counterpart.\n\nimport WatchConnectivitySwift\n\n@MainActor\nclass ConnectionMonitor: ObservableObject {\n    @Published var health: SessionHealth = .healthy\n\n    private let connection = WatchConnection()\n\n    func startMonitoring() {\n        // Observe health changes via the diagnostic event stream\n        Task {\n            for await event in connection.diagnosticEvents {\n                if case .healthChanged(_, let newHealth) = event {\n                    health = newHealth\n                }\n            }\n        }\n    }\n\n    func attemptRecovery() async {\n        // Attempt recovery if the session is unhealthy\n        if !connection.sessionHealth.isHealthy {\n            await connection.attemptRecovery()\n        }\n    }\n\n    func showRecoverySuggestion() -\u003e String? {\n        // Get localized recovery suggestion if unhealthy\n        return connection.sessionHealth.suggestion?.localizedDescription\n    }\n}\n```\n\n---\n\n## SwiftUI Integration\n\n```swift\n// 📁 watchOS App/ContentView.swift\n// 💡 This example shows a watchOS view that requests data from the iPhone.\n//    The RecipeViewModel (defined in the Quick Start section) sends the request.\n\nimport SwiftUI\n\nstruct ContentView: View {\n    // The view model handles communication with the iPhone\n    @StateObject private var viewModel = RecipeViewModel()\n\n    var body: some View {\n        VStack {\n            if let recipe = viewModel.recipe {\n                Text(recipe.title)\n                    .font(.headline)\n\n                ForEach(recipe.ingredients, id: \\.self) { ingredient in\n                    Text(\"• \\(ingredient)\")\n                }\n            }\n\n            Button(\"Load Recipe\") {\n                Task {\n                    // This triggers a request to the iPhone\n                    await viewModel.loadRecipe(id: \"carbonara\")\n                }\n            }\n        }\n    }\n}\n```\n\nYou can also observe connection state and shared state changes directly in SwiftUI:\n\n```swift\n// 📁 iOS App/SettingsView.swift  (or watchOS App/SettingsView.swift)\n// 💡 SharedState works on BOTH targets - changes sync automatically\n\nimport SwiftUI\nimport WatchConnectivitySwift\n\nstruct SettingsView: View {\n    @ObservedObject var settingsManager: SettingsManager\n\n    var body: some View {\n        Toggle(\n            \"Dark Mode\",\n            isOn: Binding(\n                get: { settingsManager.sharedSettings.value.theme == \"dark\" },\n                set: { isDark in\n                    try? settingsManager.updateTheme(isDark ? \"dark\" : \"light\")\n                }\n            )\n        )\n    }\n}\n```\n\n---\n\n## Documentation\n\nComprehensive documentation is available in the [Docs](Docs/) folder:\n\n- **[Getting Started](Docs/GettingStarted.md)** - Installation and basic setup\n- **[Architecture Overview](Docs/Architecture.md)** - Design and internals\n- **[API Reference](Docs/API-Reference.md)** - Complete API documentation\n- **[Reliability Features](Docs/Reliability.md)** - Retries, health monitoring, queuing\n- **[Shared State](Docs/SharedState.md)** - State synchronization between devices\n- **[Migration Guide](Docs/Migration-Guide.md)** - Upgrading from v1.x\n\n---\n\n## Migration from v1.x\n\nVersion 5.0.0 is a complete rewrite with a new API. See the [Migration Guide](Docs/Migration-Guide.md) for detailed instructions.\n\nKey changes:\n\n| v1.x | v5.0.0 |\n|------|--------|\n| `WatchConnectivityService` | `WatchConnection` |\n| `WatchConnectivityRPCRepository` | Protocol-based `WatchRequest` |\n| Completion handlers | `async/await` |\n| Dictionary-based messages | Type-safe Codable requests |\n| Manual retry logic | Built-in `RetryPolicy` |\n| Manual state sync | `SharedState` for automatic sync |\n\n---\n\n## Testing\n\nThe library is designed for testability. You can inject a mock session to test your iOS or watchOS code without a real device connection:\n\n```swift\n// 📁 Tests/MyAppTests.swift\n// 💡 Use MockWCSession in your unit tests to simulate Watch/iPhone communication\n\nimport WatchConnectivitySwift\n\n// Create a mock session instead of a real WCSession\nlet mockSession = MockWCSession()\nlet connection = WatchConnection(session: mockSession)\n\n// Configure the mock to return a specific response\n// This simulates what the counterpart device would send back\nmockSession.sendMessageResponse = try JSONEncoder().encode(\n    WireResponse(outcome: .success(responseData), requestID: UUID(), timestamp: Date())\n)\n\n// Test your code - the mock handles the \"network\" layer\nlet response = try await connection.send(MyRequest())\n```\n\n### E2E Tests\n\nThe Demo app includes an end-to-end test harness for verifying real iOS-watchOS communication via paired simulators.\n\n```bash\n# Run E2E tests (creates/uses paired simulators)\n./Scripts/e2e-test.sh\n\n# Keep simulators running after tests for debugging\n./Scripts/e2e-test.sh --keep-running\n\n# Skip simulator erase (faster but may be flaky)\n./Scripts/e2e-test.sh --no-clean\n```\n\n**Warning: E2E tests are inherently unstable.** WatchConnectivity in simulators has known limitations:\n- `isReachable` may never report `true` even when communication works\n- Timing is unpredictable; tests may pass or fail intermittently\n- First launch after simulator erase requires extra warm-up time\n- **After simulator erase, WCSession may report `appInstalled: NO`** for the companion app even after re-pairing and re-installing. The script retries automatically with increasing waits, but persistent failures may require rebooting the watch simulator (`xcrun simctl shutdown \u003cUDID\u003e \u0026\u0026 xcrun simctl boot \u003cUDID\u003e`) or using `--no-clean`\n- Some failures require rebooting the watch simulator to resolve\n\nThese tests are intended for manual verification and debugging, not CI automation. The unit tests with `MockWCSession` provide reliable, deterministic testing.\n\n---\n\n## License\n\nMIT\n\n---\n\n## Authors\n\nCreated by [Toni Sucic](https://github.com/ts95).\n\nCo-authored by [Claude Opus 4.5](https://www.anthropic.com/claude) with [Claude Code](https://www.anthropic.com/claude-code).\n\nContributions and feedback welcome!\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fts95%2Fwatchconnectivityswift","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fts95%2Fwatchconnectivityswift","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fts95%2Fwatchconnectivityswift/lists"}