A lightweight, observable interface to typed preferences on Apple platforms.
Define each preference's key, value type, and default once, then read and write
values through a shared store or a UserDefaults suite.
UserPreferences is an Observable interface to the user's defaults database built around reusable and type-safe design patterns.
- Swift 6+
- iOS 17+ / macOS 14+ / Mac Catalyst 17+ / tvOS 17+ / visionOS 1+ / watchOS 10+
Add the PreferenceKit library product to your target and import it. Define a
preference by conforming a type to PreferenceProtocol:
import PreferenceKit
enum ShowDebugMenuPreference: PreferenceProtocol {
static let key = "showDebugMenu"
static let defaultValue = false
}
let preferences = UserPreferences()
// `false` when absent
let showDebugMenu = preferences[ShowDebugMenuPreference.self]
// update the value to `true`
preferences[ShowDebugMenuPreference.self] = true
// remove the value to restore default state
preferences.removeValue(forPreference: ShowWelcome.self)Use a stable, unique key for each preference; changing a key does not migrate an existing value.
Reading a missing preference returns its default without writing it to storage. Removing a value lets subsequent reads fall back to the default.
Keep stored representations compatible when changing a preference's value type or coding implementation.
Use @Preference to read and write a preference as a property. By default,
the wrapper uses UserPreferences.default:
struct Settings {
@Preference(ShowDebugMenuPreference.self)
var showDebugMenu: Bool
}
let settings = Settings()
settings.showDebugMenu = trueSupply a store to use a separate defaults suite or another PreferencesProtocol
implementation. Initialize the backing wrapper when the store is provided at runtime:
struct Settings<Preferences> where Preferences: PreferencesProtocol {
@Preference<Preferences, ShowDebugMenuPreference>
var showDebugMenu: Bool
init(preferences: Preferences) {
self._showDebugMenu = Preference(preferences: preferences)
}
}Reads and writes proviede a non-throwing interface to the stores persistence layer. A failed read returns the default, and a failed write is logged. Use the store's named methods when you need to handle errors.
Add the PreferenceKit_SwiftUI library product to your target and import it.
It also re-exports PreferenceKit and SwiftUI.
import PreferenceKit_SwiftUI
struct SettingsView: View {
@Preference(ShowDebugMenuPreference.self)
private var showDebugMenu: Bool
var body: some View {
Toggle("Show Debug Menu", isOn: self._showDebugMenu.binding)
}
}The binding reads and writes the same store as the wrapped property; reads participate in Swift Observation.
Note
Use _showDebugMenu.binding, or _showDebugMenu.projectedValue, to obtain the
binding. The SwiftUI extension does not provide a $showDebugMenu accessor due missing support at the Swift compiler level.
Preferences can also store custom Codable values:
enum Appearance: Codable, Equatable, Sendable {
case dark
case light
case system
}
enum AppearancePreference: PreferenceProtocol {
static let key = "appearance"
static let defaultValue = Appearance.system
}
preferences[AppearancePreference.self] = .darkThe subscript logs errors. A failed read returns the preference's default; a failed write does not propagate an error to the caller. Use the named methods when you need to handle failures:
do {
let appearance = try preferences.value(forPreference: AppearancePreference.self)
try preferences.updateValue(appearance, forPreference: AppearancePreference.self)
} catch {
// Handle an incompatible stored value or an encoding failure.
}UserPreferences reads throw DecodingError and writes throw EncodingError.
A failed decode leaves the stored value unchanged, and an encoding failure does
not replace it. Removing a value does not throw.
UserPreferences participates in Swift Observation. Reading a preference
inside a SwiftUI view's body tracks access to that key:
import SwiftUI
struct RootView: View {
let preferences: UserPreferences
var body: some View {
var shouldShowOnboardingView = preferences[ShouldShowOnboardingViewPreference.self]
// a binding over the `shouldShowOnboardingView` copy;
// does not mutate the underlying preference value.
let shouldShowOnboardingViewBinding = Binding<Bool>(get: {
return shouldShowOnboardingView
}, set: { newValue in
shouldShowOnboardingView = newValue
})
ContentView()
.sheet(isPresented: shouldShowOnboardingViewBinding, onDismiss: {
self.preferences[ShouldShowOnboardingViewPreference.self] = false
}) {
OnboardingView()
}
}
}A property wrapper type that reflects a preference from a PreferencesProtocol.
@propertyWrapper
public struct Preference<Preferences, Preference>: Sendable where Preferences: PreferencesProtocol, Preference: PreferenceProtocol {
/// The value represented by the preference definition.
public typealias Value = Preference.Value
/// The store used to read and write the preference.
public let preferences: Preferences
/// The stored value, or the preference's default when reading fails or no value exists.
///
/// Writes update the store immediately. Failed writes are logged without
/// propagating an error; use the store's named methods to handle errors.
public var wrappedValue: Value { get nonmutating set }
/// Creates a property for interfacing with a preference in the given preferences.
///
/// - Parameters:
/// - preference: The preference definition. May be inferred from the wrapper's type.
/// - preferences: The store used for reads and writes.
public init(_ preference: Preference.Type = Preference.self, preferences: Preferences)
}extension Preference where Preferences == UserPreferences {
/// Creates a property for interfacing with a preference in ``UserPreferences``.
///
/// - Parameters:
/// - preference: The preference definition. May be inferred from the wrapper's type.
/// - preferences: The store used for reads and writes.
public init(_ preference: Preference.Type = Preference.self, preferences: Preferences = .default)
}extension Preference {
/// A binding that reads and writes the preference through its store.
///
/// - Note: The Swift compiler [does not synthesize the `$` shorthand for `-projectedValue`
/// when implemented in an external module](https://forums.swift.org/t/property-wrapper-projectedvalue-cannot-be-in-a-extension/70269).
public var projectedValue: Binding<Value> { get }
/// A convenience alias for `projectedValue`.
///
/// This property is available in order to clarify intent but is intended to be removed at a later date.
@inline(__always)
public var binding: Binding<Value> { get }
}A typed preference definition for interfacing with a corresponding PreferencesProtocol.
public protocol PreferenceProtocol: Sendable {
/// The value stored for this preference.
associatedtype Value: Codable & Equatable & Sendable
/// The key identifying this preference in the store.
///
/// - Note: Use a stable, unique key for each preference. Changing the key
/// does not migrate values stored under the previous key.
static var key: String { get }
/// The fallback returned when no stored value exists.
///
/// Reading the fallback does not persist it.
static var defaultValue: Value { get }
}An observable store of typed preferences.
public protocol PreferencesProtocol: Observable, Sendable {
/// The error raised when a persisted value cannot be queried.
associatedtype ReadFailure: Error
/// The error raised when a value cannot be persisted.
associatedtype WriteFailure: Error
/// The error raised when a value cannot be removed.
associatedtype RemoveFailure: Error
/// Returns the preference's value, or its default when no persisted value exists.
///
/// - Returns: The persisted value for the corersponding preference, or its default value when no persisted value exists.
/// - Throws: A ``ReadFailure`` when the stored value cannot be queried.
func value<Preference>(forPreference preference: Preference.Type) throws(ReadFailure) -> Preference.Value where Preference: PreferenceProtocol
/// Inserts or replaces the value for the given preference.
///
/// - Throws: A ``WriteFailure`` when the value cannot be persisted.
func updateValue<Preference>(_ value: Preference.Value, forPreference preference: Preference.Type) throws(WriteFailure) where Preference: PreferenceProtocol
/// Removes the persisted value for the given preference.
///
/// - Throws: A ``RemoveFailure`` when the value cannot be removed.
func removeValue<Preference>(forPreference preference: Preference.Type) throws(RemoveFailure) where Preference: PreferenceProtocol
}extension PreferencesProtocol {
/// Returns the preference's value, or its default when no persisted value exists.
///
/// Reading returns the default when no stored value exists or the read
/// fails. A failed write is logged without propagating the error. Use
/// ``value(forPreference:)`` and ``updateValue(_:forPreference:)`` when
/// callers need to handle failures.
public subscript<Preference>(preference: Preference.Type) -> Preference.Value where Preference: PreferenceProtocol { get nonmutating set }
}A PreferencesProtocol implementation backed by UserDefaults.
public final class UserPreferences: PreferencesProtocol {
/// The error raised when a persisted value cannot be queried.
public typealias ReadFailure = DecodingError
/// The error raised when a value cannot be persisted.
public typealias WriteFailure = EncodingError
/// The error raised when a value cannot be removed.
public typealias RemoveFailure = Never
/// The shared preference store backed by `UserDefaults.standard`.
public static let `default`: UserPreferences
/// The defaults database backing this store.
public nonisolated(unsafe) let userDefaults: UserDefaults
/// Creates a `UserPreferences` backed by the provided `UserDefaults` instance.
///
/// - Parameter userDefaults: The database to read and write. Defaults to `UserDefaults.standard`.
public init(userDefaults: UserDefaults = .standard)
/// Returns the preference's value, or its default when no persisted value exists.
///
/// - Returns: The persisted value for the corersponding preference, or its default value when no persisted value exists.
/// - Throws: A `ReadFailure` if the stored value has an incompatible type or cannot be decoded. The stored value is left unchanged.
public func value<Preference>(forPreference preference: Preference.Type) throws(ReadFailure) -> Preference.Value where Preference: PreferenceProtocol
/// Inserts or replaces the value for the given preference.
///
/// - Throws: A `WriteFailure` if the value cannot be encoded. An encoding failure leaves the stored value unchanged.
public func updateValue<Preference>(_ value: Preference.Value, forPreference preference: Preference.Type) throws(WriteFailure) where Preference: PreferenceProtocol
/// Removes the persisted value for the given preference.
///
/// - Throws: A `RemoveFailure` if the value cannot be encoded. An encoding failure leaves the stored value unchanged.
public func removeValue<Preference>(forPreference preference: Preference.Type) throws(RemoveFailure) where Preference: PreferenceProtocol
}Underscored types and requirements support persistence internally. Clients do not need to implement them when defining preferences.
UserPreferences.default uses UserDefaults.standard. Supply a defaults
instance to target a separate suite:
import Foundation
if let userDefaults = UserDefaults(suiteName: "com.example.MyApplicationContainer") {
let preferences = UserPreferences(userDefaults: userDefaults)
preferences[ShowDebugMenuPreference.self] = true
}Supported property-list values are stored directly in UserDefaults; other
values are serialized and persisted as Data.