Skip to content

Dimension-North-Inc/Defaults

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

7 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Defaults

Swift 6.0 Platforms SPM

A type-safe, centralized alternative to SwiftUI's @AppStorage for managing user defaults with compile-time safety, key reuse, and built-in cloud synchronization.

Why Defaults?

The Problem with @AppStorage

SwiftUI's @AppStorage is stringly typed—you define keys as raw strings everywhere you need them:

// View A
struct SettingsView: View {
    @AppStorage("useNSFW") private var useNSFW = true
    @AppStorage("gridColumns") private var gridColumns = 4
}

// View B - must retype strings, prone to typos
struct GalleryView: View {
    @AppStorage("useNSFW") private var useNSFW = true  // retyped
    @AppStorage("gridColumns") private var gridColumns = 4  // retyped
}

// ViewModel C - same problem
class ContentViewModel: ObservableObject {
    @Published var useNSFW: Bool {
        didSet { UserDefaults.standard.set(useNSFW, forKey: "useNSFW") }
    }
}

This approach:

  • Duplicates string literals across your codebase
  • Provides no compile-time safety—misspelling a key compiles silently
  • Scatters default values throughout views instead of centralizing them
  • Makes validation inconsistent—each view can implement its own (or none)

The Defaults Solution

Define your keys once with types, defaults, and optional validation:

// Keys.swift - Define all keys in one place
extension DefaultKeys {
    var useNSFW: DefaultKey<Bool> {
        DefaultKey("useNSFW", value: true)
    }

    var gridColumns: DefaultKey<Int> {
        DefaultKey("gridColumns", value: 4, validate: { max(1, min($0, 12)) })
    }
}

Then use them everywhere with full type safety:

struct SettingsView: View {
    @Default(\.useNSFW) private var useNSFW
    @Default(\.gridColumns) private var gridColumns
}

struct GalleryView: View {
    @Default(\.useNSFW) private var useNSFW
    @Default(\.gridColumns) private var gridColumns
}

class ContentViewModel: ObservableObject {
    @DefaultValue(\.useNSFW) var useNSFW: Bool
    @DefaultValue(\.gridColumns) var gridColumns: Int
}

Benefits:

  • Keys defined once, used everywhere—single source of truth
  • Compile-time safety—typos caught by the compiler
  • Centralized defaults—change once, applies everywhere
  • Built-in validation—optional validate closure transforms values on write
  • Cloud sync ready—supports NSUbiquitousKeyValueStore out of the box

@Default vs @DefaultValue

Property Wrapper Context SwiftUI Integration Purpose
@Default SwiftUI Views Provides Binding<T> via projectedValue For views that need to bind directly to controls like Toggle or Slider
@DefaultValue ViewModels, Services, Non-Views Provides ObservableDefault<T> via projectedValue For programmatic access with onChange callbacks

@Default — For SwiftUI Views

Use in views where you need SwiftUI's Binding integration:

struct SettingsView: View {
    @Default(\.useNSFW) private var useNSFW
    @Default(\.gridColumns) private var gridColumns

    var body: some View {
        Form {
            Toggle("Show NSFW Content", isOn: $useNSFW)

            Stepper("Grid Columns: \(gridColumns)", value: $gridColumns, in: 1...12)
        }
    }
}

@DefaultValue — For ViewModels & Non-Views

Use in view models or services where you need onChange callbacks:

class GalleryViewModel: ObservableObject {
    @DefaultValue(\.useNSFW) var useNSFW: Bool
    @DefaultValue(\.gridColumns) var gridColumns: Int

    init() {
        // React to changes via the projected value ($useNSFW)
        $useNSFW.onChange { newValue in
            self.filterContent()
        }
    }

    private func filterContent() {
        // Handle the change
    }
}

Creating a New Default Key

1. Define the Key

Add to your DefaultKeys extension:

Note: DefaultKey initializer uses positional key and labeled value parameters: DefaultKey("keyName", value: defaultValue)

extension DefaultKeys {
    var mySetting: DefaultKey<Bool> {
        DefaultKey("mySetting", value: false)
    }

    var username: DefaultKey<String> {
        DefaultKey("username", value: "")
    }

    var threshold: DefaultKey<Double> {
        DefaultKey(
            "threshold",
            value: 0.5,
            validate: { max(0.0, min(1.0, $0)) }  // clamp to 0-1
        )
    }
}

2. Best Practice: Use Value for Non-Standard Types

When a style key's value type is a custom type that isn't a built-in Swift type (e.g., Bool, Int, String, Double), name the type Value nested inside the style struct. This avoids conflicts with SwiftUI types that share common names like ColorScheme, Alignment, Edge, etc., and makes the pattern consistent across all custom types.

Example — a ColorScheme style without the naming convention:

// Conflict: ColorScheme exists in SwiftUI
public enum ColorScheme: Codable, Hashable {
    case system, light, dark
}
public struct ColorSchemeStyle {
    public var scheme: ColorScheme  // compiles but shadows SwiftUI.ColorScheme
}

Correct approach — use Value for the nested type:

public struct ColorSchemeStyle: StyleKeys, Codable, Hashable {
    public enum Value: Codable, Hashable {
        case system, light, dark
    }
    public var value: Value
    public init(_ value: Value = .system) {
        self.value = value
    }
    public static var name: String { "outline.colorScheme" }
    public static var initial: Value { .system }
}

extension Style.Keys {
    var colorScheme: DefaultKey<ColorSchemeStyle> {
        DefaultKey(ColorSchemeStyle.name, value: ColorSchemeStyle.initial)
    }
}

This convention ensures that when a DefaultKey<ColorSchemeStyle> is used, ColorSchemeStyle.Value is unambiguous and doesn't shadow SwiftUI.ColorScheme. Apply the same pattern for any custom non-built-in type — the Value wrapper avoids name collisions regardless of whether the underlying type is an enum, a struct, or any other custom type.

The convention also makes call-sites natural: IndentationStyle.Value reads clearly and is concise. It also makes call sites searchable — searching for .Value across a codebase surfaces usages of custom style values without noise from generic Any/AnyObject casts.

2. Use in a SwiftUI View

struct ProfileView: View {
    @Default(\.username) private var username

    var body: some View {
        TextField("Username", text: $username)
    }
}

3. Use in a ViewModel

class ProfileViewModel: ObservableObject {
    @DefaultValue(\.username) var username: String

    func validate() -> Bool {
        return !username.isEmpty
    }
}

Cloud Synchronization

Defaults supports NSUbiquitousKeyValueStore for automatic iCloud sync:

// Use cloud storage instead of local
@Default(\.mySetting, storage: .cloud(.ubiquitous)) private var mySetting

// Or with explicit store
@Default(\.mySetting, storage: .local(UserDefaults(suiteName: "group.com.app")!)) private var mySetting

// Using the group helper
@Default(\.mySetting, storage: .group("group.com.app")) private var mySetting

Installation

Swift Package Manager

// Package.swift
dependencies: [
    .package(url: "https://github.com/your-org/Defaults.git", from: "1.0.0")
]
// Your project
.target(
    name: "YourTarget",
    dependencies: ["Defaults"]
)

Requirements

Platform Minimum Version
macOS 11.0 (Big Sur)
iOS 14.0
Swift 6.0

License

Copyright © 2024 Dimension North Inc. All rights reserved.

About

type-safe, centralized alternative to SwiftUI's `@AppStorage`

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages