🎬 Camera Controlがモバイル写真撮影の未来である理由#
想像してみてください:お気に入りのバンドのコンサートにいます。群衆が四方から押し寄せ、両手がふさがり、完璧な写真を撮ろうとしています。以前は、画面を不器用にタップして、スマートフォンを落とすリスクを冒していました。今は、iPhone 16のサイドボタンを1回押すだけ — その瞬間が捉えられます。
これはSFではありません。これはCamera Control — 近年のAppleによるモバイル写真撮影における最も重要なイノベーションの1つです。
この記事で学ぶこと#
このガイドでは、ゼロから完全に機能するCamera Control統合までの全行程を歩みます:
- Camera Controlアーキテクチャの仕組みとその理由
- ゼロからのプロジェクト設定ステップバイステップ
- Lock Screen Camera Extensionの作成(設定に表示されるために必須)
- システムズームと露出コントロールの追加
- シャッターボタンイベントの処理
- UIKitとSwiftUI両方への統合
- 一般的な問題の解決とデバッグ
📱 Camera Controlとは何か、どのように機能するか?#
Camera Controlは単なる「もう一つのボタン」ではありません。ハードウェア、ソフトウェアAPI、直感的なユーザーインターフェースを組み合わせた完全なエコシステムです。
ハードウェア基盤#
iPhone 16以降のモデルでは、サイドボタンに特殊なセンサーが搭載されています:
- Force Touch — 押す圧力を検出
- 静電容量センシング — タッチとスワイプを認識
- 触覚フィードバック — 触感的な応答を提供
この組み合わせにより、ボタンは以下として機能します:
- カメラシャッター(完全な押下)
- 設定スライダー(ボタン上でスワイプ)
- モード切り替え(ダブルプレス)
Camera Controlの3つの主要機能#
- アプリ起動 — Camera Controlはロック画面から直接アプリを開くことができます
- シャッターボタン — 写真撮影またはビデオの開始/停止のための物理的な押下
- パラメータ調整 — ボタン上でスワイプしてズーム、露出、その他のパラメータを調整
🎯 要件と互換性#
コードを書く前に、プロジェクトが要件を満たしていることを確認してください。
システム要件#
| コンポーネント | 最小バージョン |
|---|---|
| iOS | 18.0+ |
| デバイス | iPhone 16, 16 Plus, 16 Pro, 16 Pro Max |
| Xcode | 16.0+ |
| Swift | 5.9+ |
必要なフレームワーク#
import AVFoundation // カメラ作業のメインフレームワーク
import AVKit // AVCaptureEventInteraction用
import Photos // 写真をギャラリーに保存用コードでの互換性チェック#
使用前に常にCamera Controlの利用可能性を確認してください:
func checkCameraControlSupport() -> Bool {
// iOSバージョンをチェック
guard #available(iOS 18.0, *) else {
print("❌ iOS 18.0以上が必要です")
return false
}
// デバイスサポートをチェック
let session = AVCaptureSession()
guard session.supportsControls else {
print("❌ デバイスはCamera Controlをサポートしていません")
return false
}
print("✅ Camera Controlは完全にサポートされています")
return true
}⚠️ 重要:
supportsControlsプロパティはiPhone 16以降でのみtrueを返します。シミュレーターと古いデバイスでは常にfalseです。
🏗️ プロジェクトアーキテクチャ#
推奨ファイル構造#
CameraControlApp/
├── CameraControlApp.xcodeproj
├── CameraControlApp/
│ ├── App/
│ │ ├── CameraControlApp.swift // エントリーポイント (SwiftUI)
│ │ └── AppDelegate.swift // (オプション、UIKit用)
│ ├── Camera/
│ │ ├── CameraController.swift // メインカメラロジック
│ │ ├── CameraPreviewView.swift // UIKit用プレビュー
│ │ └── CameraModel.swift // SwiftUI用ObservableObject
│ ├── Views/
│ │ ├── CameraView.swift // SwiftUIカメラビュー
│ │ └── CameraViewController.swift // UIKitコントローラー
│ └── Extensions/
│ └── AVCaptureSession+Extensions.swift
├── LockScreenCameraExtension/ // Lock Screen Extension
│ ├── LockScreenCameraExtension.swift
│ └── Info.plist
└── Tests/
└── CameraControlTests.swift🔧 ステップ1: CameraControllerの作成#
CameraControllerは、すべてのカメラとCamera Controlロジックを管理するクラスです。
import AVFoundation
import AVKit
import Photos
/// Camera Controlサポート付きメインカメラコントローラー
class CameraController: NSObject, ObservableObject {
// MARK: - パブリックプロパティ
/// 現在のズーム倍率
@Published var currentZoomFactor: CGFloat = 1.0
/// 現在の露出バイアス
@Published var currentExposureBias: Float = 0.0
/// Camera Controlオーバーレイがアクティブかどうか
@Published var isCameraControlActive: Bool = false
/// 最後に撮影した画像(プレビュー用)
@Published var lastCapturedImage: UIImage?
/// カメラ許可ステータス
@Published var permissionStatus: AVAuthorizationStatus = .notDetermined
// MARK: - 内部プロパティ
/// キャプチャセッション — AVFoundationの中央オブジェクト
let session = AVCaptureSession()
/// セッション設定用キュー(メインスレッドでは絶対に実行しない!)
private let sessionQueue = DispatchQueue(
label: "com.yourapp.camera.session",
qos: .userInitiated
)
/// 写真出力
private let photoOutput = AVCapturePhotoOutput()
/// カメラ入力
private var deviceInput: AVCaptureDeviceInput?
/// KVOオブザーバーのストレージ
private var observations: [NSKeyValueObservation] = []
// MARK: - 初期化
override init() {
super.init()
checkPermissions()
}
// MARK: - 許可処理
/// カメラ許可をチェックしてリクエスト
func checkPermissions() {
switch AVCaptureDevice.authorizationStatus(for: .video) {
case .authorized:
permissionStatus = .authorized
configureSession()
case .notDetermined:
sessionQueue.suspend()
AVCaptureDevice.requestAccess(for: .video) { [weak self] granted in
guard let self = self else { return }
DispatchQueue.main.async {
self.permissionStatus = granted ? .authorized : .denied
}
if granted {
self.configureSession()
}
self.sessionQueue.resume()
}
case .denied, .restricted:
permissionStatus = .denied
print("⚠️ カメラアクセスが拒否されました")
@unknown default:
break
}
}
// MARK: - セッション設定
/// AVCaptureSessionを設定
private func configureSession() {
sessionQueue.async { [weak self] in
guard let self = self else { return }
self.session.beginConfiguration()
self.session.sessionPreset = .photo
// カメラ入力を設定
guard self.setupCameraInput() else {
self.session.commitConfiguration()
return
}
// 写真出力を設定
guard self.setupPhotoOutput() else {
self.session.commitConfiguration()
return
}
// Camera Controlを設定 (iOS 18+)
if #available(iOS 18.0, *) {
self.setupCameraControls()
}
self.session.commitConfiguration()
self.session.startRunning()
print("✅ カメラセッションが開始されました")
}
}
/// カメラ入力を設定
private func setupCameraInput() -> Bool {
guard let camera = AVCaptureDevice.default(
.builtInWideAngleCamera,
for: .video,
position: .back
) else {
print("❌ カメラが利用できません")
return false
}
do {
let input = try AVCaptureDeviceInput(device: camera)
if session.canAddInput(input) {
session.addInput(input)
deviceInput = input
setupDeviceObservers(device: camera)
return true
} else {
print("❌ カメラ入力を追加できませんでした")
return false
}
} catch {
print("❌ エラー: \(error)")
return false
}
}
/// 写真出力を設定
private func setupPhotoOutput() -> Bool {
guard session.canAddOutput(photoOutput) else {
print("❌ 写真出力を追加できませんでした")
return false
}
session.addOutput(photoOutput)
photoOutput.isHighResolutionCaptureEnabled = true
if let connection = photoOutput.connection(with: .video),
connection.isVideoStabilizationSupported {
connection.preferredVideoStabilizationMode = .auto
}
return true
}
/// デバイス変更オブザーバーを設定
private func setupDeviceObservers(device: AVCaptureDevice) {
// ズーム変更を監視
let zoomObserver = device.observe(
\.videoZoomFactor,
options: [.new]
) { [weak self] device, change in
guard let newValue = change.newValue else { return }
DispatchQueue.main.async {
self?.currentZoomFactor = newValue
}
}
observations.append(zoomObserver)
// 露出変更を監視
let exposureObserver = device.observe(
\.exposureTargetBias,
options: [.new]
) { [weak self] device, change in
guard let newValue = change.newValue else { return }
DispatchQueue.main.async {
self?.currentExposureBias = newValue
}
}
observations.append(exposureObserver)
}
// MARK: - 写真撮影
/// 写真を撮影
func capturePhoto() {
guard let connection = photoOutput.connection(with: .video),
connection.isEnabled else {
print("❌ カメラ接続が非アクティブです")
return
}
let settings = AVCapturePhotoSettings()
settings.flashMode = .auto
settings.isHighResolutionPhotoEnabled = true
photoOutput.capturePhoto(with: settings, delegate: self)
print("📸 写真撮影を開始しました")
}
// MARK: - ライフサイクル
func startSession() {
sessionQueue.async { [weak self] in
guard let self = self, !self.session.isRunning else { return }
self.session.startRunning()
}
}
func stopSession() {
sessionQueue.async { [weak self] in
guard let self = self, self.session.isRunning else { return }
self.session.stopRunning()
}
}
deinit {
observations.removeAll()
stopSession()
}
}🎛️ ステップ2: Camera Controlの追加 (iOS 18+)#
// MARK: - Camera Control設定 (iOS 18+)
@available(iOS 18.0, *)
extension CameraController: AVCaptureSessionControlsDelegate {
/// Camera Controlを設定
func setupCameraControls() {
// サポートをチェック
guard session.supportsControls else {
print("⚠️ Camera Controlはサポートされていません")
return
}
guard let device = deviceInput?.device else {
print("❌ カメラデバイスが見つかりません")
return
}
// 既存のコントロールを削除
for control in session.controls {
session.removeControl(control)
}
var controls: [AVCaptureControl] = []
// 1. システムズームスライダー
let zoomSlider = AVCaptureSystemZoomSlider(
device: device
) { [weak self] zoomFactor in
DispatchQueue.main.async {
self?.currentZoomFactor = zoomFactor
}
print("🔍 ズーム: \(zoomFactor)x")
}
controls.append(zoomSlider)
// 2. システム露出スライダー
let exposureSlider = AVCaptureSystemExposureBiasSlider(
device: device
) { [weak self] bias in
DispatchQueue.main.async {
self?.currentExposureBias = bias
}
print("☀️ 露出: \(bias)")
}
controls.append(exposureSlider)
// セッションにコントロールを追加
for control in controls {
if session.canAddControl(control) {
session.addControl(control)
print("✅ 追加: \(type(of: control))")
}
}
// デリゲートを設定
session.setControlsDelegate(self, queue: sessionQueue)
print("✅ Camera Controlが設定されました")
}
// MARK: - AVCaptureSessionControlsDelegate
func sessionControlsDidBecomeActive(_ session: AVCaptureSession) {
DispatchQueue.main.async { [weak self] in
self?.isCameraControlActive = true
print("🎮 Camera Controlがアクティブになりました")
}
}
func sessionControlsDidBecomeInactive(_ session: AVCaptureSession) {
DispatchQueue.main.async { [weak self] in
self?.isCameraControlActive = false
print("🎮 Camera Controlが非アクティブになりました")
}
}
}📸 ステップ3: 写真撮影処理#
// MARK: - AVCapturePhotoCaptureDelegate
extension CameraController: AVCapturePhotoCaptureDelegate {
func photoOutput(
_ output: AVCapturePhotoOutput,
didFinishProcessingPhoto photo: AVCapturePhoto,
error: Error?
) {
if let error = error {
print("❌ エラー: \(error.localizedDescription)")
return
}
guard let imageData = photo.fileDataRepresentation(),
let image = UIImage(data: imageData) else {
print("❌ 画像を取得できませんでした")
return
}
DispatchQueue.main.async { [weak self] in
self?.lastCapturedImage = image
}
saveToPhotoLibrary(imageData: imageData)
print("✅ 写真が撮影されました")
}
private func saveToPhotoLibrary(imageData: Data) {
PHPhotoLibrary.requestAuthorization(for: .addOnly) { status in
guard status == .authorized else {
print("⚠️ 保存許可がありません")
return
}
PHPhotoLibrary.shared().performChanges {
let request = PHAssetCreationRequest.forAsset()
request.addResource(with: .photo, data: imageData, options: nil)
} completionHandler: { success, error in
if success {
print("✅ ギャラリーに保存されました")
}
}
}
}
}🔐 ステップ7: Lock Screen Camera Extension#
これは重要なステップです! Lock Screen Extensionがないと、アプリはCamera Control設定に表示されません。
Extension Targetの作成#
- Xcodeで: File → New → Target
- "Locked Camera Capture Extension" を選択
- 名前を付ける:
YourAppLockScreenCamera - Create をクリック
Extension実装#
import LockedCameraCapture
import AVFoundation
import Photos
import UIKit
@main
class LockScreenCameraExtension: NSObject, LockedCameraCaptureExtension {
private var captureSession: LockedCameraCaptureSession?
private var photoOutput: LockedCameraCapturePhotoOutput?
func beginCapture(
with device: LockedCameraCaptureDevice,
completion: @escaping (Error?) -> Void
) {
print("📷 Lock Screen Camera: キャプチャを開始")
captureSession = LockedCameraCaptureSession()
photoOutput = LockedCameraCapturePhotoOutput()
guard let session = captureSession,
let output = photoOutput else {
completion(LockScreenCameraError.initializationFailed)
return
}
do {
try session.addInput(device)
try session.addOutput(output)
session.startRunning()
print("✅ Lock Screen Camera: セッション開始")
completion(nil)
} catch {
print("❌ Lock Screen Camera: \(error)")
completion(error)
}
}
func endCapture(completion: @escaping (Error?) -> Void) {
captureSession?.stopRunning()
captureSession = nil
photoOutput = nil
completion(nil)
}
}
enum LockScreenCameraError: Error {
case initializationFailed
}⚠️ 一般的な問題と解決策#
問題1: Camera Controlが表示されない#
症状: サイドボタンが反応しない。
解決策:
session.supportsControlsを確認 — iPhone 16+が必要- セッションが実行中であることを確認
- プレビューが画面に表示されている必要がある
- コントロールが追加されている必要がある
問題2: アプリがCamera Control設定に表示されない#
原因: Lock Screen Camera Extensionがない。
解決策: Extensionを作成(ステップ7)。
問題3: UIが更新されない#
原因: メインスレッドで更新されていない。
// ✅ 正しい
let zoomSlider = AVCaptureSystemZoomSlider(device: device) { [weak self] factor in
DispatchQueue.main.async {
self?.currentZoomFactor = factor
}
}🎯 ベストプラクティス#
1. 常に互換性をチェック#
if #available(iOS 18.0, *), session.supportsControls {
setupCameraControls()
} else {
showTraditionalControls()
}2. Camera Controlがアクティブな時はコントロールを非表示#
func sessionControlsDidBecomeActive(_ session: AVCaptureSession) {
DispatchQueue.main.async {
self.zoomSlider.isHidden = true
}
}📋 リリース前チェックリスト#
- Lock Screen Camera Extensionが作成された
- Info.plistにすべての許可が含まれている
- コードがサポートの欠如を処理する
- UIがメインスレッドで更新される
- セッションがバックグラウンドスレッドで設定される
- システムコントロールが追加された
- AVCaptureEventInteractionが設定された
- 実機iPhone 16でテスト済み
- グレースフルデグラデーションが機能する
🎬 結論#
Camera Controlは、ユーザーがiPhoneでカメラとやり取りする方法の根本的な変化です。
重要なポイント#
- Lock Screen Extensionは必須 — これがないとアプリは設定に表示されない
supportsControlsをチェック — iPhone 16+でのみ機能- システムコントロールを使用 —
AVCaptureSystemZoomSliderとAVCaptureSystemExposureBiasSlider - イベントを処理 — シャッターボタン用の
AVCaptureEventInteraction - UIを同期 —
AVCaptureSessionControlsDelegateデリゲートを通じて - 実機でテスト — シミュレーターはCamera Controlをサポートしていない
便利なリンク:

