英文:
Why can't I catch a timeout error in this async function that detects if a device is paired with a watch?
问题
我正在尝试编写一个实用函数,以便我可以知道iOS设备是否与Apple Watch配对,只需像这样简单地使用:
func isPaired(
timeoutAfter maxDuration: TimeInterval
) async throws -> Bool {
我遇到了这段代码,它运行得很好... 除了超时部分。如果我注释掉委托中的代码,就永远无法捕获代码抛出的超时错误。为什么?
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())
}
}
控制台输出:
[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
这就是全部。 😢
一些注意事项:
- 超时代码部分受Running an async task with a timeout的启发。
- 该代码在此处可用:https://github.com/dirtyhenry/blocks/blob/8ae2f9b90dc3ec26e1cbf16c3b61f13efe5b3c78/Examples/BlocksApp/BlocksApp/ContentView.swift
英文:
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
答案1
得分: 0
明白。
我的回答在这里。这与需要手动取消的继续相关。如果没有它,withThrowingTaskGroup
不会返回或抛出异常。
所以,这个doWork
的实现会做:
private func doWork() async throws -> Bool {
let session = WCSession.default
session.delegate = self
return await withTaskCancellationHandler(operation: {
await withCheckedContinuation { continuation in
activationContinuation = continuation
session.activate()
}
}, onCancel: {
activationContinuation?.resume(returning: false)
activationContinuation = nil
})
}
感谢 @Rob 提供详细答案。
英文:
Got it.
My answer was here. It had to do with the cancellation of the continuation that has to be manual. Without it withThrowingTaskGroup
won't return or throw.
So this implementation of doWork
will do:
private func doWork() async throws -> Bool {
let session = WCSession.default
session.delegate = self
return await withTaskCancellationHandler(operation: {
await withCheckedContinuation { continuation in
activationContinuation = continuation
session.activate()
}
}, onCancel: {
activationContinuation?.resume(returning: false)
activationContinuation = nil
})
}
Thanks @Rob for the detailed answer.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论