SelfieCam/SelfieCam/Features/Camera/Components/CaptureEventInteraction.swift
Matt Bruce 84b565cbb5 Reorganize folder structure with consistent Views/ViewModels/Components pattern
Camera:
- Views/: ContentView, CustomCameraScreen, PhotoReviewView
- Components/: All UI components (buttons, overlays, controls)

Settings:
- Views/: SettingsView, AppLicensesView
- ViewModels/: SettingsViewModel + extensions
- Components/: ColorPresetButton, CustomColorPickerButton

Paywall:
- Views/: ProPaywallView

Shared:
- Theme/: SelfieCamTheme, DesignConstants, BrandingConfig
- Extensions/: Color extensions (moved Color+Extensions here)
2026-01-04 18:39:59 -06:00

190 lines
6.4 KiB
Swift

//
// 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
}
}
}