Contents

edonv/user-default-entries

Usage

.package(url: "https://github.com/edonv/user-default-entries.git", from: "0.0.0")
.product(name: "UserDefault", package: "user-default-entries")

Macro vs Manual

To use the @UserDefault property wrapper, you need to have added a get/set property to UserDefaults via an extension. These can be defined manually or with the packaged @DefaultEntry macro.

Macro:

import DefaultEntry

extension UserDefaults {
    @DefaultEntry(default: "defaultValue")
    var exampleWithDefault: String

    @DefaultEntry<String>
    var exampleWithoutDefault: String?

    @DefaultEntry(default: 123)
    var exampleIntWithDefault: Int

    @DefaultEntry<Int>(prefixedWith: "customPrefix_")
    var exampleIntWithoutDefault: Int?
}

would be equivalent to the following output:

extension UserDefaults {
    var exampleWithDefault: String {
        get { String(withKey: "key_exampleWithDefault", in: self) ?? "defaultValue" }
        set { newValue.store(in: self, withKey: "key_exampleWithDefault") }
    }
    
    var exampleWithoutDefault: String? {
        get { String(withKey: "key_exampleWithoutDefault", in: self) }
        set { newValue.store(in: self, withKey: "key_exampleWithoutDefault") }
    }
    
    var exampleIntWithDefault: Int {
        get { Int(withKey: "key_exampleIntWithDefault", in: self) ?? "defaultValue" }
        set { newValue.store(in: self, withKey: "key_exampleValue") }
    }
    
    var exampleIntWithoutDefault: Int? {
        get { Int(withKey: "customPrefix_exampleIntWithoutDefault", in: self) }
        set { newValue.store(in: self, withKey: "customPrefix_exampleIntWithoutDefault") }
    }
}

Then, in a SwiftUI view, you can do the following:

import SwiftUI
import UserDefault

struct ExampleView: View {
    @UserDefault(\.exampleWithDefault)
    private var exampleWithDefault // implied to be a String
    
    @UserDefault(\.exampleWithoutDefault)
    private var exampleWithoutDefault // implied to be an optional String

    var body: some View {
        VStack {
            TextField("Example Field", text: $exampleWithDefault)

            Text(exampleWithoutDefault ?? "Value is empty")
        }        
    }
}

UserDefaultable

The @UserDefault property wrapper relies on both the @DefaultEntry macro (or manual entry) and the UserDefaultable protocol. Any type used must conform to UserDefaultable. If it's a custom type, it must explicitly conform to UserDefaultable.

RawRepresentable/Codable

UserDefaultable will more conveniently support types that conform to RawRepresentable (whose RawValue types conform to UserDefaultable) and Codable, but due to Swift language restrictions, conformance can't been added to the protocols. Next steps for this library will be to add macros to attach to any RawRepresentable or Codable type, and it will synthesize conformance. For now, conformance must be written explicitly, though it's extremely simple for RawRepresentable types:

enum Example1: String, UserDefaultable {
    case a
    case b
    
    // typealias BaseType = RawValue (implied by using `RawValue` explicitly as the types below)
    
    init?(storableValue: RawValue) {
        self.init(rawValue: storableValue)
    }
    
    var storableValue: RawValue { rawValue }
}

For Codable types, you can define the above initializer and computed property to encode/decode to/from Data via JSONEncoder/JSONDecoder.

struct Example2: Codable, UserDefaultable {
    let a: String
    let b: Int
    
    // typealias BaseType = Data (implied by using `RawValue` explicitly as the types below)
    
    init?(storableValue: Data) {
        guard let value = try? JSONDecoder().decode(Self.self, from: storableValue) else { return nil }
        self = value
    }
    
    var storableValue: Data { 
        try! JSONEncoder().encode(self)
    }
}

Notes

NSNumber

Out of the box, NSNumber is supported by UserDefaults, but due to Swift language restrictions when it comes to adding initializers for a protocol to an existing non-final class, it's not possible to add UserDefaultable conformance to NSNumber. I'd recommend using Int, Float, or Double instead.

To-Do's

  • [x] Write a macro equivalent to @Entry

- `@UserDefaultsEntry(_ key: String, in userDefaults: UserDefaults? = nil)

  • [ ] Write macros for adding convenient conformance to UserDefaultable for Codable/RawRepresentable
  • [ ] Figure out some means to output access to either the key string of a @DefaultEntry, or to at least be able to remove the value from UserDefaults.

- Maybe replace macro's generated get/set with a small struct that includes the get/set and the key, and this is the property type added to UserDefaults and is accessable by @UserDefault property wrapper.

  • [ ] Add conformance of UserDefaultable to BinaryInteger and BinaryFloatingPoint types.
  • [ ] Add DocC content from README.

Package Metadata

Repository: edonv/user-default-entries

Default branch: main

README: README.md