Why can't I catch a timeout error in this async function that detects if a device is paired with a watch?

huangapple go评论70阅读模式
英文:

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 -&gt; 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: &quot;net.mickf.BlocksApp&quot;, category: &quot;Watch&quot;)

class WatchPairingTask: NSObject {
    struct TimedOutError: Error, Equatable {}

    static var shared = WatchPairingTask()

    override private init() {
        logger.debug(&quot;Creating an instance of WatchPairingTask.&quot;)
        super.init()
    }

    private var activationContinuation: CheckedContinuation&lt;Bool, Never&gt;?

    func isPaired(
        timeoutAfter maxDuration: TimeInterval
    ) async throws -&gt; Bool {
        logger.debug(&quot;Just called isPaired&quot;)
        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(&quot;Throwing timeout&quot;)
                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 -&gt; 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(&quot;Session activation did complete&quot;)

        if let error = error {
            logger.error(&quot;Session activation did complete with error: \(error.localizedDescription, privacy: .public)&quot;)
        }
        // 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(&quot;WatchState caught an error.&quot;)
            }
        }
    }
}

struct ContentView: View {
    @ObservedObject var model: WatchState

    init(model: WatchState) {
        self.model = model
    }

    var body: some View {
        VStack {
            Text(&quot;Is a watch paired with this iPhone?&quot;)
            Text(model.isPaired?.description ?? &quot;&quot;)
        }
        .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:

答案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 -&gt; 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.

huangapple
  • 本文由 发表于 2023年6月12日 03:07:35
  • 转载请务必保留本文链接:https://go.coder-hub.com/76452106.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定