Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>

This commit is contained in:
Matt Bruce 2026-01-04 16:03:50 -06:00
parent 5499b10b2a
commit fe7ab135af
8 changed files with 123 additions and 88 deletions

View File

@ -553,7 +553,6 @@ SelfieCam uses the following architectural patterns:
- Skin smoothing
- Center Stage
- Extended timers (5s, 10s)
- Video and Boomerang capture modes
## Settings & iCloud Sync
@ -621,7 +620,6 @@ var flashMode: CameraFlashMode // .off, .on, .auto
- Front/back camera switching
- Pinch-to-zoom
- Photo capture with quality settings
- Video recording (premium)
- HDR mode (premium)

View File

@ -75,7 +75,6 @@ Features/
- Post-capture preview with share functionality
- Auto-save option to Photo Library
- Front flash using screen brightness
- Support for photo, video, and boomerang modes
### 4. Freemium Model
- Built with **RevenueCat** for subscription management
@ -128,7 +127,6 @@ var isMirrorFlipped: Bool {
| Skin smoothing | Off | Configurable |
| Flash sync | Off | Configurable |
| Center stage | Off | Configurable |
| Capture modes | Photo | Photo, Video, Boomerang |
---

View File

@ -15,10 +15,8 @@ Perfect for low-light selfies, content creation, video calls, makeup application
- **Front Flash**: Uses screen brightness for front camera flash effect
- Real-time camera preview with smooth performance
### Capture Modes
### Capture
- **Photo capture** with high-quality output
- **Video recording** (Premium)
- **Boomerang mode** for looping short videos (Premium)
- Self-timer with 3-second (free), 5-second, and 10-second (Premium) options
- Pinch-to-zoom gesture support
- Rule-of-thirds grid overlay (toggleable)
@ -34,7 +32,6 @@ Perfect for low-light selfies, content creation, video calls, makeup application
- **Skin Smoothing**: Real-time subtle skin smoothing filter
- **Center Stage**: Automatic subject tracking/centering
- **Extended Timers**: 5-second and 10-second self-timer options
- **Video & Boomerang**: Video recording and looping video capture
- Ad-free experience
### iCloud Sync
@ -139,15 +136,15 @@ Add `REVENUECAT_API_KEY` as a secret in your Xcode Cloud workflow.
## Privacy
- Camera access required for preview and capture
- Photo Library access required to save photos/videos
- Microphone access required for video recording
- Photo Library access required to save photos
- Microphone access may be requested by the camera framework (not actively used)
- iCloud access for settings synchronization (optional)
- No data collection, no analytics, no tracking
## Monetization
Freemium model with optional "Pro" subscription:
- **Free**: Basic ring light, standard colors (Pure White, Warm Cream), photo capture, 3s timer, grid, zoom
- **Pro**: Full color palette, custom colors, HDR, high quality, flash sync, true mirror, skin smoothing, center stage, extended timers, video, boomerang
- **Pro**: Full color palette, custom colors, HDR, high quality, flash sync, true mirror, skin smoothing, center stage, extended timers
Implemented with RevenueCat for reliable subscription management.

View File

@ -422,9 +422,9 @@
DEVELOPMENT_TEAM = 6R7KLBPBLZ;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_NSCameraUsageDescription = "SelfieCam needs camera access to display your live selfie preview, apply real-time filters and ring light effects, capture high-quality photos and videos, and enable advanced features like Center Stage auto-framing.";
INFOPLIST_KEY_NSMicrophoneUsageDescription = "SelfieCam needs microphone access to record clear audio when capturing videos, ensuring your voiceovers and ambient sounds are captured along with your video content.";
INFOPLIST_KEY_NSPhotoLibraryAddUsageDescription = "SelfieCam needs photo library access to automatically save your captured photos and videos to your device, making them available in the Photos app and other compatible applications.";
INFOPLIST_KEY_NSCameraUsageDescription = "SelfieCam needs camera access to display your live selfie preview, apply real-time filters and ring light effects, capture high-quality photos, and enable advanced features like Center Stage auto-framing.";
INFOPLIST_KEY_NSMicrophoneUsageDescription = "SelfieCam needs microphone access for the camera framework to initialize properly. Audio is not recorded.";
INFOPLIST_KEY_NSPhotoLibraryAddUsageDescription = "SelfieCam needs photo library access to automatically save your captured photos to your device, making them available in the Photos app and other compatible applications.";
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_BackgroundColor = LaunchBackground;
@ -459,9 +459,9 @@
DEVELOPMENT_TEAM = 6R7KLBPBLZ;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_NSCameraUsageDescription = "SelfieCam needs camera access to display your live selfie preview, apply real-time filters and ring light effects, capture high-quality photos and videos, and enable advanced features like Center Stage auto-framing.";
INFOPLIST_KEY_NSMicrophoneUsageDescription = "SelfieCam needs microphone access to record clear audio when capturing videos, ensuring your voiceovers and ambient sounds are captured along with your video content.";
INFOPLIST_KEY_NSPhotoLibraryAddUsageDescription = "SelfieCam needs photo library access to automatically save your captured photos and videos to your device, making them available in the Photos app and other compatible applications.";
INFOPLIST_KEY_NSCameraUsageDescription = "SelfieCam needs camera access to display your live selfie preview, apply real-time filters and ring light effects, capture high-quality photos, and enable advanced features like Center Stage auto-framing.";
INFOPLIST_KEY_NSMicrophoneUsageDescription = "SelfieCam needs microphone access for the camera framework to initialize properly. Audio is not recorded.";
INFOPLIST_KEY_NSPhotoLibraryAddUsageDescription = "SelfieCam needs photo library access to automatically save your captured photos to your device, making them available in the Photos app and other compatible applications.";
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_BackgroundColor = LaunchBackground;

View File

@ -39,7 +39,7 @@ struct ContentView: View {
.ignoresSafeArea() // Only camera ignores safe area to fill screen
}
// Photo review overlay
// Photo review overlay - handles its own safe area
if showPhotoReview, let photo = capturedPhoto {
PhotoReviewView(
photo: photo,
@ -53,11 +53,11 @@ struct ContentView: View {
}
)
.transition(.opacity)
.ignoresSafeArea() // Photo review also fills screen
}
}
// Settings button overlay - respects safe area naturally
// Settings button overlay - only show when NOT in photo review mode
.overlay(alignment: .topTrailing) {
if !showPhotoReview {
Button {
showSettings = true
} label: {
@ -72,6 +72,7 @@ struct ContentView: View {
.padding(.horizontal, Design.Spacing.large)
.padding(.top, Design.Spacing.small)
}
}
.animation(.easeInOut(duration: Design.Animation.quick), value: showPhotoReview)
.onAppear {
// Initialize tracking of camera position

View File

@ -1,6 +1,6 @@
//
// PhotoReviewView.swift
// CameraTester
// SelfieCam
//
// Created by Matt Bruce on 1/2/26.
//
@ -17,57 +17,80 @@ struct PhotoReviewView: View {
let onRetake: () -> Void
let onSave: () -> Void
// Layout constants
private let toolbarHeight: CGFloat = 100
private let topButtonSize: CGFloat = 44 // Retake, Close (smaller, out of the way)
private let bottomButtonSize: CGFloat = 64 // Share, Save (larger, main actions)
var body: some View {
ZStack {
// Photo display
// Black background
Color.black
.ignoresSafeArea()
// Photo - centered in available space
Image(uiImage: photo.image)
.resizable()
.scaledToFit()
.ignoresSafeArea()
}
// Top toolbar with gradient background
.overlay(alignment: .top) {
ZStack(alignment: .bottom) {
// Gradient background for visibility
LinearGradient(
colors: [Color.black.opacity(0.7), Color.black.opacity(0)],
startPoint: .top,
endPoint: .bottom
)
.frame(height: toolbarHeight)
.ignoresSafeArea(edges: .top)
// Top toolbar
VStack {
// Buttons
HStack {
// Retake button
Button(action: onRetake) {
Image(systemName: "arrow.triangle.2.circlepath")
.font(.system(size: 20, weight: .medium))
.foregroundColor(.white)
.frame(width: 44, height: 44)
.background(
Circle()
.fill(Color.black.opacity(0.6))
)
.font(.system(size: 18, weight: .semibold))
.foregroundStyle(.white)
.frame(width: topButtonSize, height: topButtonSize)
.background(.ultraThinMaterial, in: Circle())
}
.accessibilityLabel("Retake photo")
Spacer()
// Close button
Button(action: onRetake) {
Image(systemName: "xmark")
.font(.system(size: 20, weight: .medium))
.foregroundColor(.white)
.frame(width: 44, height: 44)
.background(
Circle()
.fill(Color.black.opacity(0.6))
)
.font(.system(size: 18, weight: .semibold))
.foregroundStyle(.white)
.frame(width: topButtonSize, height: topButtonSize)
.background(.ultraThinMaterial, in: Circle())
}
.accessibilityLabel("Close")
}
.padding(.horizontal, Design.Spacing.large)
.padding(.top, Design.Spacing.large)
.padding(.bottom, Design.Spacing.medium)
}
}
// Bottom toolbar with gradient background
.overlay(alignment: .bottom) {
ZStack(alignment: .top) {
// Gradient background for visibility
LinearGradient(
colors: [Color.black.opacity(0), Color.black.opacity(0.85)],
startPoint: .top,
endPoint: .bottom
)
.frame(height: 160)
.ignoresSafeArea(edges: .bottom)
Spacer()
// Bottom action bar
VStack(spacing: Design.Spacing.medium) {
// Save status or error
if let error = saveError {
Text(error)
.foregroundColor(.red)
.foregroundStyle(.red)
.font(.system(size: 14, weight: .medium))
.multilineTextAlignment(.center)
.padding(.vertical, Design.Spacing.small)
@ -81,38 +104,37 @@ struct PhotoReviewView: View {
ProgressView()
.tint(.white)
Text("Saving...")
.foregroundColor(.white)
.foregroundStyle(.white)
.font(.system(size: 16, weight: .medium))
}
.padding(.vertical, Design.Spacing.small)
}
// Action buttons
HStack(spacing: Design.Spacing.xLarge) {
// Share button
// Action buttons - same size, different styling
HStack(spacing: Design.Spacing.xLarge * 2) {
// Share button (frosted glass = secondary)
ShareButton(photo: photo.image)
.frame(width: bottomButtonSize, height: bottomButtonSize)
.background(.ultraThinMaterial, in: Circle())
// Save button
// Save button (solid white = primary)
Button(action: onSave) {
ZStack {
Circle()
.fill(Color.white)
.frame(width: 80, height: 80)
Image(systemName: "checkmark")
.font(.system(size: 24, weight: .bold))
.foregroundColor(.black)
}
.shadow(radius: 5)
.font(.system(size: 26, weight: .bold))
.foregroundStyle(.black)
.frame(width: bottomButtonSize, height: bottomButtonSize)
.background(.white, in: Circle())
.shadow(color: .black.opacity(0.3), radius: Design.Shadow.radiusMedium)
}
.disabled(isSaving)
.accessibilityLabel("Save photo")
.accessibilityHint("Saves the photo to your library")
}
}
.padding(.top, Design.Spacing.xLarge)
.padding(.bottom, Design.Spacing.large)
}
.padding(.horizontal, Design.Spacing.large)
}
}
.accessibilityElement(children: .contain)
.accessibilityLabel("Photo review")
.accessibilityHint("Use the buttons at the bottom to save or share your photo")
}
}

View File

@ -15,20 +15,15 @@ struct ShareButton: View {
@State private var isShareSheetPresented = false
var body: some View {
Button(action: {
Button {
isShareSheetPresented = true
}) {
ZStack {
Circle()
.fill(Color.black.opacity(0.6))
.frame(width: 80, height: 80)
} label: {
Image(systemName: "square.and.arrow.up")
.font(.system(size: 24, weight: .medium))
.foregroundColor(.white)
}
.shadow(radius: 5)
.font(.system(size: 20, weight: .medium))
.foregroundStyle(.white)
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
.accessibilityLabel("Share photo")
.sheet(isPresented: $isShareSheetPresented) {
ShareSheet(activityItems: [photo])
}

View File

@ -507,6 +507,7 @@
},
"Boomerang" : {
"comment" : "Display name for the \"Boomerang\" capture mode.",
"extractionState" : "stale",
"isCommentAutoGenerated" : true,
"localizations" : {
"es-MX" : {
@ -769,6 +770,10 @@
}
}
},
"Close" : {
"comment" : "A button label that closes the view.",
"isCommentAutoGenerated" : true
},
"Close preview" : {
"comment" : "A button label that closes the preview screen.",
"extractionState" : "stale",
@ -1691,6 +1696,7 @@
}
},
"Photo" : {
"extractionState" : "stale",
"localizations" : {
"es-MX" : {
"stringUnit" : {
@ -1957,6 +1963,10 @@
}
}
},
"Retake photo" : {
"comment" : "A button that, when tapped, allows the user to retake a photo.",
"isCommentAutoGenerated" : true
},
"Ring Light" : {
"localizations" : {
"es-MX" : {
@ -2148,6 +2158,10 @@
}
}
},
"Save photo" : {
"comment" : "A button that saves the currently displayed photo to the user's library.",
"isCommentAutoGenerated" : true
},
"Saved to Photos" : {
"comment" : "Text shown as a toast message when a photo is successfully saved to Photos.",
"extractionState" : "stale",
@ -2173,6 +2187,10 @@
}
}
},
"Saves the photo to your library" : {
"comment" : "An accessibility hint for the save button in the photo review view.",
"isCommentAutoGenerated" : true
},
"Saving..." : {
"comment" : "A text that appears while a photo is being saved.",
"isCommentAutoGenerated" : true,
@ -2390,6 +2408,10 @@
}
}
},
"Share photo" : {
"comment" : "An accessibility label for the share button.",
"isCommentAutoGenerated" : true
},
"Show colored light ring around camera preview" : {
"comment" : "Subtitle for the \"Enable Ring Light\" toggle in the Settings view.",
"isCommentAutoGenerated" : true,
@ -3016,6 +3038,7 @@
},
"Use the buttons at the bottom to save or share your photo" : {
"comment" : "An accessibility hint for the photo review view, instructing the user on how to interact with the view.",
"extractionState" : "stale",
"isCommentAutoGenerated" : true,
"localizations" : {
"es-MX" : {
@ -3065,6 +3088,7 @@
},
"Video" : {
"comment" : "Display name for the \"Video\" capture mode.",
"extractionState" : "stale",
"isCommentAutoGenerated" : true,
"localizations" : {
"es-MX" : {