Skip to content

couchdeveloper/Settings

Repository files navigation

Settings Icon

Settings - Type-Safe UserDefaults for Swift

Settings Framework Swift 6.2 Platforms iOS 17.0+ macOS 15.0+ SPM License

Type-safe, macro-powered Swift package for UserDefaults.

Features

  • Type-safe — Compile-time type checking with generated keys
  • No boilerplate@Settings and @Setting generate accessors and metadata
  • UserDefaults-compatible — Integrates with existing suites without migration
  • Codable-first — Encode any Codable type via JSON or property list
  • Native storage — Stores property list–compatible types directly (String, Int, Bool, Date, Data, URL, Array, Dictionary)
  • Observable — Built-in Combine publishers and AsyncSequence streams
  • Customizable — Namespaced keys and pluggable encoders/decoders

SwiftUI Integration

import Settings
import SwiftUI

// Special mockable Settings container.
// Uses UserDefaults.standard per default.
extension AppSettingValues {
    @Setting public var score: Int = 0
}

struct AppSettingsView: View {
    @AppSetting(\.$score) var score

    var body: some View {
        Form {
            TextField("Enter your score", value: $score, format: .number)
                .textFieldStyle(.roundedBorder)
                .padding()

            Text("Your score was \(score).")
        }
    }
}

Use the UserDefaultsStoreMock class in Previews:

#if DEBUG
import SettingsMock

#Preview {
    AppSettingsView()
        .environment(\.userDefaultsStore, UserDefaultsStoreMock.standard)
}
#endif

Note: AppSettingValues is a mockable UserDefaults container already declared in the Settings library.

Set a global key prefix:

@main
struct MyApp: App {
    init() {
        AppSettingValues.prefix = "myapp_"
    }
    
    var body: some Scene {
        WindowGroup {
            MainView()
        }
    }
}

Note: When the prefix is not customized, Settings library will use the bundle identifier from the main bundle to derive a unique prefix for every key.

Custom Settings Container

You do not need to put the user settings into a type annotated with the @Settings macro. You also can declare or use any regular struct, class or enum as your UserDefaults container:

struct AppSettings {
    @Setting static var username: String = "Guest"
}

This will store and read from the setting in Foundation's UserDefaults.standard.

If you require more control, like having key prefixes, using your own UserDefaults suite, or if you want to mock Foundation UserDefaults in Previews or elsewhere, use the @Settings macro:

@Settings(prefix: "app_") // keys prefixed with "app_"
struct AppSettings {
    @Setting static var username: String = "Guest"
}

Projected Value ($propertyName)

Access metadata and observe changes:

// Key
print(AppSettings.$username.key) // "app_username"
print(AppSettings.$theme.key)    // "colorScheme"

// Reset
AppSettings.$theme.reset()

// Observe (Combine)
AppSettings.$theme.publisher.sink { print("Theme:", $0) }

// Observe (AsyncSequence)
for try await theme in AppSettings.$theme.stream {
    print("Theme:", theme)
}

Codable

struct UserProfile: Codable, Equatable {
    let name: String
    let age: Int
}

@Settings(prefix: "app_")
struct Settings {
    @Setting(encoding: .json)
    static var profile: UserProfile = .init(name: "Guest", age: 0)
}

Settings.profile = .init(name: "Alice", age: 30)

Optionals

@Settings(prefix: "app_")
struct Settings {
    @Setting static var apiKey: String?  // no default value!
}

Settings.apiKey = "secret123"
Settings.apiKey = nil // removes key from UserDefaults

Nested Containers

Support keys within a name space:

struct AppSettings {}

extension AppSettings { enum UserSettings {} }

extension AppSettings.UserSettings {
    @Setting static var email: String?
}

print(AppSettings.UserSettings.$email.key) // "UserSettings::email"
AppSettings.UserSettings.email = "alice@example.com"

Concurrency

Use explicit isolation if accessed across actors.

@Settings(prefix: "app_")
struct Settings {
    @MainActor @Setting static var theme: String = "light"
}

Requirements

  • Swift 6.2
  • macOS 10.15+ / iOS 17.0+ / tvOS 17.0+ / watchOS 10.0+

Installation

Swift Package Manager

Add Settings to your Package.swift:

dependencies: [
    .package(url: "https://github.com/couchdeveloper/Settings.git", from: "0.5.0")
]

Or in Xcode:

  1. File → Add Package Dependencies
  2. Enter: https://github.com/couchdeveloper/Settings.git
  3. Select version: 0.5.0 or later

Advanced

// Suite (App Group)
@Settings(prefix: "shared_", suiteName: "group.com.myapp")
struct SharedSettings {
    @Setting static var syncEnabled: Bool = true
}

// Custom key
@Setting(name: "custom_key")
static var value: Int = 42

// Custom encoders/decoders
@Setting(encoder: JSONEncoder(), decoder: JSONDecoder())
static var customProfile: UserProfile = .init(name: "Custom", age: 0)

// Key-path observation
struct User: Codable, Equatable { var name: String; var age: Int }

@Settings(prefix: "app_")
struct Settings {
    @Setting(encoding: .json)
    static var user: User = .init(name: "Guest", age: 0)
}

for try await name in Settings.$user.stream(for: \.name) {
    print("Name:", name)
}

Contributing

Contributions are welcome! Please see CONTRIBUTING.md for guidelines.

License

Apache License (v2.0) - See LICENSE file for details

About

Type-safe, macro-powered Swift package for Foundation's UserDefaults.

Resources

License

Contributing

Stars

Watchers

Forks

Packages

No packages published

Languages