From 69bf3c27afb5932b42577a0320b2657131727ec0 Mon Sep 17 00:00:00 2001 From: Wavesonics Date: Sat, 25 Oct 2025 23:00:50 -0700 Subject: [PATCH 1/5] Moved everything to be normal nav destinations --- SnapSafe/Screens/AppNavigation.swift | 2 + .../Screens/Camera/CameraContainerView.swift | 4 +- SnapSafe/Screens/ContentView.swift | 26 +++++-- .../Screens/Gallery/SecureGalleryView.swift | 55 ++++++--------- .../PhotoDetail/EnhancedPhotoDetailView.swift | 2 +- .../PhotoObfuscationView.swift | 68 +++++++++---------- SnapSafe/Screens/Settings/SettingsView.swift | 4 +- 7 files changed, 81 insertions(+), 80 deletions(-) diff --git a/SnapSafe/Screens/AppNavigation.swift b/SnapSafe/Screens/AppNavigation.swift index 8c17821..0911cee 100644 --- a/SnapSafe/Screens/AppNavigation.swift +++ b/SnapSafe/Screens/AppNavigation.swift @@ -16,6 +16,7 @@ enum AppDestination: Hashable { case pinSetup case pinVerification case camera + case photoDetail(allPhotos: [PhotoDef], initialIndex: Int) case photoObfuscation(PhotoDef) case poisonPillSetupWizard } @@ -85,6 +86,7 @@ extension AppDestination: Identifiable { case .pinSetup: return "pinSetup" case .pinVerification: return "pinVerification" case .camera: return "camera" + case .photoDetail(_, let initialIndex): return "photoDetail_\(initialIndex)" case .photoObfuscation(let photoDef): return "photoObfuscation_\(photoDef.photoName)" case .poisonPillSetupWizard: return "poisonPillSetupWizard" } diff --git a/SnapSafe/Screens/Camera/CameraContainerView.swift b/SnapSafe/Screens/Camera/CameraContainerView.swift index 953f54b..c2b8f30 100644 --- a/SnapSafe/Screens/Camera/CameraContainerView.swift +++ b/SnapSafe/Screens/Camera/CameraContainerView.swift @@ -128,7 +128,7 @@ struct CameraContainerView: View { HStack { Button(action: { - nav.presentFullScreenCover(.gallery) + nav.navigate(to:.gallery) }) { ZStack { Image(systemName: "photo.on.rectangle") @@ -176,7 +176,7 @@ struct CameraContainerView: View { Spacer() Button(action: { - nav.presentSheet(.settings) + nav.navigate(to:.settings) }) { Image(systemName: "gear") .font(.system(size: 24)) diff --git a/SnapSafe/Screens/ContentView.swift b/SnapSafe/Screens/ContentView.swift index a956c8e..3388b8e 100644 --- a/SnapSafe/Screens/ContentView.swift +++ b/SnapSafe/Screens/ContentView.swift @@ -31,16 +31,16 @@ struct ContentView: View { .navigationBarHidden(true) .navigationDestination(for: AppDestination.self) { destination in navigationDestinationView(for: destination) - .navigationBarHidden(destination != .gallery) + .navigationBarHidden(shouldHideNavigationBar(for: destination)) } } + .securityManaged() .sheet(item: $nav.presentedSheet) { destination in navigationDestinationView(for: destination) } .fullScreenCover(item: $nav.presentedFullScreenCover) { destination in navigationDestinationView(for: destination) } - .securityManaged() .onAppear { viewModel.onAppear() navigateToRootDestination() @@ -78,8 +78,19 @@ struct ContentView: View { } } + // MARK: - Navigation Helper Methods + + private func shouldHideNavigationBar(for destination: AppDestination) -> Bool { + switch destination { + case .gallery, .photoObfuscation, .settings: + return false + default: + return true + } + } + // MARK: - Navigation Destination Views - + @ViewBuilder private func navigationDestinationView(for destination: AppDestination) -> some View { switch destination { @@ -87,7 +98,7 @@ struct ContentView: View { SettingsView() case .gallery: SecureGalleryView(onDismiss: { - nav.dismissFullScreenCover() + nav.navigateBack() }) case .pinSetup: PINSetupIntroView() @@ -95,6 +106,13 @@ struct ContentView: View { PINVerificationView() case .camera: CameraContainerView() + case .photoDetail(let allPhotos, let initialIndex): + EnhancedPhotoDetailView( + allPhotos: allPhotos, + initialIndex: initialIndex, + onDelete: nil, + onDismiss: nil + ) case .photoObfuscation(let photoDef): PhotoObfuscationView(photoDef: photoDef, navigator: nav) case .poisonPillSetupWizard: diff --git a/SnapSafe/Screens/Gallery/SecureGalleryView.swift b/SnapSafe/Screens/Gallery/SecureGalleryView.swift index 4428726..9c9b896 100644 --- a/SnapSafe/Screens/Gallery/SecureGalleryView.swift +++ b/SnapSafe/Screens/Gallery/SecureGalleryView.swift @@ -29,7 +29,8 @@ struct SecureGalleryView: View { @AppStorage("showFaceDetection") private var showFaceDetection = true // Using AppStorage to share with Settings @StateObject private var viewModel: SecureGalleryViewModel @Environment(\.dismiss) private var dismiss - + @EnvironmentObject private var nav: AppNavigationState + // Callback for dismissing the gallery let onDismiss: (() -> Void)? @@ -47,8 +48,7 @@ struct SecureGalleryView: View { var body: some View { - NavigationStack { - ZStack { + ZStack { Group { if viewModel.photos.isEmpty { EmptyGalleryView(onDismiss: { @@ -83,21 +83,22 @@ struct SecureGalleryView: View { .navigationTitle(viewModel.navigationTitle) .navigationBarTitleDisplayMode(.inline) .toolbar { - // Back button in the leading position - ToolbarItem(placement: .navigationBarLeading) { - Button(action: { - if viewModel.isSelectingDecoys { + // Back button in the leading position (only for decoy selection mode) + if viewModel.isSelectingDecoys { + ToolbarItem(placement: .navigationBarLeading) { + Button(action: { viewModel.exitDecoyMode() - } - onDismiss?() - dismiss() - }) { - HStack { - Image(systemName: "chevron.left") + onDismiss?() + dismiss() + }) { + HStack { + Image(systemName: "chevron.left") + Text("Back") + } } } } - + // Action buttons in the trailing position (simplified for top toolbar) ToolbarItem(placement: .navigationBarTrailing) { HStack(spacing: 16) { @@ -192,30 +193,15 @@ struct SecureGalleryView: View { } .onAppear(perform: viewModel.onAppear) .onChange(of: viewModel.selectedPhoto) { _, newValue in - viewModel.onSelectedPhotoChange(newValue) - } - .fullScreenCover(item: $viewModel.selectedPhoto) { photoDef in + if let photoDef = newValue { // Find the index of the selected photo in the photos array if let initialIndex = viewModel.photos.firstIndex(where: { $0.photoName == photoDef.photoName }) { - EnhancedPhotoDetailView( - allPhotos: viewModel.photos, - initialIndex: initialIndex, - onDelete: { _ in viewModel.onAppear() }, - onDismiss: { - viewModel.clearMemoryForAllPhotos() - } - ) - } else { - // Fallback if photo not found in array - PhotoDetailView( - photo: photoDef, - onDelete: { _ in viewModel.onAppear() }, - onDismiss: { - viewModel.clearMemoryForPhoto(photoDef) - } - ) + nav.navigate(to: .photoDetail(allPhotos: viewModel.photos, initialIndex: initialIndex)) } + // Reset selectedPhoto so it can be selected again + viewModel.selectedPhoto = nil } + } .alert( viewModel.deleteAlertTitle, isPresented: $viewModel.showDeleteConfirmation, @@ -256,7 +242,6 @@ struct SecureGalleryView: View { } ) } - } // Photo grid subview private var photosGridView: some View { diff --git a/SnapSafe/Screens/PhotoDetail/EnhancedPhotoDetailView.swift b/SnapSafe/Screens/PhotoDetail/EnhancedPhotoDetailView.swift index 983f43f..dd1efac 100644 --- a/SnapSafe/Screens/PhotoDetail/EnhancedPhotoDetailView.swift +++ b/SnapSafe/Screens/PhotoDetail/EnhancedPhotoDetailView.swift @@ -112,7 +112,7 @@ struct EnhancedPhotoDetailView: View { onInfo: { viewModel.showImageInfo = true }, onObfuscate: { if let current = viewModel.currentPhotoDef { - nav.presentedFullScreenCover = .photoObfuscation(current) + nav.navigate(to: .photoObfuscation(current)) } }, onShare: { viewModel.shareCurrentPhoto() }, diff --git a/SnapSafe/Screens/PhotoObfuscation/PhotoObfuscationView.swift b/SnapSafe/Screens/PhotoObfuscation/PhotoObfuscationView.swift index 488018d..8218e92 100644 --- a/SnapSafe/Screens/PhotoObfuscation/PhotoObfuscationView.swift +++ b/SnapSafe/Screens/PhotoObfuscation/PhotoObfuscationView.swift @@ -15,53 +15,51 @@ struct PhotoObfuscationView: View { _viewModel = StateObject(wrappedValue: PhotoObfuscationViewModel( photoDef: photoDef, - onSave: { _ in navigator.presentedFullScreenCover = nil }, - onDismiss: { navigator.presentedFullScreenCover = nil } + onSave: { _ in navigator.navigateBack() }, + onDismiss: { navigator.navigateBack() } ) ) } - + private func onDismiss() { - nav.presentedFullScreenCover = nil + nav.navigateBack() } var body: some View { - NavigationView { - ZStack { - Color.black - .ignoresSafeArea() - - if viewModel.isImageLoading { - ProgressView("Loading image...") - .progressViewStyle(CircularProgressViewStyle(tint: .white)) - .foregroundColor(.white) - } else { - imageContent - } - } - .navigationTitle("Photo Obfuscation") - .navigationBarTitleDisplayMode(.inline) - .toolbar { - ToolbarItem(placement: .navigationBarLeading) { - Button("Cancel") { - viewModel.cancel() - onDismiss() - } + ZStack { + Color.black + .ignoresSafeArea() + + if viewModel.isImageLoading { + ProgressView("Loading image...") + .progressViewStyle(CircularProgressViewStyle(tint: .white)) .foregroundColor(.white) + } else { + imageContent + } + } + .navigationTitle("Photo Obfuscation") + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .navigationBarLeading) { + Button("Cancel") { + viewModel.cancel() + onDismiss() } - - ToolbarItem(placement: .navigationBarTrailing) { - Button("Save") { - viewModel.saveChanges() - onDismiss() - } - .foregroundColor(.blue) - .fontWeight(.semibold) + .foregroundColor(.white) + } + + ToolbarItem(placement: .navigationBarTrailing) { + Button("Save") { + viewModel.saveChanges() + onDismiss() } + .foregroundColor(.blue) + .fontWeight(.semibold) } - .toolbarBackground(Color.black, for: .navigationBar) - .toolbarBackground(.visible, for: .navigationBar) } + .toolbarBackground(Color.black, for: .navigationBar) + .toolbarBackground(.visible, for: .navigationBar) .alert("Obscure Faces", isPresented: $viewModel.showObscureConfirmation) { Button("Cancel", role: .cancel) { } Button(viewModel.maskActionTitle) { diff --git a/SnapSafe/Screens/Settings/SettingsView.swift b/SnapSafe/Screens/Settings/SettingsView.swift index 450e2fa..3ccbeb7 100644 --- a/SnapSafe/Screens/Settings/SettingsView.swift +++ b/SnapSafe/Screens/Settings/SettingsView.swift @@ -26,8 +26,7 @@ struct SettingsView: View { @EnvironmentObject private var nav: AppNavigationState var body: some View { - NavigationView { - List { + List { // ABOUT SECTION Section { @@ -209,6 +208,5 @@ struct SettingsView: View { }) } } - } } } From 5b1756f9def2b2022365be7f9a2e44351550fa45 Mon Sep 17 00:00:00 2001 From: Bill Booth Date: Thu, 30 Oct 2025 00:28:01 -0700 Subject: [PATCH 2/5] Submit for review to false We need to set this for one release to change the tagline and make sure the language is set properly. --- fastlane/Fastfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 59250ae..2124c6f 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -117,7 +117,7 @@ platform :ios do upload_to_app_store( skip_screenshots: true, skip_metadata: true, - submit_for_review: true, + submit_for_review: false, force: false, precheck_include_in_app_purchases: false ) From 7d7cfca3181dc3f282ee4c8ee38532674e630231 Mon Sep 17 00:00:00 2001 From: Bill Booth Date: Thu, 30 Oct 2025 00:37:22 -0700 Subject: [PATCH 3/5] Shift securitymanaged downward to hide settings The ordering of the .securitymanaged modifier matters because the setup screen is presented differently. This hides the settings screen now. Still not fixed: hiding the info screen for a photo --- SnapSafe/Screens/ContentView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SnapSafe/Screens/ContentView.swift b/SnapSafe/Screens/ContentView.swift index 3388b8e..87d1e7b 100644 --- a/SnapSafe/Screens/ContentView.swift +++ b/SnapSafe/Screens/ContentView.swift @@ -34,13 +34,13 @@ struct ContentView: View { .navigationBarHidden(shouldHideNavigationBar(for: destination)) } } - .securityManaged() .sheet(item: $nav.presentedSheet) { destination in navigationDestinationView(for: destination) } .fullScreenCover(item: $nav.presentedFullScreenCover) { destination in navigationDestinationView(for: destination) } + .securityManaged() .onAppear { viewModel.onAppear() navigateToRootDestination() From ded6f0cf559867a5e61074af718f907f201d9057 Mon Sep 17 00:00:00 2001 From: Bill Booth Date: Thu, 30 Oct 2025 00:59:08 -0700 Subject: [PATCH 4/5] Add info sheet into the main nav stack This page wasn't inside the new nav stack we're making here. Adding it in now. The reason we have to use .securitymanaged() twice more here is sheets have their own view hierarchy so we must be setting this at their root. Everything's in ContentView now so we have one central place/view to manage where the security overlay is called. --- SnapSafe/Screens/AppNavigation.swift | 2 ++ SnapSafe/Screens/ContentView.swift | 4 ++++ .../Screens/PhotoDetail/EnhancedPhotoDetailView.swift | 11 +++++------ 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/SnapSafe/Screens/AppNavigation.swift b/SnapSafe/Screens/AppNavigation.swift index 0911cee..e3c0cb4 100644 --- a/SnapSafe/Screens/AppNavigation.swift +++ b/SnapSafe/Screens/AppNavigation.swift @@ -17,6 +17,7 @@ enum AppDestination: Hashable { case pinVerification case camera case photoDetail(allPhotos: [PhotoDef], initialIndex: Int) + case photoInfo(PhotoDef) case photoObfuscation(PhotoDef) case poisonPillSetupWizard } @@ -87,6 +88,7 @@ extension AppDestination: Identifiable { case .pinVerification: return "pinVerification" case .camera: return "camera" case .photoDetail(_, let initialIndex): return "photoDetail_\(initialIndex)" + case .photoInfo(let photoDef): return "photoInfo_\(photoDef.photoName)" case .photoObfuscation(let photoDef): return "photoObfuscation_\(photoDef.photoName)" case .poisonPillSetupWizard: return "poisonPillSetupWizard" } diff --git a/SnapSafe/Screens/ContentView.swift b/SnapSafe/Screens/ContentView.swift index 87d1e7b..aa44e3f 100644 --- a/SnapSafe/Screens/ContentView.swift +++ b/SnapSafe/Screens/ContentView.swift @@ -36,9 +36,11 @@ struct ContentView: View { } .sheet(item: $nav.presentedSheet) { destination in navigationDestinationView(for: destination) + .securityManaged() } .fullScreenCover(item: $nav.presentedFullScreenCover) { destination in navigationDestinationView(for: destination) + .securityManaged() } .securityManaged() .onAppear { @@ -113,6 +115,8 @@ struct ContentView: View { onDelete: nil, onDismiss: nil ) + case .photoInfo(let photoDef): + ImageInfoView(photoDef: photoDef) case .photoObfuscation(let photoDef): PhotoObfuscationView(photoDef: photoDef, navigator: nav) case .poisonPillSetupWizard: diff --git a/SnapSafe/Screens/PhotoDetail/EnhancedPhotoDetailView.swift b/SnapSafe/Screens/PhotoDetail/EnhancedPhotoDetailView.swift index dd1efac..d6b9a8a 100644 --- a/SnapSafe/Screens/PhotoDetail/EnhancedPhotoDetailView.swift +++ b/SnapSafe/Screens/PhotoDetail/EnhancedPhotoDetailView.swift @@ -109,7 +109,11 @@ struct EnhancedPhotoDetailView: View { Spacer() if viewModel.currentIndex < viewModel.photoFiles.count { PhotoControlsView( - onInfo: { viewModel.showImageInfo = true }, + onInfo: { + if let current = viewModel.currentPhotoDef { + nav.presentSheet(.photoInfo(current)) + } + }, onObfuscate: { if let current = viewModel.currentPhotoDef { nav.navigate(to: .photoObfuscation(current)) @@ -173,10 +177,5 @@ struct EnhancedPhotoDetailView: View { Text("Are you sure you want to delete this photo? This action cannot be undone.") } ) - .sheet(isPresented: $viewModel.showImageInfo) { - if let photoDef = viewModel.currentPhotoDef { - ImageInfoView(photoDef: photoDef) - } - } } } From a96af198bb4daf45d70845ba868398eee2b31822 Mon Sep 17 00:00:00 2001 From: Bill Booth Date: Thu, 30 Oct 2025 01:08:59 -0700 Subject: [PATCH 5/5] Fix frozen camera background bug This should fix the issue where you have camera view open, background, then on foreground it is frozen with the image on the screen immediately before it was minimized. --- SnapSafe/Screens/Camera/CameraViewModel.swift | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/SnapSafe/Screens/Camera/CameraViewModel.swift b/SnapSafe/Screens/Camera/CameraViewModel.swift index 454935f..fa35ced 100644 --- a/SnapSafe/Screens/Camera/CameraViewModel.swift +++ b/SnapSafe/Screens/Camera/CameraViewModel.swift @@ -119,6 +119,12 @@ class CameraViewModel: NSObject, ObservableObject { name: UIApplication.willEnterForegroundNotification, object: nil ) + NotificationCenter.default.addObserver( + self, + selector: #selector(handleAppWillResignActive), + name: UIApplication.willResignActiveNotification, + object: nil + ) } deinit { @@ -132,6 +138,11 @@ class CameraViewModel: NSObject, ObservableObject { resetZoomLevel() } + @objc private func handleAppWillResignActive() { + Logger.camera.info("App will resign active, stopping camera") + stopCameraSession() + } + func restartCameraSessionIfNeeded() { let session = self.session if !session.isRunning { @@ -141,6 +152,16 @@ class CameraViewModel: NSObject, ObservableObject { } } } + + func stopCameraSession() { + let session = self.session + if session.isRunning { + Logger.camera.info("Stopping camera session") + DispatchQueue.global(qos: .userInitiated).async { + session.stopRunning() + } + } + } func checkAndSetupCamera() async {