Features: - Camera preview with ring light effect - Adjustable ring size with slider - Light color presets (white, warm cream, ice blue, soft pink, warm amber, cool lavender) - Light intensity control (opacity) - Front flash (hides preview during capture) - True mirror mode - Skin smoothing toggle - Grid overlay (rule of thirds) - Self-timer options - Photo and video capture modes - iCloud sync for settings across devices Architecture: - SwiftUI with @Observable view models - Protocol-oriented design (RingLightConfigurable, CaptureControlling) - Bedrock design system integration - CloudSyncManager for iCloud settings sync - RevenueCat for premium features
234 lines
6.3 KiB
Swift
234 lines
6.3 KiB
Swift
import SwiftUI
|
|
import Bedrock
|
|
|
|
// MARK: - Timer Options
|
|
|
|
enum TimerOption: String, CaseIterable, Identifiable {
|
|
case off = "off"
|
|
case three = "3"
|
|
case five = "5"
|
|
case ten = "10"
|
|
|
|
var id: String { rawValue }
|
|
|
|
var displayName: String {
|
|
switch self {
|
|
case .off: return String(localized: "Off")
|
|
case .three: return String(localized: "3s")
|
|
case .five: return String(localized: "5s")
|
|
case .ten: return String(localized: "10s")
|
|
}
|
|
}
|
|
|
|
var seconds: Int {
|
|
switch self {
|
|
case .off: return 0
|
|
case .three: return 3
|
|
case .five: return 5
|
|
case .ten: return 10
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Capture Mode
|
|
|
|
enum CaptureMode: String, CaseIterable, Identifiable {
|
|
case photo = "photo"
|
|
case video = "video"
|
|
case boomerang = "boomerang"
|
|
|
|
var id: String { rawValue }
|
|
|
|
var displayName: String {
|
|
switch self {
|
|
case .photo: return String(localized: "Photo")
|
|
case .video: return String(localized: "Video")
|
|
case .boomerang: return String(localized: "Boomerang")
|
|
}
|
|
}
|
|
|
|
var systemImage: String {
|
|
switch self {
|
|
case .photo: return "camera.fill"
|
|
case .video: return "video.fill"
|
|
case .boomerang: return "arrow.2.squarepath"
|
|
}
|
|
}
|
|
|
|
var isPremium: Bool {
|
|
switch self {
|
|
case .photo: return false
|
|
case .video, .boomerang: return true
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Settings ViewModel
|
|
|
|
/// Observable settings view model with iCloud sync across all devices.
|
|
/// Uses Bedrock's CloudSyncManager for automatic synchronization.
|
|
@MainActor
|
|
@Observable
|
|
final class SettingsViewModel: RingLightConfigurable {
|
|
|
|
// MARK: - Ring Size Limits
|
|
|
|
/// Minimum ring border size in points
|
|
static let minRingSize: CGFloat = 10
|
|
|
|
/// Maximum ring border size in points
|
|
static let maxRingSize: CGFloat = 120
|
|
|
|
/// Default ring border size
|
|
static let defaultRingSize: CGFloat = 40
|
|
|
|
// MARK: - Cloud Sync Manager
|
|
|
|
/// Manages iCloud sync for settings across all devices
|
|
private let cloudSync = CloudSyncManager<SyncedSettings>()
|
|
|
|
// MARK: - Observable Properties (Synced)
|
|
|
|
/// Ring border size in points
|
|
var ringSize: CGFloat {
|
|
get { cloudSync.data.ringSize }
|
|
set { updateSettings { $0.ringSize = newValue } }
|
|
}
|
|
|
|
/// ID of the selected light color preset
|
|
var lightColorId: String {
|
|
get { cloudSync.data.lightColorId }
|
|
set { updateSettings { $0.lightColorId = newValue } }
|
|
}
|
|
|
|
/// Ring light intensity/opacity (0.5 to 1.0)
|
|
var lightIntensity: Double {
|
|
get { cloudSync.data.lightIntensity }
|
|
set { updateSettings { $0.lightIntensity = newValue } }
|
|
}
|
|
|
|
/// Whether front flash is enabled (hides preview during capture)
|
|
var isFrontFlashEnabled: Bool {
|
|
get { cloudSync.data.isFrontFlashEnabled }
|
|
set { updateSettings { $0.isFrontFlashEnabled = newValue } }
|
|
}
|
|
|
|
/// Whether the camera preview is flipped to show a true mirror
|
|
var isMirrorFlipped: Bool {
|
|
get { cloudSync.data.isMirrorFlipped }
|
|
set { updateSettings { $0.isMirrorFlipped = newValue } }
|
|
}
|
|
|
|
/// Whether skin smoothing filter is enabled
|
|
var isSkinSmoothingEnabled: Bool {
|
|
get { cloudSync.data.isSkinSmoothingEnabled }
|
|
set { updateSettings { $0.isSkinSmoothingEnabled = newValue } }
|
|
}
|
|
|
|
/// Whether the grid overlay is visible
|
|
var isGridVisible: Bool {
|
|
get { cloudSync.data.isGridVisible }
|
|
set { updateSettings { $0.isGridVisible = newValue } }
|
|
}
|
|
|
|
/// Current camera zoom factor
|
|
var currentZoomFactor: Double {
|
|
get { cloudSync.data.currentZoomFactor }
|
|
set { updateSettings { $0.currentZoomFactor = newValue } }
|
|
}
|
|
|
|
// MARK: - Computed Properties
|
|
|
|
/// Convenience property for border width (same as ringSize)
|
|
var borderWidth: CGFloat { ringSize }
|
|
|
|
var selectedTimer: TimerOption {
|
|
get { TimerOption(rawValue: cloudSync.data.selectedTimerRaw) ?? .off }
|
|
set { updateSettings { $0.selectedTimerRaw = newValue.rawValue } }
|
|
}
|
|
|
|
var selectedCaptureMode: CaptureMode {
|
|
get { CaptureMode(rawValue: cloudSync.data.selectedCaptureModeRaw) ?? .photo }
|
|
set { updateSettings { $0.selectedCaptureModeRaw = newValue.rawValue } }
|
|
}
|
|
|
|
var selectedLightColor: RingLightColor {
|
|
get { RingLightColor.fromId(lightColorId) }
|
|
set { lightColorId = newValue.id }
|
|
}
|
|
|
|
var lightColor: Color {
|
|
selectedLightColor.color
|
|
}
|
|
|
|
// MARK: - Sync Status
|
|
|
|
/// Whether iCloud sync is available
|
|
var iCloudAvailable: Bool { cloudSync.iCloudAvailable }
|
|
|
|
/// Whether iCloud sync is enabled
|
|
var iCloudEnabled: Bool {
|
|
get { cloudSync.iCloudEnabled }
|
|
set { cloudSync.iCloudEnabled = newValue }
|
|
}
|
|
|
|
/// Last sync date
|
|
var lastSyncDate: Date? { cloudSync.lastSyncDate }
|
|
|
|
/// Current sync status message
|
|
var syncStatus: String { cloudSync.syncStatus }
|
|
|
|
/// Whether initial sync has completed
|
|
var hasCompletedInitialSync: Bool { cloudSync.hasCompletedInitialSync }
|
|
|
|
// MARK: - Initialization
|
|
|
|
init() {
|
|
// CloudSyncManager handles syncing automatically
|
|
}
|
|
|
|
// MARK: - Private Methods
|
|
|
|
/// Updates settings and saves to cloud
|
|
private func updateSettings(_ transform: (inout SyncedSettings) -> Void) {
|
|
cloudSync.update { settings in
|
|
transform(&settings)
|
|
settings.modificationCount += 1
|
|
}
|
|
}
|
|
|
|
// MARK: - Sync Actions
|
|
|
|
/// Forces a sync with iCloud
|
|
func forceSync() {
|
|
cloudSync.sync()
|
|
}
|
|
|
|
/// Resets all settings to defaults
|
|
func resetToDefaults() {
|
|
cloudSync.reset()
|
|
}
|
|
|
|
// MARK: - Validation
|
|
|
|
var isValidConfiguration: Bool {
|
|
ringSize >= Self.minRingSize && lightIntensity >= 0.5
|
|
}
|
|
}
|
|
|
|
// MARK: - CaptureControlling Conformance
|
|
|
|
extension SettingsViewModel: CaptureControlling {
|
|
func startCountdown() async {
|
|
// Countdown handled by CameraViewModel
|
|
}
|
|
|
|
func performCapture() {
|
|
// Capture handled by CameraViewModel
|
|
}
|
|
|
|
func performFlashBurst() async {
|
|
// Flash handled by CameraViewModel
|
|
}
|
|
}
|