Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
This commit is contained in:
parent
f7c554d018
commit
66736d39f9
@ -522,6 +522,7 @@ SelfieCam uses the following architectural patterns:
|
|||||||
| `RingLightConfigurable` | Ring light settings (size, color, opacity) | `SettingsViewModel` |
|
| `RingLightConfigurable` | Ring light settings (size, color, opacity) | `SettingsViewModel` |
|
||||||
| `CaptureControlling` | Capture actions (timer, flash, shutter) | `SettingsViewModel` |
|
| `CaptureControlling` | Capture actions (timer, flash, shutter) | `SettingsViewModel` |
|
||||||
| `PremiumManaging` | Subscription state and purchases | `PremiumManager` |
|
| `PremiumManaging` | Subscription state and purchases | `PremiumManager` |
|
||||||
|
| `CaptureEventHandling` | Hardware button capture events (Camera Control, Action button) | `CaptureEventManager` |
|
||||||
|
|
||||||
|
|
||||||
## Premium Features
|
## Premium Features
|
||||||
|
|||||||
@ -75,6 +75,8 @@ Features/
|
|||||||
- Post-capture preview with share functionality
|
- Post-capture preview with share functionality
|
||||||
- Auto-save option to Photo Library
|
- Auto-save option to Photo Library
|
||||||
- Front flash using screen brightness
|
- Front flash using screen brightness
|
||||||
|
- **Camera Control button** (iPhone 16+): Full press captures, light press locks focus/exposure
|
||||||
|
- **Hardware shutter**: Volume buttons trigger capture via `VolumeButtonObserver`
|
||||||
|
|
||||||
### 4. Freemium Model
|
### 4. Freemium Model
|
||||||
- Built with **RevenueCat** for subscription management
|
- Built with **RevenueCat** for subscription management
|
||||||
@ -98,6 +100,52 @@ Features/
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Camera Control Button Integration
|
||||||
|
|
||||||
|
### Overview
|
||||||
|
|
||||||
|
The app supports the **Camera Control** button on iPhone 16+ via `AVCaptureEventInteraction` (iOS 17.2+).
|
||||||
|
|
||||||
|
### Files Involved
|
||||||
|
|
||||||
|
| File | Purpose |
|
||||||
|
|------|---------|
|
||||||
|
| `Shared/Protocols/CaptureEventHandling.swift` | Protocol defining hardware capture event handling |
|
||||||
|
| `Features/Camera/Views/CaptureEventInteraction.swift` | `AVCaptureEventInteraction` wrapper and SwiftUI integration |
|
||||||
|
| `Features/Camera/Views/VolumeButtonObserver.swift` | Volume button capture support (legacy) |
|
||||||
|
|
||||||
|
### Supported Hardware Events
|
||||||
|
|
||||||
|
| Event | Hardware | Action |
|
||||||
|
|-------|----------|--------|
|
||||||
|
| **Primary (full press)** | Camera Control, Action Button | Capture photo |
|
||||||
|
| **Secondary (light press)** | Camera Control | Lock focus/exposure |
|
||||||
|
| **Volume buttons** | All iPhones | Capture photo |
|
||||||
|
|
||||||
|
### Implementation Details
|
||||||
|
|
||||||
|
```swift
|
||||||
|
// CaptureEventInteractionView is added to the camera ZStack
|
||||||
|
CaptureEventInteractionView(
|
||||||
|
onCapture: { performCapture() },
|
||||||
|
onFocusLock: { locked in handleFocusLock(locked) }
|
||||||
|
)
|
||||||
|
|
||||||
|
// The interaction uses AVCaptureEventInteraction (iOS 17.2+)
|
||||||
|
AVCaptureEventInteraction(
|
||||||
|
primaryEventHandler: { phase in /* capture on .ended */ },
|
||||||
|
secondaryEventHandler: { phase in /* focus lock on .began/.ended */ }
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Device Compatibility
|
||||||
|
|
||||||
|
- **iPhone 16+**: Full Camera Control button support (press + light press)
|
||||||
|
- **iPhone 15 Pro+**: Action button support (when configured for camera)
|
||||||
|
- **All iPhones**: Volume button shutter via `VolumeButtonObserver`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Premium Feature Implementation
|
## Premium Feature Implementation
|
||||||
|
|
||||||
### How Premium Gating Works
|
### How Premium Gating Works
|
||||||
|
|||||||
@ -21,6 +21,8 @@ Perfect for low-light selfies, content creation, video calls, makeup application
|
|||||||
- Pinch-to-zoom gesture support
|
- Pinch-to-zoom gesture support
|
||||||
- Rule-of-thirds grid overlay (toggleable)
|
- Rule-of-thirds grid overlay (toggleable)
|
||||||
- Post-capture preview with share functionality
|
- Post-capture preview with share functionality
|
||||||
|
- **Camera Control button** support (iPhone 16+): Full press to capture, light press to lock focus/exposure
|
||||||
|
- **Hardware shutter**: Volume buttons trigger capture
|
||||||
|
|
||||||
### Premium Features (Freemium Model)
|
### Premium Features (Freemium Model)
|
||||||
- **Custom ring light colors** with full color picker
|
- **Custom ring light colors** with full color picker
|
||||||
@ -162,6 +164,7 @@ SelfieCam/
|
|||||||
│ │ │ ├── RingLightOverlay.swift
|
│ │ │ ├── RingLightOverlay.swift
|
||||||
│ │ │ ├── CaptureButton.swift
|
│ │ │ ├── CaptureButton.swift
|
||||||
│ │ │ ├── ExpandableControlsPanel.swift
|
│ │ │ ├── ExpandableControlsPanel.swift
|
||||||
|
│ │ │ ├── CaptureEventInteraction.swift # Camera Control button support
|
||||||
│ │ │ └── ...
|
│ │ │ └── ...
|
||||||
│ │ ├── GridOverlay.swift # Rule of thirds overlay
|
│ │ ├── GridOverlay.swift # Rule of thirds overlay
|
||||||
│ │ └── PostCapturePreviewView.swift
|
│ │ └── PostCapturePreviewView.swift
|
||||||
@ -183,6 +186,7 @@ SelfieCam/
|
|||||||
│ ├── Protocols/ # Shared protocols
|
│ ├── Protocols/ # Shared protocols
|
||||||
│ │ ├── RingLightConfigurable.swift
|
│ │ ├── RingLightConfigurable.swift
|
||||||
│ │ ├── CaptureControlling.swift
|
│ │ ├── CaptureControlling.swift
|
||||||
|
│ │ ├── CaptureEventHandling.swift # Hardware capture events
|
||||||
│ │ └── PremiumManaging.swift
|
│ │ └── PremiumManaging.swift
|
||||||
│ ├── Premium/ # Subscription management
|
│ ├── Premium/ # Subscription management
|
||||||
│ │ └── PremiumManager.swift
|
│ │ └── PremiumManager.swift
|
||||||
|
|||||||
263
SelfieCam/Features/Camera/Views/CaptureEventInteraction.swift
Normal file
263
SelfieCam/Features/Camera/Views/CaptureEventInteraction.swift
Normal file
@ -0,0 +1,263 @@
|
|||||||
|
//
|
||||||
|
// CaptureEventInteraction.swift
|
||||||
|
// SelfieCam
|
||||||
|
//
|
||||||
|
// Handles hardware capture events using AVCaptureEventInteraction (iOS 17.2+).
|
||||||
|
// 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
|
||||||
|
///
|
||||||
|
/// - Note: Requires iOS 17.2+ for `AVCaptureEventInteraction` API
|
||||||
|
@Observable @MainActor
|
||||||
|
final class CaptureEventManager {
|
||||||
|
private(set) var isActive = false
|
||||||
|
private(set) var isFocusLocked = false
|
||||||
|
|
||||||
|
// Store as Any to avoid availability issues with the type declaration
|
||||||
|
private var eventInteraction: AnyObject?
|
||||||
|
private var onCapture: (() -> Void)?
|
||||||
|
private var onFocusLock: ((Bool) -> Void)?
|
||||||
|
|
||||||
|
/// 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 UIInteraction to be added to a view
|
||||||
|
/// - Returns: The configured interaction, or nil if not available
|
||||||
|
func createInteraction() -> (any UIInteraction)? {
|
||||||
|
guard #available(iOS 17.2, *) else {
|
||||||
|
Design.debugLog("CaptureEventInteraction: iOS 17.2+ required")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return createCaptureEventInteraction()
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(iOS 17.2, *)
|
||||||
|
private func createCaptureEventInteraction() -> 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
self.eventInteraction = interaction
|
||||||
|
self.isActive = true
|
||||||
|
Design.debugLog("CaptureEventInteraction: Hardware capture events enabled")
|
||||||
|
|
||||||
|
return interaction
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Removes the interaction and cleans up
|
||||||
|
func invalidate() {
|
||||||
|
eventInteraction = nil
|
||||||
|
isActive = false
|
||||||
|
isFocusLocked = false
|
||||||
|
Design.debugLog("CaptureEventInteraction: Hardware capture events disabled")
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Event Handlers
|
||||||
|
|
||||||
|
@available(iOS 17.2, *)
|
||||||
|
private func handlePrimaryEvent(_ event: AVCaptureEvent) {
|
||||||
|
switch event.phase {
|
||||||
|
case .began:
|
||||||
|
Design.debugLog("CaptureEventInteraction: Primary event began (press detected)")
|
||||||
|
// Haptic feedback on press
|
||||||
|
triggerHaptic(.light)
|
||||||
|
|
||||||
|
case .ended:
|
||||||
|
Design.debugLog("CaptureEventInteraction: Primary event ended (capture)")
|
||||||
|
onCapture?()
|
||||||
|
|
||||||
|
case .cancelled:
|
||||||
|
Design.debugLog("CaptureEventInteraction: Primary event cancelled")
|
||||||
|
|
||||||
|
@unknown default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(iOS 17.2, *)
|
||||||
|
private func handleSecondaryEvent(_ event: AVCaptureEvent) {
|
||||||
|
switch event.phase {
|
||||||
|
case .began:
|
||||||
|
Design.debugLog("CaptureEventInteraction: Secondary event began (focus lock)")
|
||||||
|
isFocusLocked = true
|
||||||
|
onFocusLock?(true)
|
||||||
|
triggerHaptic(.light)
|
||||||
|
|
||||||
|
case .ended:
|
||||||
|
Design.debugLog("CaptureEventInteraction: Secondary event ended (focus unlock)")
|
||||||
|
isFocusLocked = false
|
||||||
|
onFocusLock?(false)
|
||||||
|
|
||||||
|
case .cancelled:
|
||||||
|
Design.debugLog("CaptureEventInteraction: Secondary event cancelled")
|
||||||
|
isFocusLocked = false
|
||||||
|
onFocusLock?(false)
|
||||||
|
|
||||||
|
@unknown default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Haptic Feedback
|
||||||
|
|
||||||
|
private func triggerHaptic(_ style: UIImpactFeedbackGenerator.FeedbackStyle) {
|
||||||
|
let generator = UIImpactFeedbackGenerator(style: style)
|
||||||
|
generator.impactOccurred()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - SwiftUI View for Hosting the Interaction
|
||||||
|
|
||||||
|
/// A SwiftUI view that hosts AVCaptureEventInteraction for Camera Control button support
|
||||||
|
///
|
||||||
|
/// Add this view to your camera hierarchy to enable hardware capture events.
|
||||||
|
/// The view is invisible and passes through all touches.
|
||||||
|
///
|
||||||
|
/// Usage:
|
||||||
|
/// ```swift
|
||||||
|
/// ZStack {
|
||||||
|
/// CameraPreview()
|
||||||
|
/// CaptureEventInteractionView(
|
||||||
|
/// onCapture: { performCapture() },
|
||||||
|
/// onFocusLock: { locked in handleFocusLock(locked) }
|
||||||
|
/// )
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
struct CaptureEventInteractionView: UIViewRepresentable {
|
||||||
|
/// Called when primary capture event occurs (full press)
|
||||||
|
let onCapture: () -> Void
|
||||||
|
|
||||||
|
/// Called when focus/exposure lock state changes (light press)
|
||||||
|
var onFocusLock: ((Bool) -> Void)?
|
||||||
|
|
||||||
|
func makeUIView(context: Context) -> CaptureEventHostView {
|
||||||
|
let view = CaptureEventHostView()
|
||||||
|
view.backgroundColor = .clear
|
||||||
|
view.isUserInteractionEnabled = true
|
||||||
|
|
||||||
|
// Configure and add the interaction
|
||||||
|
context.coordinator.manager.configure(
|
||||||
|
onCapture: onCapture,
|
||||||
|
onFocusLock: onFocusLock
|
||||||
|
)
|
||||||
|
|
||||||
|
if let interaction = context.coordinator.manager.createInteraction() {
|
||||||
|
view.addInteraction(interaction)
|
||||||
|
context.coordinator.interaction = interaction
|
||||||
|
}
|
||||||
|
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateUIView(_ uiView: CaptureEventHostView, context: Context) {
|
||||||
|
// Update callbacks if they change
|
||||||
|
context.coordinator.manager.configure(
|
||||||
|
onCapture: onCapture,
|
||||||
|
onFocusLock: onFocusLock
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeCoordinator() -> Coordinator {
|
||||||
|
Coordinator()
|
||||||
|
}
|
||||||
|
|
||||||
|
static func dismantleUIView(_ uiView: CaptureEventHostView, coordinator: Coordinator) {
|
||||||
|
if let interaction = coordinator.interaction {
|
||||||
|
uiView.removeInteraction(interaction)
|
||||||
|
}
|
||||||
|
coordinator.manager.invalidate()
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
class Coordinator {
|
||||||
|
let manager = CaptureEventManager()
|
||||||
|
var interaction: (any UIInteraction)?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// UIView subclass that hosts AVCaptureEventInteraction
|
||||||
|
///
|
||||||
|
/// This view is transparent and allows touches to pass through while
|
||||||
|
/// still receiving hardware capture events.
|
||||||
|
final class CaptureEventHostView: UIView {
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
setup()
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
super.init(coder: coder)
|
||||||
|
setup()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func setup() {
|
||||||
|
backgroundColor = .clear
|
||||||
|
isUserInteractionEnabled = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow touches to pass through to underlying views
|
||||||
|
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||||
|
// Return nil to pass touches through, but the interaction still receives hardware events
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Device Capability Check
|
||||||
|
|
||||||
|
extension CaptureEventManager {
|
||||||
|
/// Checks if the device supports Camera Control button
|
||||||
|
static var isCameraControlSupported: Bool {
|
||||||
|
guard #available(iOS 17.2, *) else { return false }
|
||||||
|
|
||||||
|
// Camera Control is available on iPhone 16 series
|
||||||
|
// We check for iOS 17.2+ as the API requirement
|
||||||
|
// The actual button is hardware-dependent
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a description of supported hardware capture methods
|
||||||
|
static var supportedHardwareMethods: String {
|
||||||
|
var methods: [String] = []
|
||||||
|
|
||||||
|
if #available(iOS 17.2, *) {
|
||||||
|
methods.append("Camera Control (iPhone 16+)")
|
||||||
|
methods.append("Action Button")
|
||||||
|
methods.append("Volume Buttons")
|
||||||
|
} else {
|
||||||
|
methods.append("Volume Buttons (legacy)")
|
||||||
|
}
|
||||||
|
|
||||||
|
return methods.joined(separator: ", ")
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -167,6 +167,16 @@ struct CustomCameraScreen: MCameraScreen {
|
|||||||
// Hidden volume view to prevent system HUD when using volume buttons as shutter
|
// Hidden volume view to prevent system HUD when using volume buttons as shutter
|
||||||
HiddenVolumeView()
|
HiddenVolumeView()
|
||||||
.frame(width: 0, height: 0)
|
.frame(width: 0, height: 0)
|
||||||
|
|
||||||
|
// Camera Control button and hardware capture event support (iOS 17.2+)
|
||||||
|
CaptureEventInteractionView(
|
||||||
|
onCapture: { performCapture() },
|
||||||
|
onFocusLock: { locked in
|
||||||
|
handleFocusLock(locked)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||||
|
.allowsHitTesting(false)
|
||||||
}
|
}
|
||||||
.animation(.easeInOut(duration: 0.05), value: isShowingScreenFlash)
|
.animation(.easeInOut(duration: 0.05), value: isShowingScreenFlash)
|
||||||
.animation(.easeInOut(duration: 0.3), value: isCountdownActive)
|
.animation(.easeInOut(duration: 0.3), value: isCountdownActive)
|
||||||
@ -412,6 +422,46 @@ struct CustomCameraScreen: MCameraScreen {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Focus/Exposure Lock (Camera Control Light Press)
|
||||||
|
|
||||||
|
/// Handles focus/exposure lock from Camera Control button light press
|
||||||
|
/// - Parameter locked: Whether focus/exposure is being locked
|
||||||
|
private func handleFocusLock(_ locked: Bool) {
|
||||||
|
guard let device = AVCaptureDevice.default(
|
||||||
|
.builtInWideAngleCamera,
|
||||||
|
for: .video,
|
||||||
|
position: cameraPosition == .front ? .front : .back
|
||||||
|
) else { return }
|
||||||
|
|
||||||
|
do {
|
||||||
|
try device.lockForConfiguration()
|
||||||
|
|
||||||
|
if locked {
|
||||||
|
// Lock current focus and exposure
|
||||||
|
if device.isFocusModeSupported(.locked) {
|
||||||
|
device.focusMode = .locked
|
||||||
|
}
|
||||||
|
if device.isExposureModeSupported(.locked) {
|
||||||
|
device.exposureMode = .locked
|
||||||
|
}
|
||||||
|
Design.debugLog("Focus/Exposure locked via Camera Control")
|
||||||
|
} else {
|
||||||
|
// Restore continuous autofocus and auto exposure
|
||||||
|
if device.isFocusModeSupported(.continuousAutoFocus) {
|
||||||
|
device.focusMode = .continuousAutoFocus
|
||||||
|
}
|
||||||
|
if device.isExposureModeSupported(.continuousAutoExposure) {
|
||||||
|
device.exposureMode = .continuousAutoExposure
|
||||||
|
}
|
||||||
|
Design.debugLog("Focus/Exposure unlocked via Camera Control")
|
||||||
|
}
|
||||||
|
|
||||||
|
device.unlockForConfiguration()
|
||||||
|
} catch {
|
||||||
|
Design.debugLog("Failed to set focus/exposure lock: \(error)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Camera Gestures
|
// MARK: - Camera Gestures
|
||||||
|
|
||||||
/// Flips between front and back camera with haptic feedback
|
/// Flips between front and back camera with haptic feedback
|
||||||
|
|||||||
45
SelfieCam/Shared/Protocols/CaptureEventHandling.swift
Normal file
45
SelfieCam/Shared/Protocols/CaptureEventHandling.swift
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
//
|
||||||
|
// CaptureEventHandling.swift
|
||||||
|
// SelfieCam
|
||||||
|
//
|
||||||
|
// Protocol for handling hardware capture events (Camera Control button, Action button, volume buttons).
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
/// Protocol defining hardware capture event handling capabilities
|
||||||
|
///
|
||||||
|
/// Conforming types can respond to hardware button events for camera capture,
|
||||||
|
/// including the Camera Control button on iPhone 16+, Action button on iPhone 15 Pro+,
|
||||||
|
/// and volume buttons.
|
||||||
|
///
|
||||||
|
/// - Note: This protocol is designed to work with `AVCaptureEventInteraction` (iOS 17.2+)
|
||||||
|
protocol CaptureEventHandling: AnyObject {
|
||||||
|
/// Called when a primary capture event occurs (full press on Camera Control, volume button press)
|
||||||
|
/// - Parameter phase: The phase of the event (.began, .ended, .cancelled)
|
||||||
|
func handlePrimaryCaptureEvent(phase: CaptureEventPhase)
|
||||||
|
|
||||||
|
/// Called when a secondary capture event occurs (light press on Camera Control for focus/exposure)
|
||||||
|
/// - Parameter phase: The phase of the event (.began, .ended, .cancelled)
|
||||||
|
func handleSecondaryCaptureEvent(phase: CaptureEventPhase)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Represents the phase of a hardware capture event
|
||||||
|
enum CaptureEventPhase: Sendable {
|
||||||
|
/// Event began (button pressed down or light press detected)
|
||||||
|
case began
|
||||||
|
/// Event ended (button released)
|
||||||
|
case ended
|
||||||
|
/// Event was cancelled
|
||||||
|
case cancelled
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Default Implementations
|
||||||
|
|
||||||
|
extension CaptureEventHandling {
|
||||||
|
/// Default implementation for secondary events (focus/exposure lock)
|
||||||
|
/// Override in conforming types if focus/exposure lock is needed
|
||||||
|
func handleSecondaryCaptureEvent(phase: CaptureEventPhase) {
|
||||||
|
// Default: no-op, override if focus/exposure lock support is needed
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user