I am attempting to write a utility function so that I can know if an iOS device is paired with an Apple Watch with something as simple as:
func isPaired(
timeoutAfter maxDuration: TimeInterval
) async throws -> Bool {
I came along with this code that works fine… except for the timeout part. If I comment out the code in the delegate, I can never catch the time out error that is thrown by the code. How come?
import Combine
import os
import SwiftUI
import WatchConnectivity
let logger = Logger(subsystem: "net.mickf.BlocksApp", category: "Watch")
class WatchPairingTask: NSObject {
struct TimedOutError: Error, Equatable {}
static var shared = WatchPairingTask()
override private init() {
logger.debug("Creating an instance of WatchPairingTask.")
super.init()
}
private var activationContinuation: CheckedContinuation<Bool, Never>?
func isPaired(
timeoutAfter maxDuration: TimeInterval
) async throws -> Bool {
logger.debug("Just called isPaired")
return try await withThrowingTaskGroup(of: Bool.self) { group in
group.addTask {
try await self.doWork()
}
group.addTask {
try await Task.sleep(nanoseconds: UInt64(maxDuration * 1_000_000_000))
try Task.checkCancellation()
// We’ve reached the timeout.
logger.error("Throwing timeout")
throw TimedOutError()
}
// First finished child task wins, cancel the other task.
let result = try await group.next()!
group.cancelAll()
return result
}
}
private func doWork() async throws -> Bool {
let session = WCSession.default
session.delegate = self
return await withCheckedContinuation { continuation in
activationContinuation = continuation
session.activate()
}
}
}
extension WatchPairingTask: WCSessionDelegate {
func session(_ session: WCSession, activationDidCompleteWith _: WCSessionActivationState, error: Error?) {
logger.debug("Session activation did complete")
if let error = error {
logger.error("Session activation did complete with error: \(error.localizedDescription, privacy: .public)")
}
// Remove the comment to make things work
// activationContinuation?.resume(with: .success(session.isPaired))
}
func sessionDidBecomeInactive(_: WCSession) {
// Do nothing
}
func sessionDidDeactivate(_: WCSession) {
// Do nothing
}
}
class WatchState: ObservableObject {
@Published var isPaired: Bool?
func start() {
Task {
do {
let value = try await WatchPairingTask.shared.isPaired(timeoutAfter: 1)
await MainActor.run {
isPaired = value
}
} catch {
print("WatchState caught an error.")
}
}
}
}
struct ContentView: View {
@ObservedObject var model: WatchState
init(model: WatchState) {
self.model = model
}
var body: some View {
VStack {
Text("Is a watch paired with this iPhone?")
Text(model.isPaired?.description ?? "…")
}
.padding()
.onAppear {
model.start()
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView(model: WatchState())
}
}
The console:
[Watch] Creating an instance of WatchPairingTask.
[Watch] Just called isPaired
[Watch] Session activation did complete
[WC] -[WCSession onqueue_handleUpdateSessionState:]_block_invoke dropping as pairingIDs no longer match. pairingID (null), client pairingID: (null)
[WC] WCSession is not paired
[Watch] Throwing timeout
And that's it.
Some notes:
- The task with timeout code is inspired by Running an async task with a timeout;
- The code is available here: https://github.com/dirtyhenry/blocks/blob/8ae2f9b90dc3ec26e1cbf16c3b61f13efe5b3c78/Examples/BlocksApp/BlocksApp/ContentView.swift