How to Make Custom Property Wrapper Bindable, Possibly with @State?

1.1k Views Asked by At

I have created a custom property wrapper that writes and reads from a file as its setter and getter.

@propertyWrapper struct Specifier<Value> {
    let key: String
    let defaultValue: Value
    let plistPath: String
    
    var prefs: NSDictionary {
        NSDictionary(contentsOfFile: plistPath) ?? NSDictionary()
    }

    var wrappedValue: Value {
        get {
            return prefs.value(forKey: key) as? Value ?? defaultValue
        }
        set {
            prefs.setValue(newValue, forKey: key)
            prefs.write(toFile: plistPath, atomically: true)
        }
    }
}

I now want to use this as in a settings view like so:

struct PrefsView: View {
    @Specifier<Bool>(key: "enable", defaultValue: true, plistPath: "/Library/path/to/Prefs.plist") private var enable

    var body: some View {
        Form {
            Toggle("Enable", isOn: $enable)
        }
    }

}

I am given the following error:

Cannot find '$enable' in scope

How might I achieve this effect of writing and reading to a file for setting and getting variable but also allow it to bind to a value?

NOTE: My project does not need to be "App Store Legal"

1

There are 1 best solutions below

5
On

Your property wrapper returns Bool and not Binding<Bool> which is needed by the Toggle.

You can specify the projectedValue (as NewDev suggested) - this will allow you to access the Binding with the $:

@propertyWrapper struct Specifier<Value> {
    ...

    var wrappedValue: Value {
        get {
            projectedValue.wrappedValue
        }
        set {
            projectedValue.wrappedValue = newValue
        }
    }
    
    var projectedValue: Binding<Value> {
        .init(get: {
            prefs.value(forKey: key) as? Value ?? defaultValue
        }, set: {
            prefs.setValue($0, forKey: key)
            prefs.write(toFile: plistPath, atomically: true)
        })
    }
}

struct ContentView: View {
    @Specifier(key: "enable", defaultValue: true, plistPath: "/Library/path/to/Prefs.plist") private var enable

    var body: some View {
        Form {
            Toggle("Enable", isOn: $enable)
            Text(String(enable))
        }
    }
}

Alternatively, you can change the return type of the wrappedValue to Binding<Bool>:

@propertyWrapper struct Specifier<Value> {
    ...

    var wrappedValue: Binding<Value> {
        .init(get: {
            prefs.value(forKey: key) as? Value ?? defaultValue
        }, set: {
            prefs.setValue($0, forKey: key)
            prefs.write(toFile: plistPath, atomically: true)
        })
    }
}

struct ContentView: View {
    @Specifier(key: "enable", defaultValue: true, plistPath: "/Library/path/to/Prefs.plist") private var enable

    var body: some View {
        Form {
            Toggle("Enable", isOn: enable) // access as a Binding
            Text(String(enable.wrappedValue)) // access as a Bool
        }
    }
}