Skip to content

Add async API variants of detect() #2

@kennethphough

Description

@kennethphough

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

  1. 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)
  2. 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.

  3. 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

  • async variants available for all four input types
  • Compiles cleanly under Swift 6 strict concurrency
  • Documented Sendable / thread-safety story in the Detector doc comments
  • Test: concurrent TaskGroup with N tasks each detecting a different fixture image returns correct results

Notes

The synchronous API works fine from concurrency contexts via Task.detached { ... } in user code; async variants are a convenience, not a missing capability.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions