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
This commit is contained in:
Matt Bruce 2026-01-02 15:46:56 -06:00
parent 9066635a4d
commit c442acf464
2 changed files with 98 additions and 4 deletions

View File

@ -32,6 +32,12 @@ class CameraViewModel: NSObject {
/// Toast message to display /// Toast message to display
var toastMessage: String? 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 let settings = SettingsViewModel() // Shared config
// MARK: - Screen Brightness Handling // MARK: - Screen Brightness Handling
@ -90,12 +96,74 @@ class CameraViewModel: NSObject {
session.commitConfiguration() session.commitConfiguration()
session.startRunning() session.startRunning()
// Check Center Stage availability
updateCenterStageAvailability()
UIApplication.shared.isIdleTimerDisabled = true UIApplication.shared.isIdleTimerDisabled = true
saveCurrentBrightness() saveCurrentBrightness()
// Set screen to full brightness for best ring light effect // Set screen to full brightness for best ring light effect
setBrightness(1.0) 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() { func switchCamera() {
guard let session = captureSession else { return } guard let session = captureSession else { return }
session.beginConfiguration() session.beginConfiguration()
@ -109,6 +177,9 @@ class CameraViewModel: NSObject {
session.addInput(input) session.addInput(input)
} }
session.commitConfiguration() session.commitConfiguration()
// Update Center Stage availability (only works on front camera)
updateCenterStageAvailability()
} }
func capturePhoto() { func capturePhoto() {
@ -131,13 +202,14 @@ class CameraViewModel: NSObject {
let captureSettings = AVCapturePhotoSettings() let captureSettings = AVCapturePhotoSettings()
photoOutput?.capturePhoto(with: captureSettings, delegate: self) 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() { func startRecording() {
guard let videoOutput = videoOutput, !isRecording else { return } guard let videoOutput = videoOutput, !isRecording else { return }
let url = FileManager.default.temporaryDirectory.appendingPathComponent("video.mov") let url = FileManager.default.temporaryDirectory.appendingPathComponent("video.mov")
@ -224,6 +296,9 @@ extension CameraViewModel: AVCapturePhotoCaptureDelegate {
let image = UIImage(data: data) else { return } let image = UIImage(data: data) else { return }
Task { @MainActor in Task { @MainActor in
// Restore preview first (in case front flash was used)
restorePreviewAfterFlash()
// Store the captured image for preview // Store the captured image for preview
capturedMedia = .photo(image) capturedMedia = .photo(image)

View File

@ -59,6 +59,9 @@ struct ContentView: View {
.onDisappear { .onDisappear {
viewModel.restoreBrightness() viewModel.restoreBrightness()
} }
.onReceive(NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification)) { _ in
viewModel.updateVideoOrientation(for: UIDevice.current.orientation)
}
.sheet(isPresented: $showPaywall) { .sheet(isPresented: $showPaywall) {
ProPaywallView() ProPaywallView()
} }
@ -163,6 +166,22 @@ struct ContentView: View {
private var topControlBar: some View { private var topControlBar: some View {
HStack { 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() Spacer()
// Grid toggle // Grid toggle