diff --git a/ios/Ascently.xcodeproj/project.pbxproj b/ios/Ascently.xcodeproj/project.pbxproj index 5169b71..dccd08b 100644 --- a/ios/Ascently.xcodeproj/project.pbxproj +++ b/ios/Ascently.xcodeproj/project.pbxproj @@ -465,7 +465,7 @@ CODE_SIGN_ENTITLEMENTS = Ascently/Ascently.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 37; + CURRENT_PROJECT_VERSION = 38; DEVELOPMENT_TEAM = 4BC9Y2LL4B; DRIVERKIT_DEPLOYMENT_TARGET = 24.6; ENABLE_PREVIEWS = YES; @@ -487,7 +487,7 @@ "@executable_path/Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 15.6; - MARKETING_VERSION = 2.5.0; + MARKETING_VERSION = 2.5.1; PRODUCT_BUNDLE_IDENTIFIER = com.atridad.Ascently; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -513,7 +513,7 @@ CODE_SIGN_ENTITLEMENTS = Ascently/Ascently.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 37; + CURRENT_PROJECT_VERSION = 38; DEVELOPMENT_TEAM = 4BC9Y2LL4B; DRIVERKIT_DEPLOYMENT_TARGET = 24.6; ENABLE_PREVIEWS = YES; @@ -535,7 +535,7 @@ "@executable_path/Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 15.6; - MARKETING_VERSION = 2.5.0; + MARKETING_VERSION = 2.5.1; PRODUCT_BUNDLE_IDENTIFIER = com.atridad.Ascently; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -602,7 +602,7 @@ ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; CODE_SIGN_ENTITLEMENTS = SessionStatusLiveExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 37; + CURRENT_PROJECT_VERSION = 38; DEVELOPMENT_TEAM = 4BC9Y2LL4B; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = SessionStatusLive/Info.plist; @@ -613,7 +613,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.5.0; + MARKETING_VERSION = 2.5.1; PRODUCT_BUNDLE_IDENTIFIER = com.atridad.Ascently.SessionStatusLive; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; @@ -632,7 +632,7 @@ ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; CODE_SIGN_ENTITLEMENTS = SessionStatusLiveExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 37; + CURRENT_PROJECT_VERSION = 38; DEVELOPMENT_TEAM = 4BC9Y2LL4B; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = SessionStatusLive/Info.plist; @@ -643,7 +643,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.5.0; + MARKETING_VERSION = 2.5.1; PRODUCT_BUNDLE_IDENTIFIER = com.atridad.Ascently.SessionStatusLive; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; diff --git a/ios/Ascently.xcodeproj/project.xcworkspace/xcuserdata/atridad.xcuserdatad/UserInterfaceState.xcuserstate b/ios/Ascently.xcodeproj/project.xcworkspace/xcuserdata/atridad.xcuserdatad/UserInterfaceState.xcuserstate index b8fa1db..eedb83d 100644 Binary files a/ios/Ascently.xcodeproj/project.xcworkspace/xcuserdata/atridad.xcuserdatad/UserInterfaceState.xcuserstate and b/ios/Ascently.xcodeproj/project.xcworkspace/xcuserdata/atridad.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/ios/Ascently/Components/CameraImagePicker.swift b/ios/Ascently/Components/CameraImagePicker.swift index 93008f7..d40cb4d 100644 --- a/ios/Ascently/Components/CameraImagePicker.swift +++ b/ios/Ascently/Components/CameraImagePicker.swift @@ -1,52 +1,104 @@ import SwiftUI import UIKit +/// A native iOS camera picker presented from a hosting controller so it can +/// respect all supported interface orientations. Present with `.fullScreenCover()`. struct CameraImagePicker: UIViewControllerRepresentable { - @Binding var isPresented: Bool + @Environment(\.dismiss) private var dismiss let onImageCaptured: (UIImage) -> Void - func makeUIViewController(context: Context) -> UIImagePickerController { - let picker = UIImagePickerController() - picker.delegate = context.coordinator - picker.sourceType = .camera - picker.cameraCaptureMode = .photo - picker.cameraDevice = .rear - picker.allowsEditing = false - return picker + func makeUIViewController(context: Context) -> CameraHostViewController { + let host = CameraHostViewController() + host.onImageCaptured = { image in + onImageCaptured(image) + dismiss() + } + host.onCancel = { + dismiss() + } + host.pickerDelegate = context.coordinator + context.coordinator.onImageCaptured = { image in + onImageCaptured(image) + dismiss() + } + context.coordinator.onCancel = { + dismiss() + } + return host } - func updateUIViewController(_ uiViewController: UIImagePickerController, context: Context) { - // Nothing here actually... Q_Q + func updateUIViewController(_ uiViewController: CameraHostViewController, context: Context) { + // No dynamic updates needed } func makeCoordinator() -> Coordinator { - Coordinator(self) + Coordinator() } class Coordinator: NSObject, UIImagePickerControllerDelegate, UINavigationControllerDelegate { - let parent: CameraImagePicker - - init(_ parent: CameraImagePicker) { - self.parent = parent - } + var onImageCaptured: ((UIImage) -> Void)? + var onCancel: (() -> Void)? func imagePickerController( _ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any] ) { - if let image = info[.originalImage] as? UIImage { - parent.onImageCaptured(image) + picker.dismiss(animated: true) { + if let image = info[.originalImage] as? UIImage { + self.onImageCaptured?(image) + } else { + self.onCancel?() + } } - parent.isPresented = false } func imagePickerControllerDidCancel(_ picker: UIImagePickerController) { - parent.isPresented = false + picker.dismiss(animated: true) { + self.onCancel?() + } } } } -// Extension to check camera availability +// MARK: - Hosting VC to own presentation/orientation +final class CameraHostViewController: UIViewController { + var onImageCaptured: ((UIImage) -> Void)? + var onCancel: (() -> Void)? + weak var pickerDelegate: (UIImagePickerControllerDelegate & UINavigationControllerDelegate)? + + private var didPresent = false + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + presentIfNeeded() + } + + private func presentIfNeeded() { + guard !didPresent, UIImagePickerController.isSourceTypeAvailable(.camera) else { return } + didPresent = true + + let picker = UIImagePickerController() + picker.delegate = pickerDelegate + picker.sourceType = .camera + picker.cameraCaptureMode = .photo + picker.cameraDevice = .rear + picker.allowsEditing = false + picker.modalPresentationStyle = .fullScreen + + present(picker, animated: true) + } + + override var supportedInterfaceOrientations: UIInterfaceOrientationMask { + // Defer to app-supported orientations; returning .all allows rotation when permitted by the app + return .all + } + + override var shouldAutorotate: Bool { + true + } +} + +// MARK: - Camera Availability Check extension CameraImagePicker { static var isCameraAvailable: Bool { UIImagePickerController.isSourceTypeAvailable(.camera) diff --git a/ios/Ascently/Views/AddEdit/AddAttemptView.swift b/ios/Ascently/Views/AddEdit/AddAttemptView.swift index f09611d..4aa8723 100644 --- a/ios/Ascently/Views/AddEdit/AddAttemptView.swift +++ b/ios/Ascently/Views/AddEdit/AddAttemptView.swift @@ -26,19 +26,19 @@ struct AddAttemptView: View { enum SheetType: Identifiable { case photoOptions - case camera var id: Int { switch self { case .photoOptions: return 0 - case .camera: return 1 } } } @State private var activeSheet: SheetType? + @State private var showCamera = false @State private var showPhotoPicker = false @State private var isPhotoPickerActionPending = false + @State private var isCameraActionPending = false private var activeProblems: [Problem] { dataManager.activeProblems(forGym: gym.id) @@ -110,6 +110,11 @@ struct AddAttemptView: View { .sheet( item: $activeSheet, onDismiss: { + if isCameraActionPending { + showCamera = true + isCameraActionPending = false + return + } if isPhotoPickerActionPending { showPhotoPicker = true isPhotoPickerActionPending = false @@ -123,7 +128,8 @@ struct AddAttemptView: View { imageData: $imageData, maxImages: 5, onCameraSelected: { - activeSheet = .camera + isCameraActionPending = true + activeSheet = nil }, onPhotoLibrarySelected: { isPhotoPickerActionPending = true @@ -132,16 +138,12 @@ struct AddAttemptView: View { activeSheet = nil } ) - case .camera: - CameraImagePicker( - isPresented: Binding( - get: { activeSheet == .camera }, - set: { if !$0 { activeSheet = nil } } - ) - ) { capturedImage in - if let jpegData = capturedImage.jpegData(compressionQuality: 0.8) { - imageData.append(jpegData) - } + } + } + .fullScreenCover(isPresented: $showCamera) { + CameraImagePicker { capturedImage in + if let jpegData = capturedImage.jpegData(compressionQuality: 0.8) { + imageData.append(jpegData) } } } @@ -778,19 +780,19 @@ struct EditAttemptView: View { enum SheetType: Identifiable { case photoOptions - case camera var id: Int { switch self { case .photoOptions: return 0 - case .camera: return 1 } } } @State private var activeSheet: SheetType? + @State private var showCamera = false @State private var showPhotoPicker = false @State private var isPhotoPickerActionPending = false + @State private var isCameraActionPending = false private var availableProblems: [Problem] { guard let session = dataManager.session(withId: attempt.sessionId) else { @@ -883,6 +885,11 @@ struct EditAttemptView: View { .sheet( item: $activeSheet, onDismiss: { + if isCameraActionPending { + showCamera = true + isCameraActionPending = false + return + } if isPhotoPickerActionPending { showPhotoPicker = true isPhotoPickerActionPending = false @@ -896,7 +903,8 @@ struct EditAttemptView: View { imageData: $imageData, maxImages: 5, onCameraSelected: { - activeSheet = .camera + isCameraActionPending = true + activeSheet = nil }, onPhotoLibrarySelected: { isPhotoPickerActionPending = true @@ -905,16 +913,12 @@ struct EditAttemptView: View { activeSheet = nil } ) - case .camera: - CameraImagePicker( - isPresented: Binding( - get: { activeSheet == .camera }, - set: { if !$0 { activeSheet = nil } } - ) - ) { capturedImage in - if let jpegData = capturedImage.jpegData(compressionQuality: 0.8) { - imageData.append(jpegData) - } + } + } + .fullScreenCover(isPresented: $showCamera) { + CameraImagePicker { capturedImage in + if let jpegData = capturedImage.jpegData(compressionQuality: 0.8) { + imageData.append(jpegData) } } } diff --git a/ios/Ascently/Views/AddEdit/AddEditProblemView.swift b/ios/Ascently/Views/AddEdit/AddEditProblemView.swift index 5039b8f..9478fab 100644 --- a/ios/Ascently/Views/AddEdit/AddEditProblemView.swift +++ b/ios/Ascently/Views/AddEdit/AddEditProblemView.swift @@ -25,19 +25,19 @@ struct AddEditProblemView: View { @State private var isEditing = false enum SheetType: Identifiable { case photoOptions - case camera var id: Int { switch self { case .photoOptions: return 0 - case .camera: return 1 } } } @State private var activeSheet: SheetType? + @State private var showCamera = false @State private var showPhotoPicker = false @State private var isPhotoPickerActionPending = false + @State private var isCameraActionPending = false private var existingProblem: Problem? { guard let problemId = problemId else { return nil } @@ -120,6 +120,11 @@ struct AddEditProblemView: View { .sheet( item: $activeSheet, onDismiss: { + if isCameraActionPending { + showCamera = true + isCameraActionPending = false + return + } if isPhotoPickerActionPending { showPhotoPicker = true isPhotoPickerActionPending = false @@ -133,7 +138,8 @@ struct AddEditProblemView: View { imageData: $imageData, maxImages: 5, onCameraSelected: { - activeSheet = .camera + isCameraActionPending = true + activeSheet = nil }, onPhotoLibrarySelected: { isPhotoPickerActionPending = true @@ -142,16 +148,12 @@ struct AddEditProblemView: View { activeSheet = nil } ) - case .camera: - CameraImagePicker( - isPresented: Binding( - get: { activeSheet == .camera }, - set: { if !$0 { activeSheet = nil } } - ) - ) { capturedImage in - if let jpegData = capturedImage.jpegData(compressionQuality: 0.8) { - imageData.append(jpegData) - } + } + } + .fullScreenCover(isPresented: $showCamera) { + CameraImagePicker { capturedImage in + if let jpegData = capturedImage.jpegData(compressionQuality: 0.8) { + imageData.append(jpegData) } } }