From c442acf464792c9b0feebb5d4c902c258f80a0ae Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Fri, 2 Jan 2026 15:46:56 -0600 Subject: [PATCH] Fix camera issues: front flash reset, rotation, Center Stage Fixes: 1. Front flash now properly resets after capture - restorePreviewAfterFlash() called in photo delegate - Preview no longer stays white after taking photo 2. Device rotation support - updateVideoOrientation() updates capture connections - Uses modern videoRotationAngle API (iOS 17+) - Listens to UIDevice.orientationDidChangeNotification 3. Center Stage support for supported devices - Detects Center Stage availability on front camera - Toggle button in top control bar (person.crop.rectangle icon) - Yellow highlight when enabled - Updates availability on camera switch --- .../Features/Camera/CameraViewModel.swift | 83 ++++++++++++++++++- .../Features/Camera/ContentView.swift | 19 +++++ 2 files changed, 98 insertions(+), 4 deletions(-) diff --git a/SelfieRingLight/Features/Camera/CameraViewModel.swift b/SelfieRingLight/Features/Camera/CameraViewModel.swift index 2887e32..2edeea8 100644 --- a/SelfieRingLight/Features/Camera/CameraViewModel.swift +++ b/SelfieRingLight/Features/Camera/CameraViewModel.swift @@ -32,6 +32,12 @@ class CameraViewModel: NSObject { /// Toast message to display var toastMessage: String? + /// Whether Center Stage is available on this device + var isCenterStageAvailable = false + + /// Whether Center Stage is currently enabled + var isCenterStageEnabled = false + let settings = SettingsViewModel() // Shared config // MARK: - Screen Brightness Handling @@ -90,12 +96,74 @@ class CameraViewModel: NSObject { session.commitConfiguration() session.startRunning() + // Check Center Stage availability + updateCenterStageAvailability() + UIApplication.shared.isIdleTimerDisabled = true saveCurrentBrightness() // Set screen to full brightness for best ring light effect setBrightness(1.0) } + // MARK: - Center Stage + + /// Updates Center Stage availability based on current camera + private func updateCenterStageAvailability() { + isCenterStageAvailable = AVCaptureDevice.isCenterStageEnabled || checkCenterStageSupport() + isCenterStageEnabled = AVCaptureDevice.isCenterStageEnabled + } + + /// Checks if the current device supports Center Stage + private func checkCenterStageSupport() -> Bool { + guard let device = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .front) else { + return false + } + // Center Stage is available if the device has it as an active format feature + return device.activeFormat.isCenterStageSupported + } + + /// Toggles Center Stage on/off + func toggleCenterStage() { + guard isCenterStageAvailable else { return } + + AVCaptureDevice.centerStageControlMode = .app + AVCaptureDevice.isCenterStageEnabled.toggle() + isCenterStageEnabled = AVCaptureDevice.isCenterStageEnabled + } + + // MARK: - Orientation + + /// Updates video orientation based on device orientation + func updateVideoOrientation(for orientation: UIDeviceOrientation) { + guard let connection = photoOutput?.connection(with: .video) else { return } + + // Calculate rotation angle (in degrees) + let rotationAngle: CGFloat + switch orientation { + case .portrait: + rotationAngle = 90 + case .portraitUpsideDown: + rotationAngle = 270 + case .landscapeLeft: + rotationAngle = 0 + case .landscapeRight: + rotationAngle = 180 + default: + rotationAngle = 90 // Default to portrait + } + + // Use modern rotation angle API (iOS 17+) + if connection.isVideoRotationAngleSupported(rotationAngle) { + connection.videoRotationAngle = rotationAngle + } + + // Also update video output connection + if let videoConnection = videoOutput?.connection(with: .video), + videoConnection.isVideoRotationAngleSupported(rotationAngle) { + videoConnection.videoRotationAngle = rotationAngle + } + } + func switchCamera() { guard let session = captureSession else { return } session.beginConfiguration() @@ -109,6 +177,9 @@ class CameraViewModel: NSObject { session.addInput(input) } session.commitConfiguration() + + // Update Center Stage availability (only works on front camera) + updateCenterStageAvailability() } func capturePhoto() { @@ -131,13 +202,14 @@ class CameraViewModel: NSObject { let captureSettings = AVCapturePhotoSettings() photoOutput?.capturePhoto(with: captureSettings, delegate: self) - - // Restore preview after capture completes - try? await Task.sleep(for: .milliseconds(200)) - isPreviewHidden = false } } + /// Restores the preview after front flash capture + func restorePreviewAfterFlash() { + isPreviewHidden = false + } + func startRecording() { guard let videoOutput = videoOutput, !isRecording else { return } let url = FileManager.default.temporaryDirectory.appendingPathComponent("video.mov") @@ -224,6 +296,9 @@ extension CameraViewModel: AVCapturePhotoCaptureDelegate { let image = UIImage(data: data) else { return } Task { @MainActor in + // Restore preview first (in case front flash was used) + restorePreviewAfterFlash() + // Store the captured image for preview capturedMedia = .photo(image) diff --git a/SelfieRingLight/Features/Camera/ContentView.swift b/SelfieRingLight/Features/Camera/ContentView.swift index fd2e4b7..a401f5c 100644 --- a/SelfieRingLight/Features/Camera/ContentView.swift +++ b/SelfieRingLight/Features/Camera/ContentView.swift @@ -59,6 +59,9 @@ struct ContentView: View { .onDisappear { viewModel.restoreBrightness() } + .onReceive(NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification)) { _ in + viewModel.updateVideoOrientation(for: UIDevice.current.orientation) + } .sheet(isPresented: $showPaywall) { ProPaywallView() } @@ -163,6 +166,22 @@ struct ContentView: View { private var topControlBar: some View { HStack { + // Center Stage button (only shown when available) + if viewModel.isCenterStageAvailable { + Button { + viewModel.toggleCenterStage() + } label: { + Image(systemName: viewModel.isCenterStageEnabled ? "person.crop.rectangle.fill" : "person.crop.rectangle") + .font(.body) + .foregroundStyle(viewModel.isCenterStageEnabled ? .yellow : .white) + .padding(Design.Spacing.small) + .background(.ultraThinMaterial, in: .circle) + } + .accessibilityLabel(String(localized: "Center Stage")) + .accessibilityValue(viewModel.isCenterStageEnabled ? "On" : "Off") + .accessibilityHint(String(localized: "Keeps you centered in frame")) + } + Spacer() // Grid toggle