SelfieRingLight/SelfieRingLight/Features/Settings/SettingsViewModel.swift
Matt Bruce 74e65829de Initial commit: SelfieRingLight app
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
2026-01-02 13:01:24 -06:00

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
}
}