From eedd709ef5e4f990efc134fef47ce45fa9b64d2a Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Sun, 4 Jan 2026 11:34:49 -0600 Subject: [PATCH] Signed-off-by: Matt Bruce --- SelfieCam/Features/Camera/ContentView.swift | 70 +---------- .../Camera/Views/CustomCameraScreen.swift | 118 +++++++----------- 2 files changed, 51 insertions(+), 137 deletions(-) diff --git a/SelfieCam/Features/Camera/ContentView.swift b/SelfieCam/Features/Camera/ContentView.swift index 86f6ed7..9c50be2 100644 --- a/SelfieCam/Features/Camera/ContentView.swift +++ b/SelfieCam/Features/Camera/ContentView.swift @@ -2,47 +2,7 @@ import SwiftUI import MijickCamera import Bedrock -// MARK: - Camera Settings Observable -/// Shared observable class for camera settings to prevent MCamera recreation on changes -@Observable @MainActor -final class CameraSettingsState { - var photoQuality: PhotoQuality - var isRingLightEnabled: Bool - var ringLightColor: Color - var ringLightSize: CGFloat - var ringLightOpacity: Double - var flashMode: CameraFlashMode - var isFlashSyncedWithRingLight: Bool - var hdrMode: CameraHDRMode - var isGridVisible: Bool - var cameraPosition: CameraPosition - init(settings: SettingsViewModel) { - self.photoQuality = settings.photoQuality - self.isRingLightEnabled = settings.isRingLightEnabled - self.ringLightColor = settings.lightColor - self.ringLightSize = settings.ringSize - self.ringLightOpacity = settings.ringLightOpacity - self.flashMode = settings.flashMode - self.isFlashSyncedWithRingLight = settings.isFlashSyncedWithRingLight - self.hdrMode = settings.hdrMode - self.isGridVisible = settings.isGridVisible - self.cameraPosition = settings.cameraPosition - } - - func update(from settings: SettingsViewModel) { - self.photoQuality = settings.photoQuality - self.isRingLightEnabled = settings.isRingLightEnabled - self.ringLightColor = settings.lightColor - self.ringLightSize = settings.ringSize - self.ringLightOpacity = settings.ringLightOpacity - self.flashMode = settings.flashMode - self.isFlashSyncedWithRingLight = settings.isFlashSyncedWithRingLight - self.hdrMode = settings.hdrMode - self.isGridVisible = settings.isGridVisible - self.cameraPosition = settings.cameraPosition - } -} struct ContentView: View { @State private var settings = SettingsViewModel() @@ -55,8 +15,7 @@ struct ContentView: View { @State private var isSavingPhoto = false @State private var saveError: String? - /// Camera settings in a shared observable to prevent MCamera recreation - @State private var cameraSettings: CameraSettingsState? + /// Settings are managed by SettingsViewModel which is already @Observable /// Unique key to force MCamera recreation after photo capture /// Incrementing this value creates a new camera session with fresh AVCapturePhotoOutput @@ -67,7 +26,7 @@ struct ContentView: View { // Camera view - only recreates when sessionKey changes due to Equatable if !showPhotoReview { CameraContainerView( - cameraSettings: cameraSettings ?? CameraSettingsState(settings: settings), + settings: settings, sessionKey: cameraSessionKey, onImageCaptured: { image in capturedPhoto = CapturedPhoto(image: image, timestamp: Date()) @@ -120,29 +79,16 @@ struct ContentView: View { } .ignoresSafeArea() .animation(.easeInOut(duration: Design.Animation.quick), value: showPhotoReview) - .onAppear { - cameraSettings = CameraSettingsState(settings: settings) - } - .onChange(of: settings.photoQuality) { _, _ in updateCameraSettings() } .onChange(of: settings.isRingLightEnabled) { _, newValue in - updateCameraSettings() if settings.isFlashSyncedWithRingLight { settings.flashMode = newValue ? .on : .off } } - .onChange(of: settings.lightColor) { _, _ in updateCameraSettings() } - .onChange(of: settings.ringSize) { _, _ in updateCameraSettings() } - .onChange(of: settings.ringLightOpacity) { _, _ in updateCameraSettings() } - .onChange(of: settings.flashMode) { _, _ in updateCameraSettings() } .onChange(of: settings.isFlashSyncedWithRingLight) { _, newValue in - updateCameraSettings() if newValue { settings.flashMode = settings.isRingLightEnabled ? .on : .off } } - .onChange(of: settings.hdrMode) { _, _ in updateCameraSettings() } - .onChange(of: settings.isGridVisible) { _, _ in updateCameraSettings() } - .onChange(of: settings.cameraPosition) { _, _ in updateCameraSettings() } .sheet(isPresented: $showSettings) { SettingsView(viewModel: settings, showPaywall: $showPaywall) } @@ -151,10 +97,6 @@ struct ContentView: View { } } - private func updateCameraSettings() { - cameraSettings?.update(from: settings) - } - /// Resets state and regenerates camera session key to create a fresh camera instance private func resetCameraForNextCapture() { capturedPhoto = nil @@ -169,7 +111,7 @@ struct ContentView: View { isSavingPhoto = true saveError = nil - let quality = cameraSettings?.photoQuality ?? .high + let quality = settings.photoQuality Task { let result = await PhotoLibraryService.savePhotoToLibrary(image, quality: quality) @@ -199,7 +141,7 @@ struct ContentView: View { // MARK: - Camera Container View /// Wrapper view for MCamera - only recreates when sessionKey changes struct CameraContainerView: View, Equatable { - let cameraSettings: CameraSettingsState + let settings: SettingsViewModel let sessionKey: UUID let onImageCaptured: (UIImage) -> Void @@ -216,12 +158,12 @@ struct CameraContainerView: View, Equatable { cameraManager: cameraManager, namespace: namespace, closeMCameraAction: closeAction, - cameraSettings: cameraSettings, + cameraSettings: settings, onPhotoCaptured: onImageCaptured ) } .setCapturedMediaScreen(nil) - .setCameraPosition(cameraSettings.cameraPosition) + .setCameraPosition(settings.cameraPosition) .startSession() } } diff --git a/SelfieCam/Features/Camera/Views/CustomCameraScreen.swift b/SelfieCam/Features/Camera/Views/CustomCameraScreen.swift index b66bd9a..d70de4f 100644 --- a/SelfieCam/Features/Camera/Views/CustomCameraScreen.swift +++ b/SelfieCam/Features/Camera/Views/CustomCameraScreen.swift @@ -18,40 +18,10 @@ struct CustomCameraScreen: MCameraScreen { let closeMCameraAction: () -> () /// Shared camera settings state - using Observable class prevents MCamera recreation - var cameraSettings: CameraSettingsState + var cameraSettings: SettingsViewModel /// Callback when photo is captured - bypasses MijickCamera's callback system var onPhotoCaptured: ((UIImage) -> Void)? - - // Convenience accessors for settings - private var photoQuality: PhotoQuality { - get { cameraSettings.photoQuality } - nonmutating set { cameraSettings.photoQuality = newValue } - } - private var isRingLightEnabled: Bool { - get { cameraSettings.isRingLightEnabled } - nonmutating set { cameraSettings.isRingLightEnabled = newValue } - } - private var ringLightColor: Color { - get { cameraSettings.ringLightColor } - nonmutating set { cameraSettings.ringLightColor = newValue } - } - private var ringLightSize: CGFloat { - get { cameraSettings.ringLightSize } - nonmutating set { cameraSettings.ringLightSize = newValue } - } - private var ringLightOpacity: Double { - get { cameraSettings.ringLightOpacity } - nonmutating set { cameraSettings.ringLightOpacity = newValue } - } - private var flashMode: CameraFlashMode { - get { cameraSettings.flashMode } - nonmutating set { cameraSettings.flashMode = newValue } - } - private var isFlashSyncedWithRingLight: Bool { - get { cameraSettings.isFlashSyncedWithRingLight } - nonmutating set { cameraSettings.isFlashSyncedWithRingLight = newValue } - } // Center Stage state @State private var isCenterStageEnabled: Bool = AVCaptureDevice.isCenterStageEnabled @@ -98,9 +68,9 @@ struct CustomCameraScreen: MCameraScreen { // Ring light overlay - covers corners and creates rounded inner edge // When ring light is off, still show black corners to maintain rounded appearance RingLightOverlay( - color: isRingLightEnabled ? ringLightColor : .black, - width: isRingLightEnabled ? ringLightSize : Design.CornerRadius.large, - opacity: isRingLightEnabled ? ringLightOpacity : 1.0, + color: cameraSettings.isRingLightEnabled ? cameraSettings.lightColor : .black, + width: cameraSettings.isRingLightEnabled ? cameraSettings.ringSize : Design.CornerRadius.large, + opacity: cameraSettings.isRingLightEnabled ? cameraSettings.ringLightOpacity : 1.0, cornerRadius: Design.CornerRadius.large ) .allowsHitTesting(false) // Allow touches to pass through to camera view @@ -119,31 +89,31 @@ struct CustomCameraScreen: MCameraScreen { isExpanded: $isControlsExpanded, hasActiveSettings: hasActiveSettings, activeSettingsIcons: activeSettingsIcons, - flashMode: flashMode, + flashMode: cameraSettings.flashMode, flashIcon: flashIcon, onFlashTap: toggleFlash, - isFlashSyncedWithRingLight: isFlashSyncedWithRingLight, + isFlashSyncedWithRingLight: cameraSettings.isFlashSyncedWithRingLight, onFlashSyncTap: toggleFlashSync, hdrMode: cameraSettings.hdrMode, hdrIcon: hdrIcon, onHDRTap: toggleHDR, - isGridVisible: isGridVisible, + isGridVisible: cameraSettings.isGridVisible, gridIcon: gridIcon, onGridTap: toggleGrid, - photoQuality: photoQuality, + photoQuality: cameraSettings.photoQuality, onQualityTap: cycleQuality, isCenterStageAvailable: isCenterStageAvailable, isCenterStageEnabled: isCenterStageEnabled, onCenterStageTap: toggleCenterStage, isFrontCamera: cameraPosition == .front, onFlipCameraTap: flipCamera, - isRingLightEnabled: isRingLightEnabled, + isRingLightEnabled: cameraSettings.isRingLightEnabled, onRingLightTap: toggleRingLight, - ringLightColor: ringLightColor, + ringLightColor: cameraSettings.lightColor, onRingLightColorTap: toggleRingLightColorPicker, - ringLightSize: ringLightSize, + ringLightSize: cameraSettings.ringSize, onRingLightSizeTap: toggleRingLightSizeSlider, - ringLightOpacity: ringLightOpacity, + ringLightOpacity: cameraSettings.ringLightOpacity, onRingLightOpacityTap: toggleRingLightOpacitySlider ) .padding(.horizontal, Design.Spacing.large) @@ -169,31 +139,31 @@ struct CustomCameraScreen: MCameraScreen { isExpanded: $isControlsExpanded, hasActiveSettings: hasActiveSettings, activeSettingsIcons: activeSettingsIcons, - flashMode: flashMode, + flashMode: cameraSettings.flashMode, flashIcon: flashIcon, onFlashTap: toggleFlash, - isFlashSyncedWithRingLight: isFlashSyncedWithRingLight, + isFlashSyncedWithRingLight: cameraSettings.isFlashSyncedWithRingLight, onFlashSyncTap: toggleFlashSync, hdrMode: cameraSettings.hdrMode, hdrIcon: hdrIcon, onHDRTap: toggleHDR, - isGridVisible: isGridVisible, + isGridVisible: cameraSettings.isGridVisible, gridIcon: gridIcon, onGridTap: toggleGrid, - photoQuality: photoQuality, + photoQuality: cameraSettings.photoQuality, onQualityTap: cycleQuality, isCenterStageAvailable: isCenterStageAvailable, isCenterStageEnabled: isCenterStageEnabled, onCenterStageTap: toggleCenterStage, isFrontCamera: cameraPosition == .front, onFlipCameraTap: flipCamera, - isRingLightEnabled: isRingLightEnabled, + isRingLightEnabled: cameraSettings.isRingLightEnabled, onRingLightTap: toggleRingLight, - ringLightColor: ringLightColor, + ringLightColor: cameraSettings.lightColor, onRingLightColorTap: toggleRingLightColorPicker, - ringLightSize: ringLightSize, + ringLightSize: cameraSettings.ringSize, onRingLightSizeTap: toggleRingLightSizeSlider, - ringLightOpacity: ringLightOpacity, + ringLightOpacity: cameraSettings.ringLightOpacity, onRingLightOpacityTap: toggleRingLightOpacitySlider ) .padding(.horizontal, Design.Spacing.large) @@ -222,8 +192,8 @@ struct CustomCameraScreen: MCameraScreen { if showRingLightColorPicker { ColorPickerOverlay( selectedColor: Binding( - get: { cameraSettings.ringLightColor }, - set: { cameraSettings.ringLightColor = $0 } + get: { cameraSettings.selectedLightColor.color }, + set: { cameraSettings.selectedLightColor = RingLightColor.custom(with: $0) } ), isPresented: $showRingLightColorPicker ) @@ -234,8 +204,8 @@ struct CustomCameraScreen: MCameraScreen { if showRingLightSizeSlider { SizeSliderOverlay( selectedSize: Binding( - get: { cameraSettings.ringLightSize }, - set: { cameraSettings.ringLightSize = $0 } + get: { cameraSettings.ringSize }, + set: { cameraSettings.ringSize = $0 } ), isPresented: $showRingLightSizeSlider ) @@ -290,7 +260,7 @@ struct CustomCameraScreen: MCameraScreen { // Initialize zoom gesture state lastMagnification = zoomFactor } - .onChange(of: isFlashSyncedWithRingLight) { _, _ in + .onChange(of: cameraSettings.isFlashSyncedWithRingLight) { _, _ in // Only update when sync setting changes, not on every color change updateFlashSyncState() } @@ -309,22 +279,22 @@ struct CustomCameraScreen: MCameraScreen { /// Returns true if any setting is in a non-default state private var hasActiveSettings: Bool { - flashMode != .off || cameraSettings.hdrMode != .off || isGridVisible || isCenterStageEnabled || isRingLightEnabled + cameraSettings.flashMode != .off || cameraSettings.hdrMode != .off || cameraSettings.isGridVisible || isCenterStageEnabled || cameraSettings.isRingLightEnabled } /// Returns icons for currently active settings (for collapsed pill display) private var activeSettingsIcons: [String] { var icons: [String] = [] - if flashMode != .off { + if cameraSettings.flashMode != .off { icons.append(flashIcon) } if cameraSettings.hdrMode != .off { icons.append(hdrIcon) } - if isGridVisible { + if cameraSettings.isGridVisible { icons.append(gridIcon) } - if isRingLightEnabled { + if cameraSettings.isRingLightEnabled { icons.append("circle.fill") } if isCenterStageEnabled { @@ -335,7 +305,7 @@ struct CustomCameraScreen: MCameraScreen { // MARK: - Control Icons private var flashIcon: String { - flashMode.icon + cameraSettings.flashMode.icon } private var hdrIcon: String { @@ -347,7 +317,7 @@ struct CustomCameraScreen: MCameraScreen { } private var gridIcon: String { - isGridVisible ? "grid" : "grid" + cameraSettings.isGridVisible ? "grid" : "grid" } private var isCenterStageAvailable: Bool { @@ -360,18 +330,18 @@ struct CustomCameraScreen: MCameraScreen { // MARK: - Actions private func toggleFlash() { let nextMode: CameraFlashMode - switch flashMode { + switch cameraSettings.flashMode { case .off: nextMode = .auto case .auto: nextMode = .on case .on: nextMode = .off } - flashMode = nextMode + cameraSettings.flashMode = nextMode // Update MijickCamera's flash mode so it knows to use iOS Retina Flash setFlashMode(nextMode.toMijickFlashMode) } private func toggleFlashSync() { - isFlashSyncedWithRingLight.toggle() + cameraSettings.isFlashSyncedWithRingLight.toggle() } private func toggleHDR() { @@ -392,7 +362,8 @@ struct CustomCameraScreen: MCameraScreen { } private func toggleGrid() { - setGridVisibility(!isGridVisible) + cameraSettings.isGridVisible.toggle() + setGridVisibility(cameraSettings.isGridVisible) } private func flipCamera() { @@ -400,6 +371,7 @@ struct CustomCameraScreen: MCameraScreen { do { let newPosition: CameraPosition = (cameraPosition == .front) ? .back : .front try await setCameraPosition(newPosition) + cameraSettings.cameraPosition = newPosition } catch { print("Failed to flip camera: \(error)") } @@ -442,34 +414,34 @@ struct CustomCameraScreen: MCameraScreen { private func cycleQuality() { let allCases = PhotoQuality.allCases - let currentIndex = allCases.firstIndex(of: photoQuality) ?? 0 + let currentIndex = allCases.firstIndex(of: cameraSettings.photoQuality) ?? 0 let nextIndex = (currentIndex + 1) % allCases.count - photoQuality = allCases[nextIndex] + cameraSettings.photoQuality = allCases[nextIndex] } private func toggleRingLight() { - isRingLightEnabled.toggle() + cameraSettings.isRingLightEnabled.toggle() } private func updateFlashSyncState() { // Tell MijickCamera whether we're handling flash ourselves (sync enabled) // or if iOS should handle it (sync disabled) // We use .white as a placeholder - the actual color comes from ringLightColor in SwiftUI - if isFlashSyncedWithRingLight { + if cameraSettings.isFlashSyncedWithRingLight { setScreenFlashColor(.white) // Non-nil = we handle flash, disable iOS flash } else { setScreenFlashColor(nil) // nil = let iOS handle Retina Flash } } - + /// The color to use for screen flash overlay private var screenFlashColor: Color { - isFlashSyncedWithRingLight ? ringLightColor : .white + cameraSettings.isFlashSyncedWithRingLight ? cameraSettings.lightColor : .white } - + /// Whether to use custom screen flash (front camera + flash on + sync enabled) private var shouldUseCustomScreenFlash: Bool { - cameraPosition == .front && flashMode != .off && isFlashSyncedWithRingLight + cameraPosition == .front && cameraSettings.flashMode != .off && cameraSettings.isFlashSyncedWithRingLight } /// Performs capture with screen flash if needed