Use case
Allow callers to use SwiftAprilTag from Swift Concurrency contexts (async/await, TaskGroup) without having to wrap the synchronous methods in Task.detached { ... } themselves. Cleaner integration with SwiftUI lifecycle, AsyncSequence-based capture pipelines, and structured concurrency.
Proposed API
extension Detector {
public func detect(luminance: Data, width: Int, height: Int, stride: Int? = nil) async throws -> [Detection]
#if canImport(CoreGraphics)
public func detect(cgImage: CGImage) async throws -> [Detection]
#endif
#if canImport(UIKit)
public func detect(uiImage: UIImage) async throws -> [Detection]
#endif
#if canImport(CoreVideo)
public func detect(pixelBuffer: CVPixelBuffer, plane: Int = 0) async throws -> [Detection]
#endif
}
Each async variant just hops the synchronous call onto a background executor (Task.detached(priority: .userInitiated)) so it doesn't block the caller's actor.
Open design questions
-
Detector is not Sendable. It owns a non-thread-safe apriltag_detector_t*. Three options:
- Keep
Detector as a class, mark all async methods nonisolated, document "use one Detector per concurrent task"
- Convert
Detector to an actor — clean Sendable story but every existing call site needs await (breaking change for v2.0)
- Keep current API for sync, add
AsyncDetector actor as a new type for async use (no breaking change but two parallel types)
-
Swift 6 strict concurrency. Whatever we ship needs to compile cleanly under Swift 6's strict concurrency mode. Worth waiting until Swift 6 ships before finalizing the API to avoid churn.
-
Per-task vs shared detector. AprilTag detector setup costs ~milliseconds; reusing one detector across many frames is desirable. The actor approach naturally serializes calls (good correctness, possible bottleneck under high parallelism). The nonisolated approach lets the user juggle multiple detectors but adds footguns.
Implementation sketch (option 1: nonisolated methods)
extension Detector {
public func detect(luminance: Data, width: Int, height: Int, stride: Int? = nil) async throws -> [Detection] {
try await Task.detached(priority: .userInitiated) {
try self.detect(luminance: luminance, width: width, height: height, stride: stride)
}.value
}
}
The simplest path. Documentation update warns against calling detect(...) on the same Detector from multiple Tasks concurrently.
Acceptance criteria
Notes
The synchronous API works fine from concurrency contexts via Task.detached { ... } in user code; async variants are a convenience, not a missing capability.
Use case
Allow callers to use SwiftAprilTag from Swift Concurrency contexts (
async/await,TaskGroup) without having to wrap the synchronous methods inTask.detached { ... }themselves. Cleaner integration with SwiftUI lifecycle, AsyncSequence-based capture pipelines, and structured concurrency.Proposed API
Each
asyncvariant just hops the synchronous call onto a background executor (Task.detached(priority: .userInitiated)) so it doesn't block the caller's actor.Open design questions
Detector is not Sendable. It owns a non-thread-safe
apriltag_detector_t*. Three options:Detectoras a class, mark allasyncmethodsnonisolated, document "use one Detector per concurrent task"Detectorto anactor— clean Sendable story but every existing call site needsawait(breaking change for v2.0)AsyncDetectoractor as a new type for async use (no breaking change but two parallel types)Swift 6 strict concurrency. Whatever we ship needs to compile cleanly under Swift 6's strict concurrency mode. Worth waiting until Swift 6 ships before finalizing the API to avoid churn.
Per-task vs shared detector. AprilTag detector setup costs ~milliseconds; reusing one detector across many frames is desirable. The
actorapproach naturally serializes calls (good correctness, possible bottleneck under high parallelism). Thenonisolatedapproach lets the user juggle multiple detectors but adds footguns.Implementation sketch (option 1: nonisolated methods)
The simplest path. Documentation update warns against calling
detect(...)on the sameDetectorfrom multiple Tasks concurrently.Acceptance criteria
asyncvariants available for all four input typesDetectordoc commentsTaskGroupwith N tasks each detecting a different fixture image returns correct resultsNotes
The synchronous API works fine from concurrency contexts via
Task.detached { ... }in user code;asyncvariants are a convenience, not a missing capability.