Fix camera not showing - use MCamera as base with ring overlay
Issue: MCamera wasn't rendering when embedded inside ZStack with background color in front of it. Solution: Restructure view hierarchy: - MCamera is now the base view (not inside a ZStack) - Ring light effect is now an overlay using mask/blendMode - Grid, controls, and toast are overlays on top The ring light overlay uses: 1. Full-screen settings.lightColor 2. A mask with destinationOut blend mode 3. A RoundedRectangle cutout in the center This allows MCamera to render properly while creating the ring light border effect around the camera preview.
This commit is contained in:
parent
b97590f1c1
commit
e628897bff
@ -25,51 +25,49 @@ struct ContentView: View {
|
|||||||
let maxRingSize = calculateMaxRingSize(for: geometry)
|
let maxRingSize = calculateMaxRingSize(for: geometry)
|
||||||
let effectiveRingSize = min(settings.ringSize, maxRingSize)
|
let effectiveRingSize = min(settings.ringSize, maxRingSize)
|
||||||
|
|
||||||
ZStack {
|
// Use MCamera as the base, with ring light as border
|
||||||
// MARK: - Ring Light Background
|
MCamera()
|
||||||
settings.lightColor
|
.onImageCaptured { image, _ in
|
||||||
.ignoresSafeArea()
|
handleImageCaptured(image)
|
||||||
|
}
|
||||||
// MARK: - Camera with MijickCamera
|
.onVideoCaptured { url, _ in
|
||||||
MCamera()
|
handleVideoCaptured(url)
|
||||||
.onImageCaptured { image, _ in
|
}
|
||||||
handleImageCaptured(image)
|
.ignoresSafeArea()
|
||||||
|
.overlay {
|
||||||
|
// Ring light border overlay
|
||||||
|
ringLightOverlay(ringSize: effectiveRingSize)
|
||||||
|
}
|
||||||
|
.overlay {
|
||||||
|
// Grid overlay
|
||||||
|
if settings.isGridVisible {
|
||||||
|
GridOverlay(isVisible: true)
|
||||||
|
.padding(effectiveRingSize)
|
||||||
|
.allowsHitTesting(false)
|
||||||
}
|
}
|
||||||
.onVideoCaptured { url, _ in
|
}
|
||||||
handleVideoCaptured(url)
|
.overlay {
|
||||||
|
// Top controls
|
||||||
|
VStack {
|
||||||
|
topControlBar
|
||||||
|
.padding(.top, effectiveRingSize + Design.Spacing.small)
|
||||||
|
Spacer()
|
||||||
}
|
}
|
||||||
.clipShape(RoundedRectangle(cornerRadius: Design.CornerRadius.large))
|
.padding(.horizontal, effectiveRingSize + Design.Spacing.small)
|
||||||
.padding(effectiveRingSize)
|
|
||||||
.animation(.easeInOut(duration: Design.Animation.quick), value: effectiveRingSize)
|
|
||||||
|
|
||||||
// MARK: - Grid Overlay
|
|
||||||
if settings.isGridVisible {
|
|
||||||
GridOverlay(isVisible: true)
|
|
||||||
.padding(effectiveRingSize)
|
|
||||||
.allowsHitTesting(false)
|
|
||||||
}
|
}
|
||||||
|
.overlay {
|
||||||
// MARK: - Top Controls Overlay
|
// Toast
|
||||||
VStack {
|
if let message = toastMessage {
|
||||||
topControlBar
|
toastView(message: message)
|
||||||
.padding(.top, effectiveRingSize + Design.Spacing.small)
|
}
|
||||||
Spacer()
|
|
||||||
}
|
}
|
||||||
.padding(.horizontal, effectiveRingSize + Design.Spacing.small)
|
.onChange(of: geometry.size) { _, newSize in
|
||||||
|
let newMax = min(newSize.width, newSize.height) / 4
|
||||||
// MARK: - Toast Notification
|
if settings.ringSize > newMax {
|
||||||
if let message = toastMessage {
|
settings.ringSize = newMax
|
||||||
toastView(message: message)
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
.onChange(of: geometry.size) { _, newSize in
|
|
||||||
let newMax = min(newSize.width, newSize.height) / 4
|
|
||||||
if settings.ringSize > newMax {
|
|
||||||
settings.ringSize = newMax
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.ignoresSafeArea()
|
|
||||||
.onAppear {
|
.onAppear {
|
||||||
checkCenterStageAvailability()
|
checkCenterStageAvailability()
|
||||||
}
|
}
|
||||||
@ -90,6 +88,35 @@ struct ContentView: View {
|
|||||||
min(geometry.size.width, geometry.size.height) / 4
|
min(geometry.size.width, geometry.size.height) / 4
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Ring Light Overlay
|
||||||
|
|
||||||
|
/// Creates a ring light effect as a border around the camera
|
||||||
|
@ViewBuilder
|
||||||
|
private func ringLightOverlay(ringSize: CGFloat) -> some View {
|
||||||
|
// Use a rectangle with a large inner cutout to create the ring effect
|
||||||
|
GeometryReader { geo in
|
||||||
|
let innerRect = CGRect(
|
||||||
|
x: ringSize,
|
||||||
|
y: ringSize,
|
||||||
|
width: geo.size.width - (ringSize * 2),
|
||||||
|
height: geo.size.height - (ringSize * 2)
|
||||||
|
)
|
||||||
|
|
||||||
|
settings.lightColor
|
||||||
|
.mask(
|
||||||
|
Rectangle()
|
||||||
|
.overlay(
|
||||||
|
RoundedRectangle(cornerRadius: Design.CornerRadius.large)
|
||||||
|
.frame(width: innerRect.width, height: innerRect.height)
|
||||||
|
.blendMode(.destinationOut)
|
||||||
|
)
|
||||||
|
.compositingGroup()
|
||||||
|
)
|
||||||
|
.allowsHitTesting(false)
|
||||||
|
}
|
||||||
|
.animation(.easeInOut(duration: Design.Animation.quick), value: ringSize)
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Top Control Bar
|
// MARK: - Top Control Bar
|
||||||
|
|
||||||
private var topControlBar: some View {
|
private var topControlBar: some View {
|
||||||
@ -141,20 +168,16 @@ struct ContentView: View {
|
|||||||
|
|
||||||
// MARK: - Center Stage
|
// MARK: - Center Stage
|
||||||
|
|
||||||
/// Checks if Center Stage is available on this device
|
|
||||||
private func checkCenterStageAvailability() {
|
private func checkCenterStageAvailability() {
|
||||||
// Center Stage requires a compatible device (iPad with A12+ or some iPhones)
|
|
||||||
guard let device = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .front) else {
|
guard let device = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .front) else {
|
||||||
isCenterStageAvailable = false
|
isCenterStageAvailable = false
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if the device supports Center Stage
|
|
||||||
isCenterStageAvailable = device.activeFormat.isCenterStageSupported
|
isCenterStageAvailable = device.activeFormat.isCenterStageSupported
|
||||||
isCenterStageEnabled = AVCaptureDevice.isCenterStageEnabled
|
isCenterStageEnabled = AVCaptureDevice.isCenterStageEnabled
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Toggles Center Stage on/off
|
|
||||||
private func toggleCenterStage() {
|
private func toggleCenterStage() {
|
||||||
AVCaptureDevice.centerStageControlMode = .app
|
AVCaptureDevice.centerStageControlMode = .app
|
||||||
AVCaptureDevice.isCenterStageEnabled.toggle()
|
AVCaptureDevice.isCenterStageEnabled.toggle()
|
||||||
|
|||||||
@ -61,22 +61,10 @@
|
|||||||
"comment" : "Display name for the \"Boomerang\" capture mode.",
|
"comment" : "Display name for the \"Boomerang\" capture mode.",
|
||||||
"isCommentAutoGenerated" : true
|
"isCommentAutoGenerated" : true
|
||||||
},
|
},
|
||||||
"Camera Access Required" : {
|
|
||||||
"comment" : "A title displayed when camera access is denied.",
|
|
||||||
"isCommentAutoGenerated" : true
|
|
||||||
},
|
|
||||||
"Cancel" : {
|
"Cancel" : {
|
||||||
"comment" : "The text for a button that dismisses the current view.",
|
"comment" : "The text for a button that dismisses the current view.",
|
||||||
"isCommentAutoGenerated" : true
|
"isCommentAutoGenerated" : true
|
||||||
},
|
},
|
||||||
"Capture boomerang" : {
|
|
||||||
"comment" : "Label for capturing a boomerang photo.",
|
|
||||||
"isCommentAutoGenerated" : true
|
|
||||||
},
|
|
||||||
"Capture mode: %@" : {
|
|
||||||
"comment" : "A label describing the currently selected capture mode. The placeholder is replaced with the actual mode name.",
|
|
||||||
"isCommentAutoGenerated" : true
|
|
||||||
},
|
|
||||||
"Captured boomerang" : {
|
"Captured boomerang" : {
|
||||||
"comment" : "A label describing a captured boomerang.",
|
"comment" : "A label describing a captured boomerang.",
|
||||||
"isCommentAutoGenerated" : true
|
"isCommentAutoGenerated" : true
|
||||||
@ -109,10 +97,6 @@
|
|||||||
"comment" : "An accessibility label for the custom color button.",
|
"comment" : "An accessibility label for the custom color button.",
|
||||||
"isCommentAutoGenerated" : true
|
"isCommentAutoGenerated" : true
|
||||||
},
|
},
|
||||||
"Custom rotation" : {
|
|
||||||
"comment" : "Accessibility value for the rotate preview button when the user has customized the rotation angle.",
|
|
||||||
"isCommentAutoGenerated" : true
|
|
||||||
},
|
|
||||||
"Debug mode: Purchase simulated!" : {
|
"Debug mode: Purchase simulated!" : {
|
||||||
"comment" : "Announcement posted to VoiceOver when a premium purchase is simulated in debug mode.",
|
"comment" : "Announcement posted to VoiceOver when a premium purchase is simulated in debug mode.",
|
||||||
"isCommentAutoGenerated" : true
|
"isCommentAutoGenerated" : true
|
||||||
@ -168,10 +152,6 @@
|
|||||||
"comment" : "A hint that appears when a user taps on a color preset button.",
|
"comment" : "A hint that appears when a user taps on a color preset button.",
|
||||||
"isCommentAutoGenerated" : true
|
"isCommentAutoGenerated" : true
|
||||||
},
|
},
|
||||||
"No rotation" : {
|
|
||||||
"comment" : "Accessibility value for the rotation button when the preview is not rotated.",
|
|
||||||
"isCommentAutoGenerated" : true
|
|
||||||
},
|
|
||||||
"No Watermarks • Ad-Free" : {
|
"No Watermarks • Ad-Free" : {
|
||||||
"comment" : "Description of a benefit that comes with the Pro subscription.",
|
"comment" : "Description of a benefit that comes with the Pro subscription.",
|
||||||
"isCommentAutoGenerated" : true
|
"isCommentAutoGenerated" : true
|
||||||
@ -184,8 +164,8 @@
|
|||||||
"comment" : "A label describing a setting that is currently enabled.",
|
"comment" : "A label describing a setting that is currently enabled.",
|
||||||
"isCommentAutoGenerated" : true
|
"isCommentAutoGenerated" : true
|
||||||
},
|
},
|
||||||
"Open Settings" : {
|
"Open Source Licenses" : {
|
||||||
"comment" : "A button label that opens the device settings when tapped.",
|
"comment" : "A heading displayed above a list of open source licenses used in the app.",
|
||||||
"isCommentAutoGenerated" : true
|
"isCommentAutoGenerated" : true
|
||||||
},
|
},
|
||||||
"Opens upgrade options" : {
|
"Opens upgrade options" : {
|
||||||
@ -194,14 +174,6 @@
|
|||||||
},
|
},
|
||||||
"Photo" : {
|
"Photo" : {
|
||||||
|
|
||||||
},
|
|
||||||
"Photo captured" : {
|
|
||||||
"comment" : "Accessibility label for a notification that is posted when a photo is captured.",
|
|
||||||
"isCommentAutoGenerated" : true
|
|
||||||
},
|
|
||||||
"Please enable camera access in Settings to use SelfieRingLight." : {
|
|
||||||
"comment" : "A message instructing the user to enable camera access in Settings to use SelfieRingLight.",
|
|
||||||
"isCommentAutoGenerated" : true
|
|
||||||
},
|
},
|
||||||
"Premium color" : {
|
"Premium color" : {
|
||||||
"comment" : "An accessibility hint for a premium color option in the color preset button.",
|
"comment" : "An accessibility hint for a premium color option in the color preset button.",
|
||||||
@ -239,26 +211,6 @@
|
|||||||
"comment" : "The label for the ring size slider in the settings view.",
|
"comment" : "The label for the ring size slider in the settings view.",
|
||||||
"isCommentAutoGenerated" : true
|
"isCommentAutoGenerated" : true
|
||||||
},
|
},
|
||||||
"Rotate preview" : {
|
|
||||||
"comment" : "A button that rotates the camera preview by 90 degrees.",
|
|
||||||
"isCommentAutoGenerated" : true
|
|
||||||
},
|
|
||||||
"Rotated 90 degrees left" : {
|
|
||||||
"comment" : "Accessibility value for the \"Rotate preview\" button when the camera preview is rotated 90 degrees to the left.",
|
|
||||||
"isCommentAutoGenerated" : true
|
|
||||||
},
|
|
||||||
"Rotated 90 degrees right" : {
|
|
||||||
"comment" : "Accessibility value for the \"Rotate preview\" button when the camera preview is rotated 90 degrees to the right.",
|
|
||||||
"isCommentAutoGenerated" : true
|
|
||||||
},
|
|
||||||
"Rotated 180 degrees" : {
|
|
||||||
"comment" : "Accessibility value of the \"Rotate preview\" button when the preview is 180 degrees rotated.",
|
|
||||||
"isCommentAutoGenerated" : true
|
|
||||||
},
|
|
||||||
"Rotates the camera preview 90 degrees" : {
|
|
||||||
"comment" : "A hint that explains how to rotate the camera preview.",
|
|
||||||
"isCommentAutoGenerated" : true
|
|
||||||
},
|
|
||||||
"Saved to Photos" : {
|
"Saved to Photos" : {
|
||||||
"comment" : "Text shown as a toast message when a photo is successfully saved to Photos.",
|
"comment" : "Text shown as a toast message when a photo is successfully saved to Photos.",
|
||||||
"isCommentAutoGenerated" : true
|
"isCommentAutoGenerated" : true
|
||||||
@ -306,14 +258,6 @@
|
|||||||
"comment" : "Name of a ring light color preset.",
|
"comment" : "Name of a ring light color preset.",
|
||||||
"isCommentAutoGenerated" : true
|
"isCommentAutoGenerated" : true
|
||||||
},
|
},
|
||||||
"Start recording" : {
|
|
||||||
"comment" : "Label for the \"Start recording\" button in the bottom control bar when not recording a video.",
|
|
||||||
"isCommentAutoGenerated" : true
|
|
||||||
},
|
|
||||||
"Stop recording" : {
|
|
||||||
"comment" : "Label for the button that stops recording a video.",
|
|
||||||
"isCommentAutoGenerated" : true
|
|
||||||
},
|
|
||||||
"Subscribe to %@ for %@" : {
|
"Subscribe to %@ for %@" : {
|
||||||
"comment" : "A button that triggers a purchase of a premium content package. The label text is generated based on the package's title and price.",
|
"comment" : "A button that triggers a purchase of a premium content package. The label text is generated based on the package's title and price.",
|
||||||
"isCommentAutoGenerated" : true,
|
"isCommentAutoGenerated" : true,
|
||||||
@ -326,10 +270,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Switch camera" : {
|
|
||||||
"comment" : "A button label that translates to \"Switch camera\".",
|
|
||||||
"isCommentAutoGenerated" : true
|
|
||||||
},
|
|
||||||
"Sync Now" : {
|
"Sync Now" : {
|
||||||
"comment" : "A button label that triggers a sync action.",
|
"comment" : "A button label that triggers a sync action.",
|
||||||
"isCommentAutoGenerated" : true
|
"isCommentAutoGenerated" : true
|
||||||
@ -349,8 +289,8 @@
|
|||||||
"Syncing..." : {
|
"Syncing..." : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Take photo" : {
|
"Third-party libraries used in this app" : {
|
||||||
"comment" : "Label for the \"Take photo\" button in the bottom control bar when using the photo capture mode.",
|
"comment" : "A description of the third-party libraries used in this app.",
|
||||||
"isCommentAutoGenerated" : true
|
"isCommentAutoGenerated" : true
|
||||||
},
|
},
|
||||||
"Toggle grid" : {
|
"Toggle grid" : {
|
||||||
@ -385,8 +325,8 @@
|
|||||||
"comment" : "Display name for the \"Video\" capture mode.",
|
"comment" : "Display name for the \"Video\" capture mode.",
|
||||||
"isCommentAutoGenerated" : true
|
"isCommentAutoGenerated" : true
|
||||||
},
|
},
|
||||||
"Video saved" : {
|
"View on GitHub" : {
|
||||||
"comment" : "Accessibility notification text when a video is successfully saved to the user's photo library.",
|
"comment" : "A button label that says \"View on GitHub\".",
|
||||||
"isCommentAutoGenerated" : true
|
"isCommentAutoGenerated" : true
|
||||||
},
|
},
|
||||||
"Warm Amber" : {
|
"Warm Amber" : {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user