Search code examples
swiftswiftuipropertieswrappercombine

Cannot invoke initializer for type 'TextField<_>' with propertyWrapper of UseDefaults


I am working on a SwiftUI screen that updates multiple values in the UserDefaults, to allow the app to persist basic settings. I am trying to use Combine and SwiftUI, as this is a native WatchOS app.

The basic View is giving me an error that I believe has to do with the propertyWrapper for UserDefaults, but as I have never worked with propertyWrappers (or Combine for that matter) I am un able to figure out how to fix this.

here's the property wrapper:

import Foundation

@propertyWrapper
struct UserDefault<T> {
    let key: String
    let defaultValue: T

    init(_ key: String, defaultValue: T) {
        self.key = key
        self.defaultValue = defaultValue
    }

    var wrappedValue: T {
        get {
            UserDefaults(suiteName: "group.com.my.app")!.value(forKey: key) as? T ?? defaultValue
        } set {
            UserDefaults(suiteName: "group.com.my.app")!.set(newValue, forKey: key)
        }
    }
}

As you can see it wraps all the Key Pairs for the UserDefaults.

My class is similarly very simple, consisting of two bools and a double

import SwiftUI
import Foundation
import Combine

class Setup: ObservableObject {

    private var notificationSubscription: AnyCancellable?

    let objectWillChange = PassthroughSubject<Setup,Never>()

    @UserDefault("keyOpt1Enabled", defaultValue: false)
    var opt1Enabled: Bool {
        willSet{
            objectWillChange.send(self)
        }
    }

    @UserDefault("keyOpt2Enabled", defaultValue: false)
    var opt2Enabled: Bool {
        willSet{
            objectWillChange.send(self)
        }
    }

    @UserDefault("keyValueDouble", defaultValue: Double(0.00))
    var someValueDouble: Double {
        willSet{
            objectWillChange.send(self)
        }

    }
    init() {

        notificationSubscription = NotificationCenter.default.publisher(for: UserDefaults.didChangeNotification).sink { _ in
                   self.objectWillChange.send(self)
        }
    }
}

The problem is that in SwiftUI I am using a TextField to allow for entering and updating the double value


    @ObservedObject var setup: Setup = Setup()

    private var currencyFormatter: NumberFormatter = {
        let f = NumberFormatter()
        f.numberStyle = .currency
        return f
    }()


    var body: some View {
        ScrollView{
            HStack{
                TextField(self.$setup.someValueDouble,
                          formatter: currencyFormatter,
                          placeholder: "0.00",
                          onEditingChanged: {_ in
                            print("editing changed")
                          },
                          onCommit: {
                            print("updated")
                            }
                )
            }

            HStack{
                Button(action: {
                    self.setup.opt1Enabled = false
                    self.setup.opt2Enabled = true
                } ) {
                    Text(verbatim: "Opt 1")
                        .font(Font.system(size: 16, design: Font.Design.rounded))
                }
                .background(setup.opt1Enabled ? Color.blue : Color.gray)
                .disabled(self.setup.opt1Enabled)
                .cornerRadius(5)

                Button(action: {
                    self.setup.opt1Enabled = true
                    self.setup.opt2Enabled = false
                }) {
                    Text(verbatim: "Opt 2")
                }
                .background(setup.opt2Enabled ? Color.blue : Color.gray)
                .disabled(self.setup.opt2Enabled)
            }
        }
    }
}

The TextField then gives the message that the Generic parameter 'Label' could not be inferred. Xcode offers to "Fix" this but the end results is TextField which is obviously incomplete, but the only view in this whole program is "ContentView" which is invalid.


Solution

  • The problem is that TextField's initialiser takes first a String as it's placeholder and next the value it is bound to, so it should look roughly like this:

     TextField("Placeholder",
               value: self.$setup.someValueDouble,
               formatter: currencyFormatter,
               onEditingChanged: { _ in },
               onCommit:{})
         .keyboardType(.numberPad)
    

    You might want to read more about TextFields and Formatter, because it seems to be a tricky subject.