SelfieCam/SelfieCam/Features/Camera/Views/CaptureEventInteraction.swift
Matt Bruce 255f3c2d68 Add branded SelfieCamTheme with visual improvements to settings
- Create SelfieCamTheme.swift with rose/magenta-tinted surface colors matching app branding
- Add App-prefixed typealiases (AppSurface, AppAccent, AppStatus, etc.) to avoid Bedrock conflicts
- Add SettingsCard container for visual grouping of related settings
- Update SettingsView with card containers and accent-colored section headers
- Update all settings-related files to use new theme colors
- Pro section uses warning color, Debug section uses error color for visual distinction
2026-01-04 16:54:23 -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
}
}
}