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 effectiveRingSize = min(settings.ringSize, maxRingSize)
|
||||
|
||||
ZStack {
|
||||
// MARK: - Ring Light Background
|
||||
settings.lightColor
|
||||
.ignoresSafeArea()
|
||||
|
||||
// MARK: - Camera with MijickCamera
|
||||
MCamera()
|
||||
.onImageCaptured { image, _ in
|
||||
handleImageCaptured(image)
|
||||
// Use MCamera as the base, with ring light as border
|
||||
MCamera()
|
||||
.onImageCaptured { image, _ in
|
||||
handleImageCaptured(image)
|
||||
}
|
||||
.onVideoCaptured { url, _ in
|
||||
handleVideoCaptured(url)
|
||||
}
|
||||
.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(effectiveRingSize)
|
||||
.animation(.easeInOut(duration: Design.Animation.quick), value: effectiveRingSize)
|
||||
|
||||
// MARK: - Grid Overlay
|
||||
if settings.isGridVisible {
|
||||
GridOverlay(isVisible: true)
|
||||
.padding(effectiveRingSize)
|
||||
.allowsHitTesting(false)
|
||||
.padding(.horizontal, effectiveRingSize + Design.Spacing.small)
|
||||
}
|
||||
|
||||
// MARK: - Top Controls Overlay
|
||||
VStack {
|
||||
topControlBar
|
||||
.padding(.top, effectiveRingSize + Design.Spacing.small)
|
||||
Spacer()
|
||||
.overlay {
|
||||
// Toast
|
||||
if let message = toastMessage {
|
||||
toastView(message: message)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, effectiveRingSize + Design.Spacing.small)
|
||||
|
||||
// MARK: - Toast Notification
|
||||
if let message = toastMessage {
|
||||
toastView(message: message)
|
||||
.onChange(of: geometry.size) { _, newSize in
|
||||
let newMax = min(newSize.width, newSize.height) / 4
|
||||
if settings.ringSize > newMax {
|
||||
settings.ringSize = newMax
|
||||
}
|
||||
}
|
||||
}
|
||||
.onChange(of: geometry.size) { _, newSize in
|
||||
let newMax = min(newSize.width, newSize.height) / 4
|
||||
if settings.ringSize > newMax {
|
||||
settings.ringSize = newMax
|
||||
}
|
||||
}
|
||||
}
|
||||
.ignoresSafeArea()
|
||||
.onAppear {
|
||||
checkCenterStageAvailability()
|
||||
}
|
||||
@ -90,6 +88,35 @@ struct ContentView: View {
|
||||
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
|
||||
|
||||
private var topControlBar: some View {
|
||||
@ -141,20 +168,16 @@ struct ContentView: View {
|
||||
|
||||
// MARK: - Center Stage
|
||||
|
||||
/// Checks if Center Stage is available on this device
|
||||
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 {
|
||||
isCenterStageAvailable = false
|
||||
return
|
||||
}
|
||||
|
||||
// Check if the device supports Center Stage
|
||||
isCenterStageAvailable = device.activeFormat.isCenterStageSupported
|
||||
isCenterStageEnabled = AVCaptureDevice.isCenterStageEnabled
|
||||
}
|
||||
|
||||
/// Toggles Center Stage on/off
|
||||
private func toggleCenterStage() {
|
||||
AVCaptureDevice.centerStageControlMode = .app
|
||||
AVCaptureDevice.isCenterStageEnabled.toggle()
|
||||
|
||||
@ -61,22 +61,10 @@
|
||||
"comment" : "Display name for the \"Boomerang\" capture mode.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Camera Access Required" : {
|
||||
"comment" : "A title displayed when camera access is denied.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Cancel" : {
|
||||
"comment" : "The text for a button that dismisses the current view.",
|
||||
"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" : {
|
||||
"comment" : "A label describing a captured boomerang.",
|
||||
"isCommentAutoGenerated" : true
|
||||
@ -109,10 +97,6 @@
|
||||
"comment" : "An accessibility label for the custom color button.",
|
||||
"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!" : {
|
||||
"comment" : "Announcement posted to VoiceOver when a premium purchase is simulated in debug mode.",
|
||||
"isCommentAutoGenerated" : true
|
||||
@ -168,10 +152,6 @@
|
||||
"comment" : "A hint that appears when a user taps on a color preset button.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"No rotation" : {
|
||||
"comment" : "Accessibility value for the rotation button when the preview is not rotated.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"No Watermarks • Ad-Free" : {
|
||||
"comment" : "Description of a benefit that comes with the Pro subscription.",
|
||||
"isCommentAutoGenerated" : true
|
||||
@ -184,8 +164,8 @@
|
||||
"comment" : "A label describing a setting that is currently enabled.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Open Settings" : {
|
||||
"comment" : "A button label that opens the device settings when tapped.",
|
||||
"Open Source Licenses" : {
|
||||
"comment" : "A heading displayed above a list of open source licenses used in the app.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Opens upgrade options" : {
|
||||
@ -194,14 +174,6 @@
|
||||
},
|
||||
"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" : {
|
||||
"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.",
|
||||
"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" : {
|
||||
"comment" : "Text shown as a toast message when a photo is successfully saved to Photos.",
|
||||
"isCommentAutoGenerated" : true
|
||||
@ -306,14 +258,6 @@
|
||||
"comment" : "Name of a ring light color preset.",
|
||||
"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 %@" : {
|
||||
"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,
|
||||
@ -326,10 +270,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Switch camera" : {
|
||||
"comment" : "A button label that translates to \"Switch camera\".",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Sync Now" : {
|
||||
"comment" : "A button label that triggers a sync action.",
|
||||
"isCommentAutoGenerated" : true
|
||||
@ -349,8 +289,8 @@
|
||||
"Syncing..." : {
|
||||
|
||||
},
|
||||
"Take photo" : {
|
||||
"comment" : "Label for the \"Take photo\" button in the bottom control bar when using the photo capture mode.",
|
||||
"Third-party libraries used in this app" : {
|
||||
"comment" : "A description of the third-party libraries used in this app.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Toggle grid" : {
|
||||
@ -385,8 +325,8 @@
|
||||
"comment" : "Display name for the \"Video\" capture mode.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Video saved" : {
|
||||
"comment" : "Accessibility notification text when a video is successfully saved to the user's photo library.",
|
||||
"View on GitHub" : {
|
||||
"comment" : "A button label that says \"View on GitHub\".",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Warm Amber" : {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user