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)
190 lines
6.4 KiB
Swift
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
|
|
}
|
|
}
|
|
}
|