Test: Minimal MCamera implementation to verify it works

Simple ContentView with just MCamera() and nothing else.
If this works, we can add the ring light back incrementally.
This commit is contained in:
Matt Bruce 2026-01-02 16:32:09 -06:00
parent 442f5d6e1d
commit 72bac70ea1
3 changed files with 22 additions and 424 deletions

View File

@ -10,6 +10,7 @@
EA766C862F08306200DC03E1 /* RevenueCat in Frameworks */ = {isa = PBXBuildFile; productRef = EA766C852F08306200DC03E1 /* RevenueCat */; };
EA766C882F08306200DC03E1 /* RevenueCatUI in Frameworks */ = {isa = PBXBuildFile; productRef = EA766C872F08306200DC03E1 /* RevenueCatUI */; };
EA766F022F08500000DC03E1 /* Bedrock in Frameworks */ = {isa = PBXBuildFile; productRef = EA766F012F08500000DC03E1 /* Bedrock */; };
EA766F102F08600000DC03E1 /* MijickCamera in Frameworks */ = {isa = PBXBuildFile; productRef = EA766F0F2F08600000DC03E1 /* MijickCamera */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@ -63,6 +64,7 @@
EA766C862F08306200DC03E1 /* RevenueCat in Frameworks */,
EA766C882F08306200DC03E1 /* RevenueCatUI in Frameworks */,
EA766F022F08500000DC03E1 /* Bedrock in Frameworks */,
EA766F102F08600000DC03E1 /* MijickCamera in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -136,6 +138,7 @@
EA766C852F08306200DC03E1 /* RevenueCat */,
EA766C872F08306200DC03E1 /* RevenueCatUI */,
EA766F012F08500000DC03E1 /* Bedrock */,
EA766F0F2F08600000DC03E1 /* MijickCamera */,
);
productName = SelfieRingLight;
productReference = EA766C2C2F082A8400DC03E1 /* SelfieRingLight.app */;
@ -222,6 +225,7 @@
packageReferences = (
EA766C822F08306200DC03E1 /* XCRemoteSwiftPackageReference "purchases-ios-spm" */,
EA766F002F08500000DC03E1 /* XCRemoteSwiftPackageReference "Bedrock" */,
EA766F0E2F08600000DC03E1 /* XCRemoteSwiftPackageReference "Camera" */,
);
preferredProjectObjectVersion = 77;
productRefGroup = EA766C2D2F082A8400DC03E1 /* Products */;
@ -638,6 +642,14 @@
kind = branch;
};
};
EA766F0E2F08600000DC03E1 /* XCRemoteSwiftPackageReference "Camera" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/Mijick/Camera";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 3.0.0;
};
};
/* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */

View File

@ -1,433 +1,17 @@
import SwiftUI
import MijickCamera
import Bedrock
/// Simple test view to verify MijickCamera works
struct ContentView: View {
@State private var viewModel = CameraViewModel()
@State private var premiumManager = PremiumManager()
@State private var showPaywall = false
@State private var showSettings = false
@State private var showShareSheet = false
var body: some View {
GeometryReader { geometry in
let maxRingSize = calculateMaxRingSize(for: geometry)
let effectiveRingSize = min(viewModel.settings.ringSize, maxRingSize)
ZStack {
// MARK: - Ring Light Background
ringLightBackground
// MARK: - Camera Preview (full screen with ring border)
cameraPreviewArea(ringSize: effectiveRingSize)
// MARK: - Grid Overlay
if viewModel.settings.isGridVisible && !viewModel.isPreviewHidden {
GridOverlay(isVisible: true)
.padding(effectiveRingSize)
}
// MARK: - Controls Overlay (on top of preview)
controlsOverlay(ringSize: effectiveRingSize)
// MARK: - Permission Denied View
if !viewModel.isCameraAuthorized && viewModel.captureSession != nil {
permissionDeniedView
}
// MARK: - Toast Notification
if let message = viewModel.toastMessage {
toastView(message: message)
}
}
.onChange(of: geometry.size) { _, newSize in
// Update max ring size when screen size changes
let newMax = min(newSize.width, newSize.height) / 4
if viewModel.settings.ringSize > newMax {
viewModel.settings.ringSize = newMax
}
}
// Just MCamera - nothing else - to test if it renders
MCamera()
.onImageCaptured { image, _ in
print("Image captured!")
}
.ignoresSafeArea()
.task {
await viewModel.setupCamera()
}
.onDisappear {
viewModel.restoreBrightness()
}
.onReceive(NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification)) { _ in
viewModel.updateVideoOrientation(for: UIDevice.current.orientation)
}
.sheet(isPresented: $showPaywall) {
ProPaywallView()
}
.sheet(isPresented: $showSettings) {
SettingsView(viewModel: viewModel.settings, showPaywall: $showPaywall)
}
.fullScreenCover(isPresented: $viewModel.showPostCapturePreview) {
if let media = viewModel.capturedMedia {
PostCapturePreviewView(
media: media,
isPremiumUnlocked: premiumManager.isPremiumUnlocked,
onRetake: {
viewModel.retakeCapture()
},
onSave: {
viewModel.saveCurrentCapture()
},
onShare: {
showShareSheet = true
},
onDismiss: {
viewModel.dismissPostCapturePreview()
}
)
.sheet(isPresented: $showShareSheet) {
ShareSheet(items: viewModel.getShareItems())
}
}
}
}
// MARK: - Max Ring Size Calculation
/// Calculates maximum ring size based on screen dimensions
/// Ring should not exceed 1/4 of the smaller dimension
private func calculateMaxRingSize(for geometry: GeometryProxy) -> CGFloat {
min(geometry.size.width, geometry.size.height) / 4
}
// MARK: - Ring Light Background
@ViewBuilder
private var ringLightBackground: some View {
// Always use the selected light color - premium checks are done in Settings
viewModel.settings.lightColor
.ignoresSafeArea()
}
// MARK: - Camera Preview Area
@ViewBuilder
private func cameraPreviewArea(ringSize: CGFloat) -> some View {
if viewModel.isCameraAuthorized {
// Show preview unless front flash is active
if !viewModel.isPreviewHidden {
CameraPreview(
viewModel: viewModel,
isMirrorFlipped: viewModel.settings.isMirrorFlipped,
zoomFactor: viewModel.settings.currentZoomFactor,
manualRotationAngle: viewModel.manualRotationAngle,
ringLightColor: viewModel.settings.lightColor
)
.clipShape(.rect(cornerRadius: Design.CornerRadius.large))
.padding(ringSize)
.animation(.easeInOut(duration: Design.Animation.quick), value: ringSize)
.animation(.easeInOut(duration: Design.Animation.quick), value: viewModel.manualRotationAngle)
}
} else {
// Show placeholder while requesting permission
Rectangle()
.fill(viewModel.settings.lightColor)
.clipShape(.rect(cornerRadius: Design.CornerRadius.large))
.padding(ringSize)
.animation(.easeInOut(duration: Design.Animation.quick), value: ringSize)
.overlay {
if viewModel.captureSession == nil {
ProgressView()
.tint(.white)
.scaleEffect(1.5)
}
}
}
}
// MARK: - Controls Overlay
private func controlsOverlay(ringSize: CGFloat) -> some View {
VStack {
// Top bar
topControlBar
.padding(.top, ringSize + Design.Spacing.small)
Spacer()
// Bottom capture controls
bottomControlBar
.padding(.bottom, ringSize + Design.Spacing.medium)
}
.padding(.horizontal, ringSize + Design.Spacing.small)
.animation(.easeInOut(duration: Design.Animation.quick), value: ringSize)
}
// MARK: - Top Control Bar
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"))
}
// Rotate preview button
Button {
withAnimation(.easeInOut(duration: Design.Animation.quick)) {
viewModel.cycleManualRotation()
}
} label: {
Image(systemName: rotationIconName)
.font(.body)
.foregroundStyle(viewModel.manualRotationAngle != 0 ? .yellow : .white)
.padding(Design.Spacing.small)
.background(.ultraThinMaterial, in: .circle)
}
.accessibilityLabel(String(localized: "Rotate preview"))
.accessibilityValue(rotationAccessibilityValue)
.accessibilityHint(String(localized: "Rotates the camera preview 90 degrees"))
Spacer()
// Grid toggle
Button {
viewModel.settings.isGridVisible.toggle()
} label: {
Image(systemName: "square.grid.3x3")
.font(.body)
.foregroundStyle(viewModel.settings.isGridVisible ? .yellow : .white)
.padding(Design.Spacing.small)
.background(.ultraThinMaterial, in: .circle)
}
.accessibilityLabel(String(localized: "Toggle grid"))
.accessibilityValue(viewModel.settings.isGridVisible ? "On" : "Off")
// Settings button
Button {
showSettings = true
} label: {
Image(systemName: "gearshape.fill")
.font(.body)
.foregroundStyle(.white)
.padding(Design.Spacing.small)
.background(.ultraThinMaterial, in: .circle)
}
.accessibilityLabel(String(localized: "Settings"))
}
}
/// Icon name for rotation button based on current rotation
private var rotationIconName: String {
switch viewModel.manualRotationAngle {
case 90:
return "rotate.right.fill"
case 180:
return "arrow.up.arrow.down"
case 270:
return "rotate.left.fill"
default:
return "rotate.right"
}
}
/// Accessibility value for rotation button
private var rotationAccessibilityValue: String {
switch viewModel.manualRotationAngle {
case 0:
return String(localized: "No rotation")
case 90:
return String(localized: "Rotated 90 degrees right")
case 180:
return String(localized: "Rotated 180 degrees")
case 270:
return String(localized: "Rotated 90 degrees left")
default:
return String(localized: "Custom rotation")
}
}
// MARK: - Bottom Control Bar
private var bottomControlBar: some View {
HStack {
// Switch camera button
Button {
viewModel.switchCamera()
} label: {
Image(systemName: "camera.rotate.fill")
.font(.title2)
.foregroundStyle(.white)
.padding(Design.Spacing.medium)
.background(.ultraThinMaterial, in: .circle)
}
.accessibilityLabel(String(localized: "Switch camera"))
Spacer()
// Capture button
captureButton
Spacer()
// Capture mode selector
captureModeMenu
}
}
// MARK: - Capture Button
private var captureButton: some View {
Button {
captureAction()
} label: {
ZStack {
Circle()
.fill(.white)
.frame(width: Design.Capture.buttonSize, height: Design.Capture.buttonSize)
Circle()
.stroke(.white, lineWidth: Design.LineWidth.thick)
.frame(width: Design.Capture.buttonSize + Design.Spacing.small, height: Design.Capture.buttonSize + Design.Spacing.small)
// Show red stop square when recording
if viewModel.isRecording {
RoundedRectangle(cornerRadius: Design.CornerRadius.xSmall)
.fill(.red)
.frame(width: Design.Capture.stopSquare, height: Design.Capture.stopSquare)
}
}
}
.accessibilityLabel(captureButtonLabel)
.disabled(!viewModel.canCapture)
}
// MARK: - Capture Mode Menu
private var captureModeMenu: some View {
Menu {
ForEach(CaptureMode.allCases) { mode in
Button {
if !mode.isPremium || premiumManager.isPremiumUnlocked {
viewModel.settings.selectedCaptureMode = mode
} else {
showPaywall = true
}
} label: {
HStack {
Label(mode.displayName, systemImage: mode.systemImage)
if mode.isPremium && !premiumManager.isPremiumUnlocked {
Image(systemName: "crown.fill")
}
}
}
}
} label: {
Image(systemName: viewModel.settings.selectedCaptureMode.systemImage)
.font(.title2)
.foregroundStyle(.white)
.padding(Design.Spacing.medium)
.background(.ultraThinMaterial, in: .circle)
}
.accessibilityLabel(String(localized: "Capture mode: \(viewModel.settings.selectedCaptureMode.displayName)"))
}
// MARK: - Permission Denied View
private var permissionDeniedView: some View {
VStack(spacing: Design.Spacing.large) {
Image(systemName: "camera.fill")
.font(.system(size: Design.BaseFontSize.hero))
.foregroundStyle(.white.opacity(Design.Opacity.medium))
Text("Camera Access Required")
.font(.title2.bold())
.foregroundStyle(.white)
Text("Please enable camera access in Settings to use SelfieRingLight.")
.font(.body)
.foregroundStyle(.white.opacity(Design.Opacity.strong))
.multilineTextAlignment(.center)
.padding(.horizontal, Design.Spacing.xLarge)
Button("Open Settings") {
if let url = URL(string: UIApplication.openSettingsURLString) {
UIApplication.shared.open(url)
}
}
.buttonStyle(.borderedProminent)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(.black.opacity(Design.Opacity.heavy))
}
// MARK: - Capture Action
private func captureAction() {
switch viewModel.settings.selectedCaptureMode {
case .photo:
viewModel.capturePhoto()
case .video:
if viewModel.isRecording {
viewModel.stopRecording()
} else {
viewModel.startRecording()
}
case .boomerang:
// TODO: Implement boomerang capture
viewModel.capturePhoto()
}
}
private var captureButtonLabel: String {
switch viewModel.settings.selectedCaptureMode {
case .photo:
return String(localized: "Take photo")
case .video:
return viewModel.isRecording ? String(localized: "Stop recording") : String(localized: "Start recording")
case .boomerang:
return String(localized: "Capture boomerang")
}
}
// MARK: - Toast View
private func toastView(message: String) -> some View {
VStack {
Spacer()
Text(message)
.font(.system(size: Design.BaseFontSize.body, weight: .medium))
.foregroundStyle(.white)
.padding(.horizontal, Design.Spacing.large)
.padding(.vertical, Design.Spacing.medium)
.background(.ultraThinMaterial, in: Capsule())
.padding(.bottom, Design.Spacing.xxxLarge)
.transition(.move(edge: .bottom).combined(with: .opacity))
.animation(.spring(duration: Design.Animation.quick), value: message)
}
.accessibilityLabel(message)
}
}
// MARK: - Share Sheet
/// UIKit wrapper for UIActivityViewController
struct ShareSheet: UIViewControllerRepresentable {
let items: [Any]
func makeUIViewController(context: Context) -> UIActivityViewController {
UIActivityViewController(activityItems: items, applicationActivities: nil)
}
func updateUIViewController(_ uiViewController: UIActivityViewController, context: Context) {}
}
#Preview {

View File

@ -244,7 +244,8 @@
"isCommentAutoGenerated" : true
},
"Rotate preview" : {
"comment" : "A button that rotates the camera preview by 90 degrees.",
"isCommentAutoGenerated" : true
},
"Rotated 90 degrees left" : {
"comment" : "An accessibility label describing a 90-degree left rotation.",
@ -259,7 +260,8 @@
"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.",