WatchConnectivitySwift includes comprehensive reliability features to handle the inherent unreliability of WCSession connections. This guide explains how to use them effectively.
Watch connectivity is inherently unreliable due to:
- Bluetooth connection drops
- Device sleep states
- App backgrounding
- System resource constraints
- "Phantom reachability" (isReachable is true but messages fail)
The library addresses these issues through:
- Waiting Utilities - Wait for activation or connection
- Retry Policies - Automatic retry with fixed delay
- Delivery Strategies - Fallback transport mechanisms
- Health Monitoring - Detect and recover from persistent issues
- Recovery Suggestions - User-facing guidance for connectivity issues
- Request Queuing - Queue requests during offline periods
Use the @Published properties on WatchConnection to observe connection state:
struct ConnectionStatusView: View {
@ObservedObject var connection: WatchConnection
var body: some View {
HStack {
Circle()
.fill(connection.isReachable ? Color.green : Color.red)
.frame(width: 10, height: 10)
Text(connection.isReachable ? "Connected" : "Disconnected")
}
}
}Wait for the session to be ready:
// Wait for activation only
try await connection.waitForActivation(timeout: .seconds(10))
// Wait for full connectivity (activation + reachability)
try await connection.waitForConnection(timeout: .seconds(30))
// Wait indefinitely for connection
try await connection.waitForConnection()For requests that should wait for connectivity and retry on failure:
let response = try await connection.sendWhenReady(
MyRequest(),
maxAttempts: 5,
connectionTimeout: .seconds(60)
)This method:
- Waits for the connection to become available
- Sends the request
- Retries if the connection is lost during transmission
// Default policy (recommended for most cases)
// - 3 attempts
// - 200ms fixed delay between retries
// - 10 second total timeout
let response = try await connection.send(request)
// Patient policy (for important requests)
// - 5 attempts
// - 200ms fixed delay between retries
// - 30 second timeout
let response = try await connection.send(request, retryPolicy: .patient)
// No retries (fail immediately on error)
let response = try await connection.send(request, retryPolicy: .none)// Custom retry count and timeout
let policy = RetryPolicy(maxAttempts: 4, timeout: .seconds(15))
let response = try await connection.send(request, retryPolicy: policy)Time: 0ms 200ms 400ms 600ms
| | | |
v v v v
[Try 1] [Try 2] [Try 3] [Try 4]
+200 +200 +200
(fixed) (fixed) (fixed)
Retries use a fixed 200ms delay between attempts. The library automatically retries on transient errors like .notReachable, .deliveryFailed, and .replyFailed.
| Strategy | Primary | Fallback | Use Case |
|---|---|---|---|
messageWithUserInfoFallback |
Message | UserInfo | Default, guaranteed delivery |
messageWithContextFallback |
Message | Context | Latest-value-only state |
messageOnly |
Message | None | Real-time, time-sensitive |
userInfoOnly |
UserInfo | None | Guaranteed ordered delivery |
contextOnly |
Context | None | State synchronization |
| Transport | Requires Active Apps | Ordering | Delivery |
|---|---|---|---|
sendMessageData |
Yes | N/A | Immediate or fail |
transferUserInfo |
No | FIFO | Queued, guaranteed |
applicationContext |
No | Last wins | Latest value only |
// Real-time feature (game, live tracking)
// Fail fast if not reachable
let response = try await connection.send(
request,
strategy: .messageOnly,
delivery: .immediate
)
// Important data that must be delivered
// Try real-time, fall back to queue
let response = try await connection.send(
request,
strategy: .messageWithUserInfoFallback
)
// Settings/preferences sync
// Only latest value matters
let response = try await connection.send(
request,
strategy: .messageWithContextFallback
)
// Log events that must arrive in order
let response = try await connection.send(
request,
strategy: .userInfoOnly
)enum SessionHealth {
case healthy
// Session is working normally
case unhealthy
// Persistent failures, likely needs user intervention
}healthy ──(failures)──> unhealthy
▲ │
│ │
(success) (success)
│ │
└─────────────────────────┘
// Check current state
if connection.sessionHealth.isHealthy {
// Normal operation
} else {
// Prompt user for action
showRecoveryDialog()
}
// Observe health changes
Task {
for await event in connection.diagnosticEvents {
if case .healthChanged(let from, let to) = event {
handleHealthChange(from: from, to: to)
}
}
}// Attempt automatic recovery
if !connection.sessionHealth.isHealthy {
await connection.attemptRecovery()
}
// If still unhealthy, guide user with the recovery suggestion
if case .unhealthy(let suggestion) = connection.sessionHealth {
showAlert(suggestion.localizedDescription)
}When the session becomes unhealthy, it includes a RecoverySuggestion that can be displayed to users:
// Access suggestion when unhealthy
if case .unhealthy(let suggestion) = connection.sessionHealth {
showAlert(suggestion.localizedDescription)
}
// Or observe health changes via diagnostics
Task {
for await event in connection.diagnosticEvents {
if case .healthChanged(_, let to) = event,
case .unhealthy(let suggestion) = to {
showAlert(suggestion.localizedDescription)
}
}
}| Suggestion | Description |
|---|---|
openCompanionApp |
Ask user to open the app on the counterpart device |
restartWatch |
Ask user to restart their Apple Watch |
Suggestions use Swift's String Catalogs for localization.
The library emits detailed events for debugging:
Task {
for await event in connection.diagnosticEvents {
switch event {
// Session lifecycle
case .sessionActivated:
print("Session activated")
case .sessionDeactivated:
print("Session deactivated")
// Connectivity
case .reachabilityChanged(let isReachable):
print("Reachability: \(isReachable)")
// Message events
case .messageSent(let type, let duration):
print("Sent \(type) in \(duration)")
case .deliveryFailed(let type, let error, let willRetry):
print("Failed: \(type), retry: \(willRetry)")
case .fallbackUsed(let from, let to):
print("Fallback: \(from) → \(to)")
// Queue events
case .requestQueued(let type, let size):
print("Queued: \(type), queue size: \(size)")
case .queueFlushed(let count):
print("Flushed \(count) queued requests")
// Health events
case .healthChanged(let from, let to):
print("Health: \(from) → \(to)")
case .recoveryAttempted:
print("Recovery attempted")
case .recoverySucceeded:
print("Recovery succeeded")
case .recoveryFailed(let error):
print("Recovery failed: \(error)")
default:
break
}
}
}When the counterpart is not reachable, requests can be queued:
// Check pending requests
let pendingCount = connection.pendingRequestCount
// Cancel all queued requests
connection.cancelAllQueuedRequests()
// Cancel specific request
connection.cancelQueuedRequest(id: requestID)- Automatic queueing: Requests are queued when offline (depending on delivery mode)
- Automatic flush: Queued requests are sent when connectivity is restored
- In-memory: Queue is held in memory (not persisted across app termination)
// Real-time features: no retries
let response = try await connection.send(
request,
delivery: .immediate,
retryPolicy: .none
)
// Background sync: patient policy
let response = try await connection.send(
request,
delivery: .queued,
retryPolicy: .patient
)do {
let response = try await connection.send(request)
} catch WatchConnectionError.timeout {
// Specific handling for timeout
showRetryOption()
} catch WatchConnectionError.notReachable {
// Queue for later or show offline message
showOfflineMessage()
} catch {
// Generic error handling
showErrorAlert(error)
}struct ConnectionStatusView: View {
@ObservedObject var connection: WatchConnection
var body: some View {
if case .unhealthy(let suggestion) = connection.sessionHealth {
ErrorBanner(suggestion.localizedDescription) {
Task { await connection.attemptRecovery() }
}
}
}
}// Analytics don't need responses
struct LogEvent: FireAndForgetRequest {
let name: String
let parameters: [String: String]
}
// Send without waiting
await connection.send(LogEvent(name: "screen_view", parameters: [:]))// Simulate unreliable connection
let flakySession = FlakyWCSession()
flakySession.failureProbability = 0.3 // 30% failure rate
let connection = WatchConnection(session: flakySession)
// Test that your app handles failures gracefully