SelfieCam/SelfieCam/Features/Camera/ContentView.swift

195 lines
7.1 KiB
Swift

import SwiftUI
import MijickCamera
import Bedrock
struct ContentView: View {
@State private var settings = SettingsViewModel()
@State private var premiumManager = PremiumManager()
@State private var showSettings = false
@State private var showPaywall = false
@State private var capturedPhoto: CapturedPhoto?
@State private var showPhotoReview = false
@State private var isSavingPhoto = false
@State private var saveError: String?
/// 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
@State private var cameraSessionKey = UUID()
var body: some View {
ZStack {
// Camera view - wrapped in EquatableView to prevent re-evaluation on settings changes
if !showPhotoReview {
EquatableView(content: CameraContainerView(
settings: settings,
sessionKey: cameraSessionKey,
onImageCaptured: { image in
handlePhotoCaptured(image)
}
))
.ignoresSafeArea() // Only camera ignores safe area to fill screen
}
// Photo review overlay
if showPhotoReview, let photo = capturedPhoto {
PhotoReviewView(
photo: photo,
isSaving: isSavingPhoto,
saveError: saveError,
onRetake: {
resetCameraForNextCapture()
},
onSave: {
savePhotoToLibrary(photo.image)
}
)
.transition(.opacity)
.ignoresSafeArea() // Photo review also fills screen
}
}
// Settings button overlay - respects safe area naturally
.overlay(alignment: .topTrailing) {
Button {
showSettings = true
} label: {
Image(systemName: "gearshape.fill")
.font(.title3)
.foregroundStyle(.white)
.padding(Design.Spacing.medium)
.background(.ultraThinMaterial, in: Circle())
.shadow(radius: Design.Shadow.radiusSmall)
}
.accessibilityLabel("Settings")
.padding(.horizontal, Design.Spacing.large)
.padding(.top, Design.Spacing.small)
}
.animation(.easeInOut(duration: Design.Animation.quick), value: showPhotoReview)
.onChange(of: settings.isRingLightEnabled) { _, newValue in
if settings.isFlashSyncedWithRingLight {
settings.flashMode = newValue ? .on : .off
}
}
.onChange(of: settings.isFlashSyncedWithRingLight) { _, newValue in
if newValue {
settings.flashMode = settings.isRingLightEnabled ? .on : .off
}
}
.sheet(isPresented: $showSettings) {
SettingsView(viewModel: settings, showPaywall: $showPaywall)
}
.sheet(isPresented: $showPaywall) {
ProPaywallView()
}
}
/// Handles photo capture - either auto-saves or shows review screen
private func handlePhotoCaptured(_ image: UIImage) {
if settings.isAutoSaveEnabled {
// Auto-save enabled: save immediately without showing review screen
Task {
// Small delay to ensure shutter sound plays before saving
try? await Task.sleep(for: .milliseconds(200))
let quality = settings.photoQuality
let result = await PhotoLibraryService.savePhotoToLibrary(image, quality: quality)
switch result {
case .success:
Design.debugLog("Photo auto-saved successfully")
// Don't reset camera session for auto-save to avoid timing issues
case .failure(let error):
Design.debugLog("Failed to auto-save photo: \(error)")
// Don't reset on failure either
}
}
} else {
// Auto-save disabled: show review screen
capturedPhoto = CapturedPhoto(image: image, timestamp: Date())
showPhotoReview = true
isSavingPhoto = false
saveError = nil
}
}
/// Resets state and regenerates camera session key to create a fresh camera instance
private func resetCameraForNextCapture() {
capturedPhoto = nil
showPhotoReview = false
isSavingPhoto = false
saveError = nil
// Generate new key to force MCamera recreation with fresh AVCapturePhotoOutput
cameraSessionKey = UUID()
}
private func savePhotoToLibrary(_ image: UIImage) {
isSavingPhoto = true
saveError = nil
let quality = settings.photoQuality
Task {
let result = await PhotoLibraryService.savePhotoToLibrary(image, quality: quality)
await MainActor.run {
self.isSavingPhoto = false
switch result {
case .success:
Design.debugLog("Photo saved successfully")
// Auto-dismiss after successful save and reset camera for next capture
Task {
try? await Task.sleep(for: .seconds(1.5))
await MainActor.run {
self.resetCameraForNextCapture()
}
}
case .failure(let error):
Design.debugLog("Failed to save photo: \(error)")
self.saveError = error.localizedDescription
}
}
}
}
}
// MARK: - Camera Container View
/// Wrapper view for MCamera - only recreates when sessionKey changes
struct CameraContainerView: View, Equatable {
let settings: SettingsViewModel
let sessionKey: UUID
let onImageCaptured: (UIImage) -> Void
// Only compare sessionKey for equality - ignore settings and callback changes
static func == (lhs: CameraContainerView, rhs: CameraContainerView) -> Bool {
lhs.sessionKey == rhs.sessionKey
}
var body: some View {
let _ = Design.debugLog("CameraContainerView body evaluated - sessionKey: \(sessionKey)")
MCamera()
.setCameraScreen { cameraManager, namespace, closeAction in
CustomCameraScreen(
cameraManager: cameraManager,
namespace: namespace,
closeMCameraAction: closeAction,
cameraSettings: settings,
onPhotoCaptured: onImageCaptured
)
}
.setCapturedMediaScreen(nil)
// Use front camera as default for selfie app - don't read from settings here
// to avoid triggering @Observable tracking which breaks the camera
.setCameraPosition(.front)
.startSession()
}
}
#Preview {
ContentView()
}