A type-safe, centralized alternative to SwiftUI's @AppStorage for managing user defaults with compile-time safety, key reuse, and built-in cloud synchronization.
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)
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
validateclosure transforms values on write - Cloud sync ready—supports
NSUbiquitousKeyValueStoreout of the box
| 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 |
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)
}
}
}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
}
}Add to your DefaultKeys extension:
Note:
DefaultKeyinitializer uses positionalkeyand labeledvalueparameters: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
)
}
}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.
struct ProfileView: View {
@Default(\.username) private var username
var body: some View {
TextField("Username", text: $username)
}
}class ProfileViewModel: ObservableObject {
@DefaultValue(\.username) var username: String
func validate() -> Bool {
return !username.isEmpty
}
}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// Package.swift
dependencies: [
.package(url: "https://github.com/your-org/Defaults.git", from: "1.0.0")
]// Your project
.target(
name: "YourTarget",
dependencies: ["Defaults"]
)| Platform | Minimum Version |
|---|---|
| macOS | 11.0 (Big Sur) |
| iOS | 14.0 |
| Swift | 6.0 |
Copyright © 2024 Dimension North Inc. All rights reserved.