Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>

This commit is contained in:
Matt Bruce 2026-01-04 11:34:49 -06:00
parent 915088f180
commit eedd709ef5
2 changed files with 51 additions and 137 deletions

View File

@ -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()
}
}

View File

@ -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