Split SettingsViewModel into logical extensions

- TimerOption.swift - Timer enum moved to Models/
- SettingsViewModel+RingLight.swift - Ring light settings
- SettingsViewModel+Camera.swift - Camera, flash, HDR, timer, quality
- SettingsViewModel+Premium.swift - Debug toggle and premium reset
- SettingsViewModel+CloudSync.swift - iCloud sync conformance
- SettingsViewModel.swift - Core class with display/capture settings
This commit is contained in:
Matt Bruce 2026-01-04 18:23:12 -06:00
parent a3d7370f32
commit 60ea419a5b
6 changed files with 436 additions and 341 deletions

View File

@ -0,0 +1,105 @@
//
// SettingsViewModel+Camera.swift
// SelfieCam
//
// Created by Matt Bruce on 1/4/26.
//
import SwiftUI
import Bedrock
import MijickCamera
// MARK: - Camera Settings
extension SettingsViewModel {
// MARK: - Camera Position
/// Raw camera position string for comparison (avoids MijickCamera enum issues)
var cameraPositionRaw: String {
cloudSync.data.cameraPositionRaw
}
var cameraPosition: CameraPosition {
get {
let raw = cloudSync.data.cameraPositionRaw
let position: CameraPosition = raw == "front" ? .front : .back
Design.debugLog("cameraPosition getter: raw='\(raw)' -> \(position)")
return position
}
set {
let rawValue = newValue == .front ? "front" : "back"
Design.debugLog("cameraPosition setter: \(newValue) -> raw='\(rawValue)'")
updateSettings { $0.cameraPositionRaw = rawValue }
}
}
// MARK: - Flash Mode
var flashMode: CameraFlashMode {
get { CameraFlashMode(rawValue: cloudSync.data.flashModeRaw) ?? .off }
set { updateSettings { $0.flashModeRaw = newValue.rawValue } }
}
/// Whether flash is synced with ring light color (PREMIUM)
var isFlashSyncedWithRingLight: Bool {
get { PremiumGate.get(cloudSync.data.isFlashSyncedWithRingLight, default: false, isPremium: isPremiumUnlocked) }
set {
guard PremiumGate.canSet(isPremium: isPremiumUnlocked) else { return }
updateSettings { $0.isFlashSyncedWithRingLight = newValue }
}
}
// MARK: - HDR Mode (Premium)
/// HDR mode setting (PREMIUM)
var hdrMode: CameraHDRMode {
get {
let stored = CameraHDRMode(rawValue: cloudSync.data.hdrModeRaw) ?? .off
return PremiumGate.get(stored, default: .off, isPremium: isPremiumUnlocked)
}
set {
guard PremiumGate.canSet(isPremium: isPremiumUnlocked) else { return }
updateSettings { $0.hdrModeRaw = newValue.rawValue }
}
}
// MARK: - Photo Quality (High is Premium)
/// Photo quality setting (high is PREMIUM)
var photoQuality: PhotoQuality {
get {
let stored = PhotoQuality(rawValue: cloudSync.data.photoQualityRaw) ?? PhotoQuality.high
return PremiumGate.get(stored, default: .medium, premiumValues: Self.premiumPhotoQualities, isPremium: isPremiumUnlocked)
}
set {
guard PremiumGate.canSet(newValue, premiumValues: Self.premiumPhotoQualities, isPremium: isPremiumUnlocked) else { return }
updateSettings { $0.photoQualityRaw = newValue.rawValue }
}
}
// MARK: - Center Stage (Premium)
/// Whether Center Stage is enabled (PREMIUM)
var isCenterStageEnabled: Bool {
get { PremiumGate.get(cloudSync.data.isCenterStageEnabled, default: false, isPremium: isPremiumUnlocked) }
set {
guard PremiumGate.canSet(isPremium: isPremiumUnlocked) else { return }
updateSettings { $0.isCenterStageEnabled = newValue }
}
}
// MARK: - Timer (5s/10s are Premium)
/// Selected timer option (5s and 10s are PREMIUM)
var selectedTimer: TimerOption {
get {
let stored = TimerOption(rawValue: cloudSync.data.selectedTimerRaw) ?? .off
return PremiumGate.get(stored, default: .three, premiumValues: Self.premiumTimerOptions, isPremium: isPremiumUnlocked)
}
set {
guard PremiumGate.canSet(newValue, premiumValues: Self.premiumTimerOptions, isPremium: isPremiumUnlocked) else { return }
updateSettings { $0.selectedTimerRaw = newValue.rawValue }
}
}
}

View File

@ -0,0 +1,44 @@
//
// SettingsViewModel+CloudSync.swift
// SelfieCam
//
// Created by Matt Bruce on 1/4/26.
//
import Foundation
import Bedrock
// MARK: - CloudSyncable Conformance
extension SettingsViewModel: CloudSyncable {
/// Whether iCloud sync is available
var iCloudAvailable: Bool { cloudSync.iCloudAvailable }
/// Whether iCloud sync is enabled (available to all users)
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: - Sync Actions
/// Forces a sync with iCloud
func forceSync() {
cloudSync.sync()
}
/// Resets all settings to defaults
func resetToDefaults() {
cloudSync.reset()
}
}

View File

@ -0,0 +1,83 @@
//
// SettingsViewModel+Premium.swift
// SelfieCam
//
// Created by Matt Bruce on 1/4/26.
//
import SwiftUI
import Bedrock
// MARK: - Premium Features
extension SettingsViewModel {
// MARK: - Debug Premium Toggle
/// Debug premium toggle for testing (DEBUG builds only)
var isDebugPremiumEnabled: Bool {
get { premiumManager.isDebugPremiumToggleEnabled }
set {
let wasPremium = premiumManager.isDebugPremiumToggleEnabled
premiumManager.isDebugPremiumToggleEnabled = newValue
// Update premium status for UI refresh
isPremiumUnlocked = premiumManager.isPremiumUnlocked
// Reset premium settings when toggling OFF
if wasPremium && !newValue {
resetPremiumSettingsToDefaults()
}
}
}
// MARK: - Premium Reset
/// Reset premium-only settings to their free defaults when debug premium is disabled
func resetPremiumSettingsToDefaults() {
// Clear cached values that depend on premium status
_cachedRingSize = nil
_cachedLightColorId = nil
_cachedCustomColor = nil
// Reset premium settings to defaults (bypass premium gates for direct reset)
updateSettings { settings in
if settings.lightColorId != Self.defaultFreeColorId {
settings.lightColorId = Self.defaultFreeColorId
}
if settings.isMirrorFlipped {
settings.isMirrorFlipped = false
}
if settings.isSkinSmoothingEnabled {
settings.isSkinSmoothingEnabled = false
}
if settings.isFlashSyncedWithRingLight {
settings.isFlashSyncedWithRingLight = false
}
if settings.hdrModeRaw != CameraHDRMode.off.rawValue {
settings.hdrModeRaw = CameraHDRMode.off.rawValue
}
if settings.photoQualityRaw != PhotoQuality.medium.rawValue {
settings.photoQualityRaw = PhotoQuality.medium.rawValue
}
// Only reset timer if it's a premium option (5s or 10s)
let currentTimer = TimerOption(rawValue: settings.selectedTimerRaw) ?? .three
if Self.premiumTimerOptions.contains(currentTimer) {
settings.selectedTimerRaw = TimerOption.three.rawValue
}
if settings.isCenterStageEnabled {
settings.isCenterStageEnabled = false
}
}
// Force sync to ensure changes are saved
forceSync()
}
}

View File

@ -0,0 +1,113 @@
//
// SettingsViewModel+RingLight.swift
// SelfieCam
//
// Created by Matt Bruce on 1/4/26.
//
import SwiftUI
import Bedrock
// MARK: - Ring Light Settings
extension SettingsViewModel {
// MARK: - Ring Size
/// Ring border size in points (debounced save)
var ringSize: CGFloat {
get { _cachedRingSize ?? cloudSync.data.ringSize }
set {
_cachedRingSize = newValue
debouncedSave(key: "ringSize") {
self._cachedRingSize = nil
self.updateSettings { $0.ringSize = newValue }
}
}
}
/// Convenience property for border width (same as ringSize)
var borderWidth: CGFloat { ringSize }
// MARK: - Ring Light Enabled/Opacity
var isRingLightEnabled: Bool {
get { cloudSync.data.isRingLightEnabled }
set { updateSettings { $0.isRingLightEnabled = newValue } }
}
var ringLightOpacity: Double {
get { cloudSync.data.ringLightOpacity }
set { updateSettings { $0.ringLightOpacity = newValue } }
}
// MARK: - Light Color
/// ID of the selected light color preset
var lightColorId: String {
get {
let storedId = _cachedLightColorId ?? cloudSync.data.lightColorId
return PremiumGate.get(storedId, default: Self.defaultFreeColorId, premiumValues: Self.premiumColorIds, isPremium: isPremiumUnlocked)
}
set {
guard PremiumGate.canSet(newValue, premiumValues: Self.premiumColorIds, isPremium: isPremiumUnlocked) else { return }
_cachedLightColorId = newValue
updateSettings { $0.lightColorId = newValue }
}
}
/// Custom color for ring light (premium feature, debounced save)
var customColor: Color {
get {
_cachedCustomColor ?? Color(
red: cloudSync.data.customColorRed,
green: cloudSync.data.customColorGreen,
blue: cloudSync.data.customColorBlue
)
}
set {
guard PremiumGate.canSet(isPremium: isPremiumUnlocked) else { return }
_cachedCustomColor = newValue
let rgb = CustomColorRGB(from: newValue)
debouncedSave(key: "customColor") {
self._cachedCustomColor = nil
self.updateSettings {
$0.customColorRed = rgb.red
$0.customColorGreen = rgb.green
$0.customColorBlue = rgb.blue
}
}
}
}
/// The currently selected light color preset
var selectedLightColor: RingLightColor {
get { RingLightColor.fromId(lightColorId, customColor: customColor) }
set {
lightColorId = newValue.id
if newValue.isCustom {
customColor = newValue.color
}
}
}
/// The actual color to use for the ring light
var lightColor: Color {
if lightColorId == RingLightColor.customId {
return customColor
}
return selectedLightColor.color
}
/// Whether custom color is currently selected
var isCustomColorSelected: Bool {
lightColorId == RingLightColor.customId
}
/// Sets the custom color and selects it (PREMIUM)
func selectCustomColor(_ color: Color) {
guard PremiumGate.canSet(isPremium: isPremiumUnlocked) else { return }
customColor = color
lightColorId = RingLightColor.customId
}
}

View File

@ -1,44 +1,28 @@
//
// SettingsViewModel.swift
// SelfieCam
//
// Created by Matt Bruce on 1/4/26.
//
import SwiftUI
import Bedrock
import MijickCamera
// 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: - Settings ViewModel
/// Observable settings view model with iCloud sync across all devices.
/// Uses Bedrock's CloudSyncManager for automatic synchronization.
/// Premium features are automatically reset to defaults when user doesn't have premium.
///
/// Extensions:
/// - `SettingsViewModel+RingLight.swift` - Ring light settings
/// - `SettingsViewModel+Camera.swift` - Camera settings (flash, HDR, timer, etc.)
/// - `SettingsViewModel+Premium.swift` - Premium/debug toggle and reset logic
/// - `SettingsViewModel+CloudSync.swift` - iCloud sync conformance
@MainActor
@Observable
final class SettingsViewModel: RingLightConfigurable, CloudSyncable {
final class SettingsViewModel: RingLightConfigurable {
// MARK: - Ring Size Limits
@ -51,100 +35,58 @@ final class SettingsViewModel: RingLightConfigurable, CloudSyncable {
/// Default ring border size
static let defaultRingSize: CGFloat = 40
// MARK: - Premium Manager
/// Premium manager for checking subscription status
@ObservationIgnored private let premiumManager = PremiumManager()
/// Whether the user has premium access (stored property for proper UI updates)
private var _isPremiumUnlocked: Bool = false
/// Whether the user has premium access (observable for UI updates)
var isPremiumUnlocked: Bool {
get { _isPremiumUnlocked }
set {
_isPremiumUnlocked = newValue
}
}
// MARK: - Premium Constants
// MARK: - Cloud Sync Manager
/// Default free color ID for non-premium users
static let defaultFreeColorId = "pureWhite"
/// Premium color IDs that require subscription
static let premiumColorIds: Set<String> = ["iceBlue", "softPink", "warmAmber", "coolLavender", RingLightColor.customId]
/// Premium timer options that require subscription
static let premiumTimerOptions: Set<TimerOption> = [.five, .ten]
/// Premium photo quality options that require subscription
static let premiumPhotoQualities: Set<PhotoQuality> = [.high]
// MARK: - Internal Dependencies
/// Premium manager for checking subscription status
@ObservationIgnored let premiumManager = PremiumManager()
/// Manages iCloud sync for settings across all devices
private let cloudSync = CloudSyncManager<SyncedSettings>()
let cloudSync = CloudSyncManager<SyncedSettings>()
/// Debounce task for slider values
private var debounceTask: Task<Void, Never>?
var debounceTask: Task<Void, Never>?
/// Debounce delay for continuous slider updates (in seconds)
private static let debounceDelay: Duration = .milliseconds(300)
// MARK: - Cached Values (for immediate UI updates)
/// Cached ring size for immediate UI updates (before debounced save)
private var _cachedRingSize: CGFloat?
// MARK: - Observable Properties (Synced)
/// Ring border size in points (debounced save)
var ringSize: CGFloat {
get { _cachedRingSize ?? cloudSync.data.ringSize }
set {
_cachedRingSize = newValue
debouncedSave(key: "ringSize") {
self._cachedRingSize = nil
self.updateSettings { $0.ringSize = newValue }
}
}
}
var _cachedRingSize: CGFloat?
/// Cached light color ID for immediate UI updates
private var _cachedLightColorId: String?
/// Default free color ID for non-premium users
private static let defaultFreeColorId = "pureWhite"
/// Premium color IDs that require subscription
private static let premiumColorIds: Set<String> = ["iceBlue", "softPink", "warmAmber", "coolLavender", RingLightColor.customId]
/// ID of the selected light color preset
var lightColorId: String {
get {
let storedId = _cachedLightColorId ?? cloudSync.data.lightColorId
return PremiumGate.get(storedId, default: Self.defaultFreeColorId, premiumValues: Self.premiumColorIds, isPremium: isPremiumUnlocked)
}
set {
guard PremiumGate.canSet(newValue, premiumValues: Self.premiumColorIds, isPremium: isPremiumUnlocked) else { return }
_cachedLightColorId = newValue
updateSettings { $0.lightColorId = newValue }
}
}
var _cachedLightColorId: String?
/// Cached custom color for immediate UI updates
private var _cachedCustomColor: Color?
var _cachedCustomColor: Color?
/// Custom color for ring light (premium feature, debounced save)
/// Note: Getter always returns stored value to preserve user's choice if they re-subscribe
var customColor: Color {
get {
_cachedCustomColor ?? Color(
red: cloudSync.data.customColorRed,
green: cloudSync.data.customColorGreen,
blue: cloudSync.data.customColorBlue
)
}
set {
guard PremiumGate.canSet(isPremium: isPremiumUnlocked) else { return }
_cachedCustomColor = newValue
let rgb = CustomColorRGB(from: newValue)
debouncedSave(key: "customColor") {
self._cachedCustomColor = nil
self.updateSettings {
$0.customColorRed = rgb.red
$0.customColorGreen = rgb.green
$0.customColorBlue = rgb.blue
}
}
}
// MARK: - Premium Status
/// Whether the user has premium access (stored property for proper UI updates)
private var _isPremiumUnlocked: Bool = false
/// Whether the user has premium access (observable for UI updates)
var isPremiumUnlocked: Bool {
get { _isPremiumUnlocked }
set { _isPremiumUnlocked = newValue }
}
// MARK: - Display Settings
/// Whether the camera preview is flipped to show a true mirror (PREMIUM)
var isMirrorFlipped: Bool {
get { PremiumGate.get(cloudSync.data.isMirrorFlipped, default: false, isPremium: isPremiumUnlocked) }
@ -175,242 +117,26 @@ final class SettingsViewModel: RingLightConfigurable, CloudSyncable {
set { updateSettings { $0.currentZoomFactor = newValue } }
}
// MARK: - Capture Settings
/// Whether captures are auto-saved to Photo Library
var isAutoSaveEnabled: Bool {
get { cloudSync.data.isAutoSaveEnabled }
set { updateSettings { $0.isAutoSaveEnabled = newValue } }
}
// MARK: - Computed Properties
/// Convenience property for border width (same as ringSize)
var borderWidth: CGFloat { ringSize }
/// Premium timer options that require subscription
private static let premiumTimerOptions: Set<TimerOption> = [.five, .ten]
/// Selected timer option (5s and 10s are PREMIUM)
var selectedTimer: TimerOption {
get {
let stored = TimerOption(rawValue: cloudSync.data.selectedTimerRaw) ?? .off
return PremiumGate.get(stored, default: .three, premiumValues: Self.premiumTimerOptions, isPremium: isPremiumUnlocked)
}
set {
guard PremiumGate.canSet(newValue, premiumValues: Self.premiumTimerOptions, isPremium: isPremiumUnlocked) else { return }
updateSettings { $0.selectedTimerRaw = newValue.rawValue }
}
}
// MARK: - Camera Settings
var flashMode: CameraFlashMode {
get { CameraFlashMode(rawValue: cloudSync.data.flashModeRaw) ?? .off }
set { updateSettings { $0.flashModeRaw = newValue.rawValue } }
}
/// Whether flash is synced with ring light color (PREMIUM)
var isFlashSyncedWithRingLight: Bool {
get { PremiumGate.get(cloudSync.data.isFlashSyncedWithRingLight, default: false, isPremium: isPremiumUnlocked) }
set {
guard PremiumGate.canSet(isPremium: isPremiumUnlocked) else { return }
updateSettings { $0.isFlashSyncedWithRingLight = newValue }
}
}
/// HDR mode setting (PREMIUM)
var hdrMode: CameraHDRMode {
get {
let stored = CameraHDRMode(rawValue: cloudSync.data.hdrModeRaw) ?? .off
return PremiumGate.get(stored, default: .off, isPremium: isPremiumUnlocked)
}
set {
guard PremiumGate.canSet(isPremium: isPremiumUnlocked) else { return }
updateSettings { $0.hdrModeRaw = newValue.rawValue }
}
}
/// Premium photo quality options that require subscription
private static let premiumPhotoQualities: Set<PhotoQuality> = [.high]
/// Photo quality setting (high is PREMIUM)
var photoQuality: PhotoQuality {
get {
let stored = PhotoQuality(rawValue: cloudSync.data.photoQualityRaw) ?? PhotoQuality.high
return PremiumGate.get(stored, default: .medium, premiumValues: Self.premiumPhotoQualities, isPremium: isPremiumUnlocked)
}
set {
guard PremiumGate.canSet(newValue, premiumValues: Self.premiumPhotoQualities, isPremium: isPremiumUnlocked) else { return }
updateSettings { $0.photoQualityRaw = newValue.rawValue }
}
}
/// Raw camera position string for comparison (avoids MijickCamera enum issues)
var cameraPositionRaw: String {
cloudSync.data.cameraPositionRaw
}
var cameraPosition: CameraPosition {
get {
let raw = cloudSync.data.cameraPositionRaw
let position: CameraPosition = raw == "front" ? .front : .back
Design.debugLog("cameraPosition getter: raw='\(raw)' -> \(position)")
return position
}
set {
let rawValue = newValue == .front ? "front" : "back"
Design.debugLog("cameraPosition setter: \(newValue) -> raw='\(rawValue)'")
updateSettings { $0.cameraPositionRaw = rawValue }
}
}
var isRingLightEnabled: Bool {
get { cloudSync.data.isRingLightEnabled }
set { updateSettings { $0.isRingLightEnabled = newValue } }
}
var ringLightOpacity: Double {
get { cloudSync.data.ringLightOpacity }
set { updateSettings { $0.ringLightOpacity = newValue } }
}
/// Whether Center Stage is enabled (PREMIUM)
var isCenterStageEnabled: Bool {
get { PremiumGate.get(cloudSync.data.isCenterStageEnabled, default: false, isPremium: isPremiumUnlocked) }
set {
guard PremiumGate.canSet(isPremium: isPremiumUnlocked) else { return }
updateSettings { $0.isCenterStageEnabled = newValue }
}
}
var selectedLightColor: RingLightColor {
get { RingLightColor.fromId(lightColorId, customColor: customColor) }
set {
// Premium check handled by lightColorId and customColor setters
lightColorId = newValue.id
if newValue.isCustom {
customColor = newValue.color
}
}
}
var lightColor: Color {
if lightColorId == RingLightColor.customId {
return customColor
}
return selectedLightColor.color
}
/// Whether custom color is currently selected
var isCustomColorSelected: Bool {
lightColorId == RingLightColor.customId
}
// MARK: - Debug Premium Toggle
/// Debug premium toggle for testing (DEBUG builds only)
var isDebugPremiumEnabled: Bool {
get { premiumManager.isDebugPremiumToggleEnabled }
set {
let wasPremium = premiumManager.isDebugPremiumToggleEnabled
premiumManager.isDebugPremiumToggleEnabled = newValue
// Update premium status for UI refresh
isPremiumUnlocked = premiumManager.isPremiumUnlocked
// Reset premium settings when toggling OFF
if wasPremium && !newValue {
resetPremiumSettingsToDefaults()
}
}
}
/// Reset premium-only settings to their free defaults when debug premium is disabled
private func resetPremiumSettingsToDefaults() {
// Clear cached values that depend on premium status
_cachedRingSize = nil
_cachedLightColorId = nil
_cachedCustomColor = nil
// Reset premium settings to defaults (bypass premium gates for direct reset)
updateSettings { settings in
if settings.lightColorId != Self.defaultFreeColorId {
settings.lightColorId = Self.defaultFreeColorId
}
if settings.isMirrorFlipped {
settings.isMirrorFlipped = false
}
if settings.isSkinSmoothingEnabled {
settings.isSkinSmoothingEnabled = false
}
if settings.isFlashSyncedWithRingLight {
settings.isFlashSyncedWithRingLight = false
}
if settings.hdrModeRaw != CameraHDRMode.off.rawValue {
settings.hdrModeRaw = CameraHDRMode.off.rawValue
}
if settings.photoQualityRaw != PhotoQuality.medium.rawValue {
settings.photoQualityRaw = PhotoQuality.medium.rawValue
}
// Only reset timer if it's a premium option (5s or 10s)
let currentTimer = TimerOption(rawValue: settings.selectedTimerRaw) ?? .three
if Self.premiumTimerOptions.contains(currentTimer) {
settings.selectedTimerRaw = TimerOption.three.rawValue
}
if settings.isCenterStageEnabled {
settings.isCenterStageEnabled = false
}
}
// Force sync to ensure changes are saved
forceSync()
}
/// Sets the custom color and selects it (PREMIUM)
func selectCustomColor(_ color: Color) {
guard PremiumGate.canSet(isPremium: isPremiumUnlocked) else { return }
customColor = color
lightColorId = RingLightColor.customId
}
// MARK: - Sync Status
/// Whether iCloud sync is available
var iCloudAvailable: Bool { cloudSync.iCloudAvailable }
/// Whether iCloud sync is enabled (available to all users)
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() {
// Initialize premium status
_isPremiumUnlocked = premiumManager.isPremiumUnlocked
// CloudSyncManager handles syncing automatically
}
// MARK: - Private Methods
// MARK: - Internal Methods
/// Updates settings and saves to cloud immediately
private func updateSettings(_ transform: (inout SyncedSettings) -> Void) {
func updateSettings(_ transform: (inout SyncedSettings) -> Void) {
cloudSync.update { settings in
transform(&settings)
settings.modificationCount += 1
@ -418,7 +144,7 @@ final class SettingsViewModel: RingLightConfigurable, CloudSyncable {
}
/// Debounces save operations for continuous values like sliders
private func debouncedSave(key: String, action: @escaping () -> Void) {
func debouncedSave(key: String, action: @escaping () -> Void) {
// Cancel any pending debounce
debounceTask?.cancel()
@ -432,18 +158,6 @@ final class SettingsViewModel: RingLightConfigurable, CloudSyncable {
}
}
// 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 {

View File

@ -0,0 +1,36 @@
//
// TimerOption.swift
// SelfieCam
//
// Created by Matt Bruce on 1/4/26.
//
import Foundation
/// Self-timer duration 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
}
}
}