UserDefaults Management with Structs and DynamicProperty Property Wrapper in SwiftUI Views

86 views Asked by At

I'm seeking a more streamlined syntax for the given code. I'd like to consolidate all UserDefaults properties within a distinct UserSettings struct. Both method 1 and method 4 are functioning correctly. I'm exploring the feasibility of adopting a cleaner syntax akin to method 3. In other words, is it possible to utilize a property wrapper conforming to DynamicProperty from another struct within a View? I acknowledge the use of AppStorage, but it has restricted functionality, such as requiring the default value to be set in each view where it is used or a custom setter. That's why I prefer to have a centralized location to manage it in Usersettings.

import SwiftUI

@propertyWrapper
public struct MyUserDefault<Value>: DynamicProperty {
    
    let key: String
    let defaultValue: Value
    var container: UserDefaults = .standard
    @State var cachedValue: Value
    
    public init(key: String, defaultValue: Value) {
        self.key = key
        self.defaultValue = defaultValue
        self._cachedValue = State(initialValue: (container.object(forKey: key) as? Value ?? defaultValue))
    }

    public var wrappedValue: Value {
        get { cachedValue }
        nonmutating set { setNewValue(newValue) }
    }
    
    public var projectedValue: Binding<Value> {
        .init(get: getValue, set: { n in setNewValue(n)} )
    }
    
    private func getValue() -> Value { cachedValue }
    private func setNewValue(_ newValue: Value) {
        container.set(newValue, forKey: key)
        container.synchronize()
        cachedValue = newValue
        print("UserDefaults updated to: \(newValue)") ///
    }
}

struct UserSettings {
    @MyUserDefault(key: "number", defaultValue: 0)
    static var number: Int
}

struct SwiftUIView10: View {
    /// Method 1: Using dynamic ProperyWrapper in View
    @MyUserDefault(key: "number", defaultValue: 10)
    var number: Int
    
    /// Method 2: Using Setting struct
//    var number = Binding {
//        UserSettings.number
//    } set: {
//        UserSettings.number = $0
//    }
//    var number = Binding(projectedValue: UserSettings.$number) //or this

    /// Method 3: Using possible cleaner syntax
    //@Binding var number = UserSettings.number

    /// Method 4: Using possible cleaner syntax
//    @State var number = UserSettings.number /// along with `onChange` to update UserSettings.number 
    
    var body: some View {
        Text("#\(number)") // use number.wrappedValue in method 2
        
        Button("increase") {
            number += 1 // use number.wrappedValue in method 2
            print("number in view \(number)")
        }
    }
}

#Preview {
    SwiftUIView10()
}

1

There are 1 answers

0
Amirca On

Thank you all. I thought of a way to do it, and I'm sharing it here in case you're interested.

import SwiftUI

@propertyWrapper
public struct StaticUserDefault<Value> {
    let key: String
    let defaultValue: Value
    var container: UserDefaults = .standard
    
    public init(key: String, defaultValue: Value, container: UserDefaults = .standard) {
        self.key = key
        self.defaultValue = defaultValue
        self.container = container
    }
    
    public var wrappedValue: Value {
        get { getValue() }
        nonmutating set { setNewValue(newValue) }
    }
    
    public var projectedValue: Binding<Value> {
        .init(get: getValue, set: { n in setNewValue(n)} )
    }
    
    private func getValue() -> Value {
        return container.object(forKey: key) as? Value ?? defaultValue
    }
    
    private func setNewValue(_ newValue: Value) {
        container.set(newValue, forKey: key)
        container.synchronize()
        print("update static defualts: \(newValue)")
    }
}


@propertyWrapper
public struct UserDefaultDynamicBuilder<Value>: DynamicProperty {
    let inputProjectedValue: Binding<Value>
    @State var cachedValue: Value
    
    public init(projectedValue: Binding<Value>) {
        self.inputProjectedValue = projectedValue
        self._cachedValue = State(initialValue: projectedValue.wrappedValue)
    }
    
    public var wrappedValue: Value {
        get { cachedValue }
        nonmutating set { setNewValue(newValue) }
    }
    
    public var projectedValue: Binding<Value> {
        .init(get: getValue, set: { n in setNewValue(n)} )
    }
    
    private func getValue() -> Value { cachedValue }
    private func setNewValue(_ newValue: Value) {
        inputProjectedValue.wrappedValue = newValue
        cachedValue = newValue
        print("update dynamaic builder: \(newValue)")
    }
}

struct UserSettings {
    /// static
    @StaticUserDefault(key: "number", defaultValue: 0)
    static var number
}

struct SwiftUIView17: View {
    @UserDefaultDynamicBuilder(projectedValue: UserSettings.$number)
    var number
    
    var body: some View {
        Text("\(number)")
        
        Button("increase") {
            number += 1
        }
    }
}

#Preview {
    SwiftUIView17()
}