// // CaptureEventInteraction.swift // SelfieCam // // Handles hardware capture events using AVCaptureEventInteraction. // Supports Camera Control button (iPhone 16+), Action button, and volume buttons. // import AVFoundation import AVKit import Bedrock import SwiftUI import UIKit /// Manager for AVCaptureEventInteraction that handles Camera Control button events /// /// This class wraps `AVCaptureEventInteraction` to provide hardware button support: /// - **Camera Control button** (iPhone 16+): Full press to capture, light press for focus/exposure /// - **Action button** (iPhone 15 Pro+): When configured for camera /// - **Volume buttons**: Hardware shutter via system integration @Observable @MainActor final class CaptureEventManager { private(set) var isActive = false private(set) var isFocusLocked = false private var eventInteraction: AVCaptureEventInteraction? private var onCapture: (() -> Void)? private var onFocusLock: ((Bool) -> Void)? /// Pre-initialized haptic generators for immediate response private let lightHaptic = UIImpactFeedbackGenerator(style: .light) private let mediumHaptic = UIImpactFeedbackGenerator(style: .medium) private let heavyHaptic = UIImpactFeedbackGenerator(style: .heavy) /// Sets up capture event handling with the provided callbacks /// - Parameters: /// - onCapture: Called when a capture event is triggered (full press) /// - onFocusLock: Called when focus/exposure lock state changes (light press) func configure( onCapture: @escaping () -> Void, onFocusLock: ((Bool) -> Void)? = nil ) { self.onCapture = onCapture self.onFocusLock = onFocusLock } /// Creates and returns the AVCaptureEventInteraction to be added to a view func createInteraction() -> AVCaptureEventInteraction { Design.debugLog("📸 [CameraControl] Creating AVCaptureEventInteraction...") let interaction = AVCaptureEventInteraction( primary: { [weak self] event in Task { @MainActor [weak self] in self?.handlePrimaryEvent(event) } }, secondary: { [weak self] event in Task { @MainActor [weak self] in self?.handleSecondaryEvent(event) } } ) // Must explicitly enable the interaction interaction.isEnabled = true // Prepare haptic engines for immediate response prepareHaptics() self.eventInteraction = interaction self.isActive = true Design.debugLog("📸 [CameraControl] ✅ Hardware capture events ENABLED") Design.debugLog("📸 [CameraControl] Supported: Full press (capture), Light press (focus lock)") return interaction } /// Removes the interaction and cleans up func invalidate() { eventInteraction = nil isActive = false isFocusLocked = false Design.debugLog("📸 [CameraControl] Hardware capture events disabled") } // MARK: - Event Handlers private func handlePrimaryEvent(_ event: AVCaptureEvent) { Design.debugLog("📸 [CameraControl] PRIMARY - phase: \(event.phase.rawValue)") switch event.phase { case .began: Design.debugLog("📸 [CameraControl] PRIMARY BEGAN - Full press started") triggerHaptic(.medium) case .ended: Design.debugLog("📸 [CameraControl] PRIMARY ENDED - Triggering capture!") triggerHaptic(.heavy) onCapture?() case .cancelled: Design.debugLog("📸 [CameraControl] PRIMARY CANCELLED") @unknown default: Design.debugLog("📸 [CameraControl] PRIMARY UNKNOWN phase: \(event.phase.rawValue)") } } private func handleSecondaryEvent(_ event: AVCaptureEvent) { Design.debugLog("🔒 [CameraControl] SECONDARY - phase: \(event.phase.rawValue)") switch event.phase { case .began: Design.debugLog("🔒 [CameraControl] SECONDARY BEGAN - Light press! Locking focus...") isFocusLocked = true onFocusLock?(true) triggerHaptic(.medium) case .ended: Design.debugLog("🔒 [CameraControl] SECONDARY ENDED - Unlocking focus") isFocusLocked = false onFocusLock?(false) triggerHaptic(.light) case .cancelled: Design.debugLog("🔒 [CameraControl] SECONDARY CANCELLED") isFocusLocked = false onFocusLock?(false) @unknown default: Design.debugLog("🔒 [CameraControl] SECONDARY UNKNOWN phase: \(event.phase.rawValue)") } } // MARK: - Haptic Feedback /// Prepares haptic engines for low-latency feedback func prepareHaptics() { lightHaptic.prepare() mediumHaptic.prepare() heavyHaptic.prepare() } private func triggerHaptic(_ style: UIImpactFeedbackGenerator.FeedbackStyle) { switch style { case .light: lightHaptic.impactOccurred() lightHaptic.prepare() case .medium: mediumHaptic.impactOccurred() mediumHaptic.prepare() case .heavy: heavyHaptic.impactOccurred() heavyHaptic.prepare() default: let generator = UIImpactFeedbackGenerator(style: style) generator.impactOccurred() } } } // MARK: - Device Capability Check extension CaptureEventManager { /// Checks if the device supports Camera Control button /// Note: The API is always available on iOS 18+, but the physical button is iPhone 16+ only static var isCameraControlSupported: Bool { return true } /// Returns a description of supported hardware capture methods static var supportedHardwareMethods: String { return "Camera Control (iPhone 16+), Action Button, Volume Buttons" } } // MARK: - AVCaptureEventPhase Extension extension AVCaptureEventPhase { /// Converts AVCaptureEventPhase to our CaptureEventPhase enum var toCaptureEventPhase: CaptureEventPhase { switch self { case .began: return .began case .ended: return .ended case .cancelled: return .cancelled @unknown default: return .cancelled } } }