dockkit について
DockKit は、Apple の DockKit フレームワークを使い、対応する電動スタンドやジンバルを iPhone のカメラアプリと連携する方法を解説するスキルです。システム追跡や独自検出、フレーミング、モーター制御、アクセサリーイベントを扱います。
主な機能
- 01アクセサリー接続の検出と監視
- 02システムによる被写体追跡
- 03独自の検出結果の送信
- 04フレーミングモードと関心領域の設定
- 05モーター動作とアニメーションの制御
- 06追跡・バッテリー・ボタンイベントの取得
活用シーン
- 01人物を追うカメラアプリの開発
- 02独自 ML モデルによる被写体追跡
- 03動画レイアウトに合わせた構図の制限
- 04ドックのボタン処理と電池状態の監視
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ dpearson2699/ swift-ios-skills --skill dockkitインストール後、Claude Code で/dockkitを入力して読み込まれたか確認します。
同梱ファイルevals/evals.jsonreferences/dockkit-patterns.md
詳細
DockKit
iPhone を回転させて被写体を物理的に追尾する、モーター駆動カメラスタンドおよびジンバルと連携するためのフレームワークです。DockKit がモーター制御、被写体検出、フレーミングを処理するため、カメラアプリでは追加のコードなしで、パン 360 度、チルト 90 度の追尾が可能です。アプリでは、システムの追尾をオーバーライドして独自の観測結果を提供したり、モーターを直接制御したり、フレーミングを調整したりできます。iOS 17 以降、Swift 6.3。
目次
- セットアップ
- アクセサリの検出
- システム追尾
- カスタム追尾
- フレーミングと関心領域
- モーター制御
- アニメーション
- 追尾状態と被写体の選択
- アクセサリのイベント
- バッテリーのモニタリング
- よくある間違い
- レビュー用チェックリスト
- 参考資料
セットアップ
DockKit をインポートします。
import DockKit
DockKit を使用するには、DockKit 対応の物理アクセサリと実機が必要です。 シミュレータはドックのハードウェアに接続できません。
DockKit 自体に特別なエンタイトルメントや DockKit 固有の
Info.plist キーは必要ありません。デバイスのカメラを使用するカメラアプリでは、
NSCameraUsageDescription など、通常のカメラプライバシー処理が引き続き必要です。このフレームワークは
DockKit システムデーモンを介して、ペアリング済みアクセサリと自動的に通信します。
アプリでは AVFoundation のカメラ API を使用する必要があります。DockKit は カメラパイプラインに接続し、システム追尾のためにフレームを解析します。
アクセサリの検出
DockAccessoryManager.shared を使用してドックの接続を監視します。
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 は、state、
accessory、trackingButtonEnabled を含む DockAccessory.StateChange 値を発行します。名前、
カテゴリ、UUID の取得には accessory.identifier を使用します。ハードウェアの詳細は firmwareVersion
と hardwareModel から取得できます。
システム追尾
システム追尾は DockKit のデフォルトモードです。有効にすると、システムは 組み込みの ML 推論を通じてカメラフレームを解析し、顔や 体を検出して、被写体がフレーム内に収まるようにモーターを動かします。AVFoundation の カメラ API を使用するアプリであれば、自動的にこの機能を利用できます。
有効化または無効化
// Enable system tracking (default)
try await DockAccessoryManager.shared.setSystemTrackingEnabled(true)
// Disable system tracking for custom control
try await DockAccessoryManager.shared.setSystemTrackingEnabled(false)
システム追尾の状態は、アプリの終了、再起動、または バックグラウンドとフォアグラウンドの切り替えをまたいで保持されません。アプリで特定の値が 必要なときは、明示的に設定してください。
タップして被写体を選択
タップして特定の被写体を選択できるようにします。
// 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([])
カスタム追尾
カスタム ML モデルや Vision フレームワークを使用する場合は、システム追尾を 無効にし、独自の観測結果を提供します。
観測結果の提供
推論結果から DockAccessory.Observation 値を作成し、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)
}
観測タイプ
カスタム追尾をレビューする際は、サポートされている ObservationType のケースである
.humanFace、.humanBody、.object の中から明示的に選択してください。
体や物体の検出が可能な場合に、.humanFace だけを回答しないでください。
rect は左下原点の正規化座標を使用します(Vision フレームワークと同じ
座標系のため、変換は不要です)。
カメラ情報
DockAccessory.CameraInformation はアクティブなカメラを表します。プレースホルダーのデバイス情報、
内部パラメーター、フレームサイズの値をハードコードしないでください。座標がすでに左下隅を基準としている場合は、
向きを .corrected に設定します。レビューの回答では、不透明なオプショナルの cameraInfo プレースホルダーを認めず、
アクティブな AVCaptureDevice と現在の CMSampleBuffer から構築する方法を示してください。
追尾の各バリアントでは、観測結果の代わりに [AVMetadataObject] も使用できます。
DockKit で観測結果またはメタデータをキャプチャした画像バッファと組み合わせる場合は、
image: CVPixelBuffer オーバーロードを使用します。これらのオーバーロードでは image 引数が必須です。
フレーミングと関心領域
フレーミングモード
追跡対象のフレーミング方法を制御します。
try await accessory.setFramingMode(.automatic) // documented default
try await accessory.setFramingMode(.center) // explicit opt-in
| モード | 動作 |
|---|---|
.automatic | ドキュメントに記載されたデフォルト。システムが最適なフレーミングを判断します |
.center | 対象を中央に保つ明示的なオプトインモード |
.left | 対象を左側の3分の1にフレーミングします |
.right | 対象を右側の3分の1にフレーミングします |
システムのデフォルトの動作では、主要な対象が中央に配置されることがよくありますが、.center がデフォルトに近いモードになることはありません。デフォルトは .automatic です。フレームの一部をグラフィックオーバーレイが占める場合は、.left または .right を使用します。
関心領域
ビデオフレーム内の特定の領域に追跡範囲を限定します。
// 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)
標準的ではないアスペクト比(会議用の正方形のビデオなど)にクロップする場合は、対象が表示領域内に収まるよう、関心領域を使用します。
モーター制御
モーターを直接制御する前に、システムによる追跡を無効にします。
角速度
連続回転速度をラジアン毎秒で設定します。
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())
軸:
x-- ピッチ(チルト)。iOS では正の値で下向きに傾きます。y-- ヨー(パン)。正の値で右にパンします。z-- ロール(ハードウェアが対応している場合)。
姿勢の設定
指定した時間をかけて特定の位置に移動します。
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
)
クォータニオンベースの姿勢には Rotation3D も指定できます。現在の位置からの相対移動には relative: true を設定します。返される Progress オブジェクトで完了状況を追跡できます。
モーション状態
アクセサリの現在位置と速度を監視します。
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
}
}
制限の設定
軸ごとの可動範囲と最高速度を制限します。
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)
アニメーション
ドックを表現豊かに動かす、組み込みのキャラクターアニメーションです。
// 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)
| アニメーション | 効果 |
|---|---|
.yes | うなずく動き |
.no | 首を横に振る動き |
.wakeup | 起動時のような動き |
.kapow | ドラマチックに振り子のように揺れる動き |
アニメーションはアクセサリの現在位置から始まり、非同期で実行されます。完了後は必ず追跡状態を復元してください。
animate(motion:) と setOrientation(_:duration:relative:) の呼び出しは、1秒あたり2回以下にしてください。呼び出し頻度がこれを超えると、.frameRateTooHigh がスローされることがあります。
追跡状態と対象の選択
iOS 18 以降では、エラーをスローする trackingStates 非同期シーケンスを通じて、ML から導出された追跡シグナルを取得できます。各状態には time と trackedSubjects(.person または .object)があります。人物には identifier、rect、speakingConfidence、lookingAtCameraConfidence、saliencyRank が含まれます(ランクが低いほど顕著です)。
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]) }
}
}
selectSubjects(_:) を使用して UUID で追跡対象を固定します。自動選択に戻すには [] を渡します。話者には speakingConfidence、エンゲージメントには lookingAtCameraConfidence、オーバーレイには rect を使用し、フォールバックには値の小さい saliencyRank を使用します。
レビューへの回答では、lookingAtCameraConfidence と rect を説明文だけでなく、コード内で使用してください。
アクセサリのイベント
ドックの物理ボタンを押すと、エラーをスローする accessoryEvents 非同期シーケンスを通じてイベントが発生します(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
}
}
}
他社製アプリはこれらのイベントを受け取り、AVFoundation を通じて動作を実装します。
バッテリーの監視
エラーをスローする batteryStates 非同期シーケンスを通じて、ドックのバッテリー状態を監視します(iOS 18 以降)。ドックは複数のバッテリーを報告することがあり、それぞれを 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)
}
}
よくある間違い
やってはいけないこと:システムによる追跡を無効にせずにモーターを制御する
// 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)
やってはいけないこと:ライフサイクルイベントをまたいで追跡状態が維持されると想定する
// 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)
}
}
やってはいけないこと:推奨レートを超えて track() を呼び出す
// 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 calls
やってはいけないこと:姿勢やアニメーションの呼び出しを連続して行う
animate(motion:) または setOrientation(_:duration:relative:) を1秒あたり2回より多く呼び出すと、DockKit は .frameRateTooHigh をスローすることがあります。軌道を設定して Progress を監視し、短い間隔でコマンドを繰り返すのは避けてください。
やってはいけないこと:アニメーション後に追跡を復元し忘れる
// 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)
やってはいけないこと:シミュレータで DockKit を使用する
DockKit には、DockKit 対応の物理アクセサリが必要です。初期化時に利用可能かどうかを確認し、アクセサリがない場合のフォールバック動作を用意してください。
レビューチェックリスト
- 必要な箇所に
import DockKitが記述されている - ドックへの接続・取り外しイベントを検出するため、
accessoryStateChangesを購読している -
.dockedと.undockedの両方の状態を処理している - カスタムトラッキングやモーター制御を行う前に、システムトラッキングを無効にしている
- アニメーション完了後に、システムトラッキングを復元している
- カスタム観測データが 10~30 fps で提供されている
-
animateコマンドとsetOrientationコマンドの呼び出しを、毎秒 2 回までに制限している - 観測データの
rectで、正規化された座標(左下原点)を使用している - カメラ情報を、アクティブな
AVCaptureDeviceと現在のサンプルバッファーからインラインで構築している - 観測タイプの選択で
.humanFace、.humanBody、.objectを指定している - DockKit の列挙型を扱うすべての switch 文で
@unknown defaultを処理している - アクセサリの可動範囲を制限する場合、モーション制限を設定している
- アプリがフォアグラウンドに戻った後、トラッキング状態を再適用している
-
accessoryEventsを#available(iOS 17.4, *)でガードしている -
trackingStatesとbatteryStatesを#available(iOS 18.0, *)でガードしている - バッテリー UI で、複数バッテリー搭載ドックに対応するため
BatteryState.nameを保持している - Simulator ビルドで DockKit のコードパスを実行していない