
Dockkit
- 2k installs
- 944 repo stars
- Updated July 15, 2026
- dpearson2699/swift-ios-skills
dockkit is an agent skill for Control motorized camera docks and enable intelligent subject tracking using DockKit. Use when discovering DockKit-compatible accessories, implementing camera subject tracking for
About
Control motorized camera docks and enable intelligent subject tracking using DockKit. Use when discovering DockKit-compatible accessories, implementing camera subject tracking for faces or bodies, controlling dock motors for pan and tilt, configuring framing behavior, setting regions of interest, or building video apps with automatic camera tracking. The dockkit skill documents workflows and patterns from the repository SKILL.md. --- name: dockkit description: "Control motorized camera docks and enable intelligent subject tracking using DockKit. Use when discovering DockKit-compatible accessories, implementing camera subject tracking for faces or bodies, controlling dock motors for pan and tilt, configuring framing behavior, setting regions of interest, or building video apps with automatic camera tracking." --- # DockKit Framework for integrating with motorized camera stands and gimbals that physically track subjects by rotating the iPhone. DockKit handles motor control, subject detection, and framing so camera apps get 360-degree pan and 90-degree tilt tracking with no additional code.
- [Discovering Accessories](#discovering-accessories)
- [System Tracking](#system-tracking)
- [Custom Tracking](#custom-tracking)
- [Framing and Region of Interest](#framing-and-region-of-interest)
- [Motor Control](#motor-control)
Dockkit by the numbers
- 2,039 all-time installs (skills.sh)
- +104 installs in the week ending Jul 29, 2026 (Skillselion tracking)
- Ranked #118 of 1,039 Mobile Development skills by installs in the Skillselion catalog
- Security screen: LOW risk (skills.sh audit)
- Data as of Jul 31, 2026 (Skillselion catalog sync)
dockkit capabilities & compatibility
- Capabilities
- [discovering accessories](#discovering accessori · [system tracking](#system tracking) · [custom tracking](#custom tracking) · [framing and region of interest](#framing and re · [motor control](#motor control)
- Use cases
- documentation
What dockkit says it does
--- name: dockkit description: "Control motorized camera docks and enable intelligent subject tracking using DockKit.
DockKit handles motor control, subject detection, and framing so camera apps get 360-degree pan and 90-degree tilt tracking with no additional code.
Apps can override system tracking to supply custom observations, control motors directly, or adjust framing.
The Simulator cannot connect to dock hardware.
npx skills add https://github.com/dpearson2699/swift-ios-skills --skill dockkitAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 2k |
|---|---|
| repo stars | ★ 944 |
| Security audit | 3 / 3 scanners passed |
| Last updated | July 15, 2026 |
| Repository | dpearson2699/swift-ios-skills ↗ |
What problem does dockkit solve for developers using the documented workflows?
Control motorized camera docks and enable intelligent subject tracking using DockKit. Use when discovering DockKit-compatible accessories, implementing camera subject tracking for faces or bodies, con
Who is it for?
Developers working with dockkit patterns described in the skill documentation.
Skip if: Skip when docs are empty or the task is outside the skill documented scope.
When should I use this skill?
Control motorized camera docks and enable intelligent subject tracking using DockKit. Use when discovering DockKit-compatible accessories, implementing camera subject tracking for faces or bodies, con
What you get
Grounded guidance and workflows from SKILL.md for dockkit.
- DockKit integration architecture
- Event handling implementation plan
By the numbers
- Covers iOS 17.4+ DockKit accessory hardware button events
Files
DockKit
Framework for integrating with motorized camera stands and gimbals that physically track subjects by rotating the iPhone. DockKit handles motor control, subject detection, and framing so camera apps get 360-degree pan and 90-degree tilt tracking with no additional code. Apps can override system tracking to supply custom observations, control motors directly, or adjust framing. iOS 17+, Swift 6.3.
Contents
- Setup
- Discovering Accessories
- System Tracking
- Custom Tracking
- Framing and Region of Interest
- Motor Control
- Animations
- Tracking State and Subject Selection
- Accessory Events
- Battery Monitoring
- Common Mistakes
- Review Checklist
- References
Setup
Import DockKit:
import DockKitDockKit requires a physical DockKit-compatible accessory and a real device. The Simulator cannot connect to dock hardware.
DockKit itself requires no special entitlements or DockKit-specific Info.plist keys. Camera apps that use device cameras still need normal camera privacy handling, including NSCameraUsageDescription. The framework communicates with paired accessories automatically through the DockKit system daemon.
The app must use AVFoundation camera APIs. DockKit hooks into the camera pipeline to analyze frames for system tracking.
Discovering Accessories
Use DockAccessoryManager.shared to observe dock connections:
import DockKit
func observeAccessories() async throws {
for await stateChange in try DockAccessoryManager.shared.accessoryStateChanges {
switch stateChange.state {
case .docked:
guard let accessory = stateChange.accessory else { continue }
// Accessory is connected and ready
configureAccessory(accessory)
case .undocked:
// iPhone removed from dock
handleUndocked()
@unknown default:
break
}
}
}accessoryStateChanges emits DockAccessory.StateChange values with state, accessory, and trackingButtonEnabled. Use accessory.identifier for the name, category, and UUID; hardware details are available via firmwareVersion and hardwareModel.
System Tracking
System tracking is DockKit's default mode. When enabled, the system analyzes camera frames through built-in ML inference, detects faces and bodies, and drives the motors to keep subjects in frame. Any app using AVFoundation camera APIs benefits automatically.
Enable or Disable
// Enable system tracking (default)
try await DockAccessoryManager.shared.setSystemTrackingEnabled(true)
// Disable system tracking for custom control
try await DockAccessoryManager.shared.setSystemTrackingEnabled(false)System tracking state does not persist across app termination, reboots, or background/foreground transitions. Set it explicitly whenever the app needs a specific value.
Tap to Select Subject
Allow users to select a specific subject by tapping:
// Select the subject at a unit point in video-frame coordinates
try await accessory.selectSubject(at: CGPoint(x: 0.5, y: 0.5))
// Select specific subjects by identifier
try await accessory.selectSubjects([subjectUUID])
// Clear selection (return to automatic selection)
try await accessory.selectSubjects([])Custom Tracking
Disable system tracking and provide your own observations when using custom ML models or the Vision framework.
Providing Observations
Construct DockAccessory.Observation values from your inference output and pass them to the accessory at 10-30 fps:
import DockKit
import AVFoundation
func processFrame(
_ sampleBuffer: CMSampleBuffer,
accessory: DockAccessory,
activeDevice: AVCaptureDevice
) async throws {
let cameraInfo = DockAccessory.CameraInformation(
captureDevice: activeDevice.deviceType,
cameraPosition: activeDevice.position,
orientation: .corrected,
cameraIntrinsics: frameIntrinsics(from: sampleBuffer),
referenceDimensions: frameDimensions(from: sampleBuffer)
)
let detection = try await detector.detect(sampleBuffer)
let observationType: DockAccessory.Observation.ObservationType = switch detection.kind {
case .face: .humanFace
case .body: .humanBody
case .object: .object
}
let observation = DockAccessory.Observation(
identifier: detection.id,
type: observationType,
rect: detection.rect, // normalized, lower-left origin
faceYawAngle: detection.faceYawAngle
)
try await accessory.track([observation], cameraInformation: cameraInfo)
}Observation Types
When reviewing custom tracking, explicitly choose among the only supported ObservationType cases: .humanFace, .humanBody, and .object. Do not answer with only .humanFace when body or object detections are possible.
The rect uses normalized coordinates with a lower-left origin (same coordinate system as Vision framework -- no conversion needed).
Camera Information
DockAccessory.CameraInformation describes the active camera; do not hardcode placeholder device, intrinsics, or frame-size values. Set orientation to .corrected when coordinates are already relative to the bottom-left corner. In review answers, reject opaque optional cameraInfo placeholders and show construction from the active AVCaptureDevice plus the current CMSampleBuffer.
Track variants also accept [AVMetadataObject] instead of observations. Use the image: CVPixelBuffer overloads when DockKit should combine observations or metadata with the captured image buffer; the image argument is required in those overloads.
Framing and Region of Interest
Framing Modes
Control how the system frames tracked subjects:
try await accessory.setFramingMode(.automatic) // documented default
try await accessory.setFramingMode(.center) // explicit opt-in| Mode | Behavior |
|---|---|
.automatic | Documented default; system decides optimal framing |
.center | Explicit opt-in mode to keep subject centered |
.left | Frame subject in left third |
.right | Frame subject in right third |
Default system behavior often centers the primary subject, but .center is never the default-like mode; .automatic is. Use .left or .right when graphic overlays occupy part of the frame.
Region of Interest
Constrain tracking to a specific area of the video frame:
// Normalized coordinates, origin at upper-left
let squareRegion = CGRect(x: 0.25, y: 0.0, width: 0.5, height: 1.0)
try await accessory.setRegionOfInterest(squareRegion)Use region of interest when cropping to a non-standard aspect ratio (e.g., square video for conferencing) so subjects stay within the visible area.
Motor Control
Disable system tracking before controlling motors directly.
Angular Velocity
Set continuous rotation speed in radians per second:
import Spatial
// Pan right at 0.2 rad/s, tilt down at 0.1 rad/s
let velocity = Vector3D(x: 0.1, y: 0.2, z: 0.0)
try await accessory.setAngularVelocity(velocity)
// Stop all motion
try await accessory.setAngularVelocity(Vector3D())Axes:
x-- pitch (tilt). Positive tilts down on iOS.y-- yaw (pan). Positive pans right.z-- roll (if supported by hardware).
Set Orientation
Move to a specific position over a duration:
let target = Vector3D(x: 0.0, y: 0.5, z: 0.0) // Yaw 0.5 rad
let progress = try accessory.setOrientation(
target,
duration: .seconds(2),
relative: false
)Also accepts Rotation3D for quaternion-based orientation. Set relative: true to move relative to the current position. The returned Progress object tracks completion.
Motion State
Monitor the accessory's current position and velocity:
for await state in try accessory.motionStates {
let positions = state.angularPositions // Vector3D
let velocities = state.angularVelocities // Vector3D
let time = state.timestamp
if let error = state.error {
// Motor error occurred
}
}Setting Limits
Restrict range of motion and maximum speed per axis:
let yawLimit = try DockAccessory.Limits.Limit(
positionRange: -1.0 ..< 1.0, // radians
maximumSpeed: 0.5 // rad/s
)
let limits = DockAccessory.Limits(yaw: yawLimit, pitch: nil, roll: nil)
try accessory.setLimits(limits)Animations
Built-in character animations that move the dock expressively:
// Disable system tracking before animating
try await DockAccessoryManager.shared.setSystemTrackingEnabled(false)
let progress = try await accessory.animate(motion: .kapow)
// Wait for completion
while !progress.isFinished && !progress.isCancelled {
try await Task.sleep(for: .milliseconds(100))
}
// Restore system tracking
try await DockAccessoryManager.shared.setSystemTrackingEnabled(true)| Animation | Effect |
|---|---|
.yes | Nodding motion |
.no | Shaking motion |
.wakeup | Startup-style motion |
.kapow | Dramatic pendulum swing |
Animations start from the accessory's current position and execute asynchronously. Always restore tracking state after completion. Keep animate(motion:) and setOrientation(_:duration:relative:) calls to no more than twice per second; higher call rates can throw .frameRateTooHigh.
Tracking State and Subject Selection
iOS 18+ exposes ML-derived tracking signals through the throwing trackingStates async sequence. Each state has time and trackedSubjects (.person or .object); persons include identifier, rect, speakingConfidence, lookingAtCameraConfidence, and saliencyRank (lower rank is more salient).
if #available(iOS 18.0, *) {
for await state in try accessory.trackingStates {
var speaker: UUID?
var engaged: UUID?
var salient: (id: UUID, rank: Int)?
for subject in state.trackedSubjects {
switch subject {
case .person(let person):
let id = person.identifier, rect = person.rect
let speaking = person.speakingConfidence
let looking = person.lookingAtCameraConfidence
let rank = person.saliencyRank
updateSubjectOverlay(id: id, rect: rect)
if let speaking, speaking > 0.7 { speaker = id }
if let looking, looking > 0.7 { engaged = id }
if let rank, salient == nil || rank < salient!.rank { salient = (id, rank) }
case .object(let object):
let id = object.identifier, rect = object.rect
let rank = object.saliencyRank
updateSubjectOverlay(id: id, rect: rect)
if let rank, salient == nil || rank < salient!.rank { salient = (id, rank) }
}
}
if let id = speaker ?? engaged ?? salient?.id { try await accessory.selectSubjects([id]) }
}
}Use selectSubjects(_:) to lock tracking by UUID; pass [] to return to automatic selection. Use speakingConfidence for speakers, lookingAtCameraConfidence for engagement, rect for overlays, and lower saliencyRank values as fallback. In review answers, consume lookingAtCameraConfidence and rect in code, not just prose.
Accessory Events
Physical buttons on the dock trigger events through the throwing accessoryEvents async sequence (iOS 17.4+):
if #available(iOS 17.4, *) {
for await event in try accessory.accessoryEvents {
switch event {
case .cameraShutter: break
case .cameraFlip: break
case .cameraZoom(factor: let factor): break
case .button(id: let id, pressed: let pressed): break
@unknown default: break
}
}
}Third-party apps receive these events and implement behavior through AVFoundation.
Battery Monitoring
Monitor the dock's battery status through the throwing batteryStates async sequence (iOS 18+). A dock can report multiple batteries, each identified by name:
if #available(iOS 18.0, *) {
var batteryRows: [String: (Double, DockAccessory.BatteryChargeState, Bool)] = [:]
for await battery in try accessory.batteryStates {
batteryRows[battery.name] = (battery.batteryLevel, battery.chargeState, battery.lowBattery)
}
}Common Mistakes
DON'T: Control motors without disabling system tracking
// WRONG -- system tracking fights manual commands
try await accessory.setAngularVelocity(velocity)
// CORRECT -- disable system tracking first
try await DockAccessoryManager.shared.setSystemTrackingEnabled(false)
try await accessory.setAngularVelocity(velocity)DON'T: Assume tracking state persists across lifecycle events
// WRONG -- state may have reset after backgrounding
func applicationDidBecomeActive() {
// Assume custom tracking is still active
}
// CORRECT -- re-set tracking state on foreground
func applicationDidBecomeActive() {
Task {
try await DockAccessoryManager.shared.setSystemTrackingEnabled(false)
}
}DON'T: Call track() outside the recommended rate
// WRONG -- calling once per second is too slow
try await accessory.track(observations, cameraInformation: cameraInfo)
// (called at 1 fps)
// CORRECT -- call at 10-30 fps
// Hook into AVCaptureVideoDataOutputSampleBufferDelegate for per-frame callsDON'T: Spam orientation or animation calls
DockKit can throw .frameRateTooHigh if animate(motion:) or setOrientation(_:duration:relative:) is called more than twice per second. Set a trajectory, observe its Progress, and avoid tight command loops.
DON'T: Forget to restore tracking after animations
// WRONG -- tracking stays disabled after animation
try await DockAccessoryManager.shared.setSystemTrackingEnabled(false)
let progress = try await accessory.animate(motion: .kapow)
// CORRECT -- restore tracking when animation completes
try await DockAccessoryManager.shared.setSystemTrackingEnabled(false)
let progress = try await accessory.animate(motion: .kapow)
while !progress.isFinished && !progress.isCancelled {
try await Task.sleep(for: .milliseconds(100))
}
try await DockAccessoryManager.shared.setSystemTrackingEnabled(true)DON'T: Use DockKit in Simulator
DockKit requires a physical DockKit-compatible accessory. Guard initialization and provide fallback behavior when no accessory is available.
Review Checklist
- [ ]
import DockKitpresent where needed - [ ] Subscribed to
accessoryStateChangesto detect dock/undock events - [ ] Handled both
.dockedand.undockedstates - [ ] System tracking disabled before custom tracking or motor control
- [ ] System tracking restored after animations complete
- [ ] Custom observations supplied at 10-30 fps
- [ ]
animateandsetOrientationcommands limited to 2 calls per second - [ ] Observation
rectuses normalized coordinates (lower-left origin) - [ ] Camera information is built inline from the active
AVCaptureDeviceand current sample buffer - [ ] Observation type choice names
.humanFace,.humanBody, and.object - [ ]
@unknown defaulthandled in all switch statements over DockKit enums - [ ] Motion limits set if restricting accessory range of motion
- [ ] Tracking state re-applied after app returns to foreground
- [ ]
accessoryEventsguarded with#available(iOS 17.4, *) - [ ]
trackingStatesandbatteryStatesguarded with#available(iOS 18.0, *) - [ ] Battery UI preserves
BatteryState.namefor multi-battery docks - [ ] No DockKit code paths executed in Simulator builds
References
- Extended patterns (Vision integration, service architecture, custom animations): references/dockkit-patterns.md
- DockKit framework
- DockAccessoryManager
- DockAccessory
- Controlling a DockKit accessory using your camera app
- Track custom objects in a frame
- Modify rotation and positioning programmatically
- Integrate with motorized iPhone stands using DockKit -- WWDC23
- What's new in DockKit -- WWDC24
{
"skill_name": "dockkit",
"evals": [
{
"id": 0,
"prompt": "I'm building an iOS camera app that should work with a DockKit stand. Sketch the setup and control flow for system tracking, tap-to-track, framing or region-of-interest controls, manual pan/tilt mode, and hardware button events. Include the privacy/setup caveats that matter.",
"expected_output": "A DockKit camera-app implementation outline that uses DockAccessoryManager, handles dock/undock, keeps DockKit-specific setup distinct from normal camera privacy, uses current framing and ROI guidance, and handles iOS 17.4+ accessory events with throwing async sequences.",
"files": [],
"expectations": [
"States that DockKit itself has no special entitlement or DockKit-specific Info.plist key while camera capture still needs normal camera privacy such as NSCameraUsageDescription.",
"Uses DockAccessoryManager.shared.accessoryStateChanges to obtain a DockAccessory and handle both docked and undocked states.",
"Enables or disables system tracking with setSystemTrackingEnabled(_:) and does not claim the setting persists across app lifecycle events.",
"Represents .automatic as the documented default framing mode and .center as an explicit mode.",
"Uses selectSubject(at:) with normalized unit coordinates and explains that region of interest uses normalized video-frame coordinates.",
"Disables system tracking before manual setAngularVelocity or setOrientation control.",
"Marks accessoryEvents as iOS 17.4+ and iterates it as a throwing async sequence.",
"Does not route the answer into generic Bluetooth pairing, AccessorySetupKit picker, or AVFoundation-only camera setup."
]
},
{
"id": 1,
"prompt": "Review this DockKit custom tracking plan: keep system tracking on, run Vision at 5 fps, create DockAccessory.Observation rectangles in UIKit view coordinates, call track once per second with an optional CVPixelBuffer when convenient, and animate the dock repeatedly in a tight loop until the subject is centered.",
"expected_output": "A correction-focused review that disables system tracking before custom tracking, uses normalized lower-left observation coordinates, sends track() data at 10-30 fps, describes the actual image overloads, and separates track() frame-rate limits from animate/setOrientation call-rate limits.",
"files": [],
"expectations": [
"Explains that custom DockAccessory.track calls require disabling system tracking first.",
"Corrects the 5 fps or 1 fps cadence to the supported 10-30 fps range for track(_:cameraInformation:).",
"Corrects UIKit view coordinates to normalized observation rectangles with DockKit/Vision lower-left origin coordinates.",
"Builds DockAccessory.CameraInformation from the active AVCaptureDevice type, position, orientation, intrinsics, and reference dimensions as available.",
"States that CVPixelBuffer is supplied through separate image: overloads and is required when those overloads are chosen, not an optional parameter on every track call.",
"Warns that animate(motion:) and setOrientation(_:duration:relative:) should not be called more than twice per second.",
"Uses .humanFace, .humanBody, or .object observation types appropriately and avoids inventing unsupported observation cases."
]
},
{
"id": 2,
"prompt": "I want a feature that follows whichever person is speaking, shows the dock battery in my SwiftUI UI, reacts to the dock zoom and shutter buttons, and also pairs a new BLE camera accessory if none is connected. What belongs in DockKit, what availability checks should I use, and what should move to another skill?",
"expected_output": "A boundary-aware DockKit answer that uses iOS 18 trackingStates and batteryStates for intelligent tracking and status, iOS 17.4 accessoryEvents for buttons, and routes BLE accessory onboarding to AccessorySetupKit or Core Bluetooth rather than expanding DockKit scope.",
"files": [],
"expectations": [
"Uses trackingStates on iOS 18+ to read trackedSubjects and selectSubjects(_:) for speaker or saliency-driven selection.",
"Treats trackingStates as a throwing async sequence and uses person fields such as speakingConfidence, lookingAtCameraConfidence, saliencyRank, rect, and identifier accurately.",
"Uses batteryStates on iOS 18+ as a throwing async sequence and reads batteryLevel, chargeState, lowBattery, and name.",
"Uses accessoryEvents on iOS 17.4+ as a throwing async sequence for cameraShutter, cameraFlip, cameraZoom(factor:), and custom button events.",
"Keeps SwiftUI UI state updates on the main actor or an observable model without blocking camera or DockKit work on the main thread.",
"Routes BLE accessory pairing or discovery to AccessorySetupKit/Core Bluetooth rather than presenting DockKit as the pairing framework."
]
}
]
}
DockKit Extended Patterns
Deeper examples for DockKit integration covering service architecture, Vision framework integration, custom animations, multi-camera workflows, and production patterns.
Contents
- Service Architecture
- Vision Framework Integration
- AVCaptureSession Integration
- Multi-Subject Tracking Logic
- Tracking, Events, and Availability Boundaries
- Custom Motor Animations
- SwiftUI Integration
- Camera Control via Accessory Events
- Error Handling
- Testing Patterns
Service Architecture
Isolate DockKit interactions in a dedicated actor to keep motor control and tracking off the main thread:
import DockKit
import AVFoundation
import Spatial
actor DockControlService {
private var accessory: DockAccessory?
private var trackingMode: TrackingMode = .system
enum TrackingMode {
case system
case custom
case manual
}
func start() async throws {
for await stateChange in try DockAccessoryManager.shared.accessoryStateChanges {
switch stateChange.state {
case .docked:
guard let newAccessory = stateChange.accessory else { continue }
accessory = newAccessory
try await configureAccessory(newAccessory)
case .undocked:
accessory = nil
@unknown default:
break
}
}
}
private func configureAccessory(_ accessory: DockAccessory) async throws {
try await DockAccessoryManager.shared.setSystemTrackingEnabled(true)
trackingMode = .system
}
func setTrackingMode(_ mode: TrackingMode) async throws {
trackingMode = mode
let systemEnabled = mode == .system
try await DockAccessoryManager.shared.setSystemTrackingEnabled(systemEnabled)
}
var isConnected: Bool {
accessory != nil
}
}Separating Camera and Dock Concerns
Follow Apple's sample app pattern: define a CaptureService actor for AVFoundation and a DockControlService actor for DockKit. Connect them through a shared model or delegate protocol:
protocol CameraCaptureDelegate: AnyObject, Sendable {
func switchCamera() async
func startOrStopCapture() async
func zoom(factor: Double) async
}
extension DockControlService {
func subscribeToAccessoryEvents(
_ accessory: DockAccessory,
cameraDelegate: CameraCaptureDelegate
) {
guard #available(iOS 17.4, *) else { return }
Task {
do {
for await event in try accessory.accessoryEvents {
switch event {
case .cameraShutter:
await cameraDelegate.startOrStopCapture()
case .cameraFlip:
await cameraDelegate.switchCamera()
case .cameraZoom(factor: let factor):
await cameraDelegate.zoom(factor: factor)
case .button(id: _, pressed: _):
break
@unknown default:
break
}
}
} catch {
// Handle accessory event subscription errors
}
}
}
}Vision Framework Integration
Hand Tracking
Track a hand pose using Vision and feed observations to DockKit:
import Vision
import DockKit
import AVFoundation
final class HandTrackingProcessor: NSObject,
AVCaptureVideoDataOutputSampleBufferDelegate
{
private let accessory: DockAccessory
private let captureDevice: AVCaptureDevice
init(accessory: DockAccessory, captureDevice: AVCaptureDevice) {
self.accessory = accessory
self.captureDevice = captureDevice
}
func captureOutput(
_ output: AVCaptureOutput,
didOutput sampleBuffer: CMSampleBuffer,
from connection: AVCaptureConnection
) {
guard let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else {
return
}
let request = VNDetectHumanHandPoseRequest()
let handler = VNImageRequestHandler(
cvPixelBuffer: pixelBuffer,
options: [:]
)
do {
try handler.perform([request])
guard let result = request.results?.first else { return }
// Use the index finger tip as the tracking point
let thumbTip = try result.recognizedPoint(.thumbTip)
guard thumbTip.confidence > 0.5 else { return }
let rect = CGRect(
x: thumbTip.location.x - 0.05,
y: thumbTip.location.y - 0.05,
width: 0.1,
height: 0.1
)
let observation = DockAccessory.Observation(
identifier: 0,
type: .object,
rect: rect,
faceYawAngle: nil
)
let cameraInfo = DockAccessory.CameraInformation(
captureDevice: captureDevice.deviceType,
cameraPosition: captureDevice.position,
orientation: .corrected,
cameraIntrinsics: nil,
referenceDimensions: nil
)
Task {
try await accessory.track(
[observation],
cameraInformation: cameraInfo
)
}
} catch {
// Handle Vision errors
}
}
}Animal Body Detection
Track pets by detecting animal body poses:
func detectAnimal(
in pixelBuffer: CVPixelBuffer,
accessory: DockAccessory,
device: AVCaptureDevice
) throws {
let request = VNDetectAnimalBodyPoseRequest()
let handler = VNImageRequestHandler(
cvPixelBuffer: pixelBuffer,
options: [:]
)
try handler.perform([request])
guard let result = request.results?.first else { return }
// Use the bounding box from the animal pose
let allPoints = try result.recognizedPoints(.all)
let validPoints = allPoints.values.filter { $0.confidence > 0.3 }
guard !validPoints.isEmpty else { return }
let xs = validPoints.map(\.location.x)
let ys = validPoints.map(\.location.y)
let minX = xs.min()!, maxX = xs.max()!
let minY = ys.min()!, maxY = ys.max()!
let rect = CGRect(
x: minX, y: minY,
width: maxX - minX, height: maxY - minY
)
let observation = DockAccessory.Observation(
identifier: 1,
type: .object,
rect: rect,
faceYawAngle: nil
)
let cameraInfo = DockAccessory.CameraInformation(
captureDevice: device.deviceType,
cameraPosition: device.position,
orientation: .corrected,
cameraIntrinsics: nil,
referenceDimensions: nil
)
Task {
try await accessory.track(
[observation],
cameraInformation: cameraInfo
)
}
}Vision's coordinate system matches DockKit's (normalized, lower-left origin), so bounding boxes pass through without conversion.
AVCaptureSession Integration
Setting Up the Capture Pipeline
import AVFoundation
import DockKit
actor CaptureService {
private let session = AVCaptureSession()
private var currentDevice: AVCaptureDevice?
private var videoOutput: AVCaptureVideoDataOutput?
func configure() throws {
session.beginConfiguration()
defer { session.commitConfiguration() }
session.sessionPreset = .high
guard let camera = AVCaptureDevice.default(
.builtInWideAngleCamera,
for: .video,
position: .front
) else {
throw CaptureError.noCameraAvailable
}
let input = try AVCaptureDeviceInput(device: camera)
guard session.canAddInput(input) else {
throw CaptureError.cannotAddInput
}
session.addInput(input)
currentDevice = camera
let output = AVCaptureVideoDataOutput()
output.alwaysDiscardsLateVideoFrames = true
guard session.canAddOutput(output) else {
throw CaptureError.cannotAddOutput
}
session.addOutput(output)
videoOutput = output
}
func startRunning() {
session.startRunning()
}
func stopRunning() {
session.stopRunning()
}
enum CaptureError: Error {
case noCameraAvailable
case cannotAddInput
case cannotAddOutput
}
}Providing Camera Information from Capture Device
extension CaptureService {
func makeCameraInformation() -> DockAccessory.CameraInformation? {
guard let device = currentDevice else { return nil }
return DockAccessory.CameraInformation(
captureDevice: device.deviceType,
cameraPosition: device.position,
orientation: .corrected,
cameraIntrinsics: nil,
referenceDimensions: nil
)
}
}Multi-Subject Tracking Logic
Prioritizing by Saliency
func trackMostSalient(accessory: DockAccessory) async throws {
guard #available(iOS 18.0, *) else { return }
for await state in try accessory.trackingStates {
// Find the subject with saliency rank 1 (most important)
let primary = state.trackedSubjects.first { subject in
switch subject {
case .person(let person):
return person.saliencyRank == 1
case .object(let object):
return object.saliencyRank == 1
}
}
if let primary {
let id: UUID
switch primary {
case .person(let person): id = person.identifier
case .object(let object): id = object.identifier
}
try await accessory.selectSubjects([id])
}
}
}Tracking Who Looks at Camera
func trackEngagedSubjects(accessory: DockAccessory) async throws {
guard #available(iOS 18.0, *) else { return }
for await state in try accessory.trackingStates {
let engaged = state.trackedSubjects.compactMap { subject -> UUID? in
guard case .person(let person) = subject,
let confidence = person.lookingAtCameraConfidence,
confidence > 0.7 else { return nil }
return person.identifier
}
if !engaged.isEmpty {
try await accessory.selectSubjects(engaged)
}
}
}Converting Tracking Rects to View Coordinates
Tracked subject rectangles are in normalized coordinates. Convert to view space for drawing overlays:
import UIKit
func convertToViewSpace(
normalizedRect: CGRect,
viewSize: CGSize
) -> CGRect {
// DockKit uses lower-left origin; UIKit uses upper-left
let flippedY = 1.0 - normalizedRect.origin.y - normalizedRect.height
return CGRect(
x: normalizedRect.origin.x * viewSize.width,
y: flippedY * viewSize.height,
width: normalizedRect.width * viewSize.width,
height: normalizedRect.height * viewSize.height
)
}Tracking, Events, and Availability Boundaries
Use availability checks around newer DockKit streams:
| API | Availability | Notes |
|---|---|---|
accessoryEvents | iOS 17.4+ | Throwing async sequence; can throw .notConnected or .notSupportedByDevice |
trackingStates | iOS 18+ | Throwing async sequence; emits active tracking summaries |
batteryStates | iOS 18+ | Throwing async sequence; emits accessory battery summaries |
TrackingState.trackedSubjects contains .person(TrackedPerson) and .object(TrackedObject). Person fields are identifier, rect, speakingConfidence, lookingAtCameraConfidence, and saliencyRank. Object fields are identifier, rect, and saliencyRank. Identifiers are random session identifiers and do not persist across tracking sessions.
Accessory event cases are:
| Case | Use |
|---|---|
.cameraShutter | Toggle capture or recording |
.cameraFlip | Switch front/back camera |
.cameraZoom(factor:) | Apply relative zoom intent |
.button(id:pressed:) | Custom accessory button press/release |
Custom Motor Animations
Sweep Animation
Create a horizontal sweep for panoramic capture:
func performSweep(accessory: DockAccessory) async throws {
try await DockAccessoryManager.shared.setSystemTrackingEnabled(false)
// Sweep right
let rightVelocity = Vector3D(x: 0.0, y: 0.2, z: 0.0)
try await accessory.setAngularVelocity(rightVelocity)
try await Task.sleep(for: .seconds(3))
// Sweep left
let leftVelocity = Vector3D(x: 0.0, y: -0.2, z: 0.0)
try await accessory.setAngularVelocity(leftVelocity)
try await Task.sleep(for: .seconds(6))
// Sweep back to center
try await accessory.setAngularVelocity(rightVelocity)
try await Task.sleep(for: .seconds(3))
// Stop
try await accessory.setAngularVelocity(Vector3D())
try await DockAccessoryManager.shared.setSystemTrackingEnabled(true)
}Timed Position Sequence
func lookAround(accessory: DockAccessory) async throws {
try await DockAccessoryManager.shared.setSystemTrackingEnabled(false)
let positions: [(yaw: Double, pitch: Double, duration: Double)] = [
(yaw: -0.5, pitch: 0.0, duration: 1.5),
(yaw: 0.5, pitch: 0.0, duration: 3.0),
(yaw: 0.0, pitch: -0.2, duration: 1.5),
(yaw: 0.0, pitch: 0.0, duration: 1.0),
]
for pos in positions {
let target = Vector3D(x: pos.pitch, y: pos.yaw, z: 0.0)
let progress = try accessory.setOrientation(
target,
duration: .seconds(pos.duration),
relative: false
)
while !progress.isFinished && !progress.isCancelled {
try await Task.sleep(for: .milliseconds(100))
}
}
try await DockAccessoryManager.shared.setSystemTrackingEnabled(true)
}SwiftUI Integration
Dock Status View
import SwiftUI
import DockKit
@Observable
final class DockViewModel {
var isConnected = false
var accessoryName: String?
var batteryName: String?
var batteryLevel: Double?
var isCharging = false
var trackingMode: TrackingMode = .system
enum TrackingMode: String, CaseIterable {
case system = "System"
case custom = "Custom"
case manual = "Manual"
}
private var accessory: DockAccessory?
func startObserving() {
Task {
do {
for await stateChange in try DockAccessoryManager.shared.accessoryStateChanges {
await MainActor.run {
switch stateChange.state {
case .docked:
isConnected = true
accessory = stateChange.accessory
accessoryName = stateChange.accessory?.identifier.name
case .undocked:
isConnected = false
accessory = nil
accessoryName = nil
batteryName = nil
batteryLevel = nil
@unknown default:
break
}
}
if let acc = stateChange.accessory, stateChange.state == .docked {
observeBattery(acc)
}
}
} catch {
// Handle error
}
}
}
private func observeBattery(_ accessory: DockAccessory) {
guard #available(iOS 18.0, *) else { return }
Task {
do {
for await battery in try accessory.batteryStates {
await MainActor.run {
batteryName = battery.name
batteryLevel = battery.batteryLevel
isCharging = battery.chargeState == .charging
}
}
} catch {
// Handle error
}
}
}
func updateTrackingMode(_ mode: TrackingMode) {
trackingMode = mode
Task {
try await DockAccessoryManager.shared.setSystemTrackingEnabled(
mode == .system
)
}
}
}struct DockStatusView: View {
@State private var viewModel = DockViewModel()
var body: some View {
VStack(alignment: .leading) {
if viewModel.isConnected {
Label(
viewModel.accessoryName ?? "DockKit Accessory",
systemImage: "dock.rectangle"
)
.font(.headline)
if let level = viewModel.batteryLevel {
HStack {
Image(systemName: viewModel.isCharging
? "battery.100percent.bolt"
: "battery.75percent")
Text("\(Int(level * 100))%")
}
}
Picker("Tracking", selection: $viewModel.trackingMode) {
ForEach(DockViewModel.TrackingMode.allCases, id: \.self) {
Text($0.rawValue)
}
}
.pickerStyle(.segmented)
.onChange(of: viewModel.trackingMode) { _, newValue in
viewModel.updateTrackingMode(newValue)
}
} else {
Label("No Dock Connected", systemImage: "dock.rectangle")
.foregroundStyle(.secondary)
}
}
.task {
viewModel.startObserving()
}
}
}Manual Control Overlay
struct ManualControlView: View {
let accessory: DockAccessory
let speed: Double = 0.2
var body: some View {
VStack {
Button { move(.tiltUp) } label: {
Image(systemName: "chevron.up")
}
HStack {
Button { move(.panLeft) } label: {
Image(systemName: "chevron.left")
}
Button { stop() } label: {
Image(systemName: "stop.fill")
}
Button { move(.panRight) } label: {
Image(systemName: "chevron.right")
}
}
Button { move(.tiltDown) } label: {
Image(systemName: "chevron.down")
}
}
.font(.title)
}
enum Direction { case tiltUp, tiltDown, panLeft, panRight }
private func move(_ direction: Direction) {
Task {
var velocity = Vector3D()
switch direction {
case .tiltUp: velocity.x = -speed
case .tiltDown: velocity.x = speed
case .panLeft: velocity.y = -speed
case .panRight: velocity.y = speed
}
try await accessory.setAngularVelocity(velocity)
}
}
private func stop() {
Task {
try await accessory.setAngularVelocity(Vector3D())
}
}
}Camera Control via Accessory Events
Implementing Zoom
func handleZoom(factor: Double, device: AVCaptureDevice) {
do {
try device.lockForConfiguration()
let direction = factor > 0 ? 1.0 : -1.0
let scale = 0.2
var newZoom = device.videoZoomFactor + direction * scale
newZoom = max(
min(newZoom, device.maxAvailableVideoZoomFactor),
device.minAvailableVideoZoomFactor
)
device.videoZoomFactor = newZoom
device.unlockForConfiguration()
} catch {
// Handle lock error
}
}Button-Triggered Panorama
func handlePanorama(
accessory: DockAccessory,
buttonID: Int,
pressed: Bool
) async throws {
guard buttonID == 5 else { return }
if pressed {
try await DockAccessoryManager.shared.setSystemTrackingEnabled(false)
let velocity = Vector3D(x: 0.0, y: 0.15, z: 0.0)
try await accessory.setAngularVelocity(velocity)
} else {
try await accessory.setAngularVelocity(Vector3D())
try await DockAccessoryManager.shared.setSystemTrackingEnabled(true)
}
}Error Handling
DockKitError Cases
| Error | Cause | Recovery |
|---|---|---|
.notConnected | No accessory is docked | Wait for .docked state |
.notSupported | Operation not available | Check framework availability |
.notSupportedByDevice | Device lacks DockKit support | Degrade gracefully |
.invalidParameter | Bad input value | Validate before calling |
.cameraTCCMissing | Camera terms or authorization missing | Explain the camera access requirement |
.frameRateTooHigh | track() exceeds 30 fps, or animate / setOrientation exceeds 2 calls per second | Reduce call frequency |
.frameRateTooLow | Observations below 10 fps | Increase call frequency |
.noSubjectFound | No trackable subject detected | Show user guidance |
Guarding API Calls
func safeTrack(
observations: [DockAccessory.Observation],
cameraInfo: DockAccessory.CameraInformation,
accessory: DockAccessory
) async {
do {
try await accessory.track(observations, cameraInformation: cameraInfo)
} catch let error as DockKitError {
switch error {
case .notConnected:
// Accessory disconnected, stop tracking loop
break
case .frameRateTooHigh:
// Throttle observation delivery
break
case .frameRateTooLow:
// Speed up frame processing
break
case .noSubjectFound:
// No subject in observations, continue
break
default:
break
}
} catch {
// Unexpected error
}
}Testing Patterns
Conditional DockKit Integration
DockKit requires physical hardware. Use conditional compilation or runtime checks to keep the app functional without a dock:
#if canImport(DockKit)
import DockKit
#endif
final class DockController {
var isDockKitAvailable: Bool {
#if canImport(DockKit)
return true
#else
return false
#endif
}
func startTracking() async {
#if canImport(DockKit)
do {
for await stateChange in try DockAccessoryManager.shared.accessoryStateChanges {
// Handle state changes
}
} catch {
// DockKit not available on this device
}
#endif
}
}Mock Accessory for UI Development
When building UI without hardware, mock the accessory state:
@Observable
final class MockDockViewModel {
var isConnected = true
var accessoryName: String? = "Mock DockKit Stand"
var batteryLevel: Double? = 0.75
var isCharging = false
var trackingMode = "System"
// Use in SwiftUI previews
func simulateDisconnect() {
isConnected = false
accessoryName = nil
batteryLevel = nil
}
}Related skills
How it compares
Pick dockkit over general iOS camera skills when the app must integrate DockKit stands rather than only AVFoundation capture pipelines.
FAQ
Who is Dockkit for?
Developers and software engineers working with dockkit patterns from the skill documentation.
When should I use Dockkit?
Control motorized camera docks and enable intelligent subject tracking using DockKit. Use when discovering DockKit-compatible accessories, implementing camera subject tracking for faces or bodies, controlling dock motors
Is Dockkit safe to install?
Review the Security Audits panel on this page before installing in production.