📸
音量ボタン撮影をAVCaptureEventInteractionで実装する
概要
カメラで撮影する時、画面のボタンをタップする代わりにデバイスの音量ボタンを操作するのが一般的です。
iOS17.2から利用可能なAVCaptureEventInteraction
をSwiftUI製アプリに統合する方法を紹介します。
環境
- iOS 17.2以降(macOS, tvOS, watchOS, visionOSは現在非対応)
- UIKitまたはSwiftUI
- Xcode 16.1 Beta2
インタラクションをビューに追加する
UIKit
UIKitの世界で利用するにはAVCaptureEventInteraction
を初期化し、UIViewController
のview
に追加します。
import AVKit
import UIKit
@available(iOS 17.2, *)
class ViewController: UIViewController {
// カメラセッションを管理するモデルは別で用意してください
private let camera = CameraModel()
private var eventInteraction: AVCaptureEventInteraction?
override func viewDidLoad() {
super.viewDidLoad()
// ハードウェアボタン(音量ボタン等)の処理をハンドリング
configureHardwareInteraction()
}
private func configureHardwareInteraction() {
let interaction = AVCaptureEventInteraction { [weak self] (event: AVCaptureEvent) -> Void in
// ボタンを離した("press up")時は`event.phase == .ended`になる
if event.phase == .ended {
// `camera.capturePhoto()` で写真を撮影する
self?.camera.capturePhoto()
}
}
view.addInteraction(interaction)
eventInteraction = interaction
}
}
SwiftUI
カメラを利用する場合、
AVCaptureVideoPreviewLayer
をUIView
でラップし、UIViewRepresentable
(またはUIViewContollerRepresentable
を使用してSwiftUIのビュー階層に統合します。
この時点でAVCaptureEventInteraction
を差し込むシンプルな実装ができれば楽なのですが、どうしてもSwiftUIで扱いたい場合は以下のようなViewModifier
で扱いやすく整えることもできます。
import AVKit
import SwiftUI
@available(iOS 17.2, *)
struct CaptureInteractionModifier: ViewModifier {
let beganAction: () -> Void
let endedAction: () -> Void
let cancelledAction: () -> Void
func body(content: Content) -> some View {
content
.background {
CaptureInteractionWrapper(beganAction: beganAction, endedAction: endedAction, cancelledAction: cancelledAction)
}
}
struct CaptureInteractionWrapper: UIViewRepresentable {
let beganAction: () -> Void
let endedAction: () -> Void
let cancelledAction: () -> Void
func makeUIView(context: Context) -> UIView {
let view = UIView()
let interaction = AVCaptureEventInteraction { event in
switch event.phase {
case .began:
beganAction()
case .ended:
endedAction()
case .cancelled:
cancelledAction()
@unknown default:
break
}
}
view.addInteraction(interaction)
return view
}
func updateUIView(_ uiView: UIView, context: Context) {}
}
}
extension View {
@ViewBuilder
func captureInteractionIfAvailable(beganAction: @escaping () -> Void = {}, endedAction: @escaping () -> Void = {}, cancelledAction: @escaping () -> Void = {}) -> some View {
if #available(iOS 17.2, *) {
captureInteraction(beganAction: beganAction, endedAction: endedAction, cancelledAction: cancelledAction)
} else {
self
}
}
@available(iOS 17.2, *)
private func captureInteraction(beganAction: @escaping () -> Void, endedAction: @escaping () -> Void, cancelledAction: @escaping () -> Void) -> some View {
modifier(CaptureInteractionModifier(beganAction: beganAction, endedAction: endedAction, cancelledAction: cancelledAction))
}
}
SwiftUI, iOS18.0 以降
iOS18.0以降は標準ViewModifier
が生えているのでそちらを利用します。
onCameraCaptureEvent(isEnabled:action:)
onCameraCaptureEvent(isEnabled:primaryAction:secondaryAction:)
// 1種類のアクションを受け取るシンプルなAPI
func onCameraCaptureEvent(
isEnabled: Bool = true,
action: @escaping (AVCaptureEvent) -> Void
) -> some View
// primary actionとsecondary actionを受け取るAPI
func onCameraCaptureEvent(
isEnabled: Bool = true,
primaryAction: @escaping (AVCaptureEvent) -> Void,
secondaryAction: @escaping (AVCaptureEvent) -> Void
) -> some View
実際には以下のように利用します。
import AVKit
import SwiftUI
@available(iOS 18.0, *)
struct TestView: View {
let camera = CameraModel()
@State private var isInteractionEnabled = true
var body: some View {
CameraPreview()
.onCameraCaptureEvent(isEnabled: isInteractionEnabled) { event in
if event.phase == .ended {
camera.capturePhoto()
}
}
}
}
実装上の注意点
- このAPIはカメラを実際に使用しているアプリでのみ動作します。
- isEnabledの切り替え
- カメラが使用できない状況では
isEnabled
プロパティをfalseに設定し、デフォルトの音量ボタン動作が使えるようにしてください
- カメラが使用できない状況では
- iOS17.2以降でのみ利用可能な点
-
AVCaptureEventInteraction
はiOS 17.2以降でのみ利用可能です。それ以前でのバージョンでは従来の方法(音量通知の監視など)へのフォールバックを検討してください。- リジェクトの危険性がある非公開の音量ボタンの通知
_UIApplicationVolumeUpButtonDownNotification
-
outputVolume
を監視する方法など
- リジェクトの危険性がある非公開の音量ボタンの通知
-
関連資料
Discussion