7

I'm new to Swift and SwiftUI and have a model with a @Published intValue optional and a view with a TextField where its text/string property is 2-way bound to intValue of the model. Need to make sure valid text entry as a string is converted to a valid integer for the model. What is the recommended approach to handle this in SwiftUI? Code is similar to below...

//Model
class AppModel:ObservedObject {
 @Published
 var intValue: Int? = 5
}

//View
@ObservedObject
 var appModel = AppModel.shared

TextField("", text: $appModel.intValue)
                    .keyboardType(.numberPad)

Updated sample code...

///////////
import SwiftUI

class Model: ObservableObject {
    static var shared: Model = Model()

    @Published
    var number: Int = 0

    init(){}

}

struct ContentView: View {
    var body: some View {
        TextFieldFormatter()
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}


struct TextFieldFormatter: View {
    @ObservedObject
    var model: Model = Model.shared

    @State private var number: Int = 0


    let formatter: NumberFormatter = {
        let numFormatter = NumberFormatter()
        numFormatter.numberStyle = .none
        return numFormatter
    }()

    var body: some View {
        VStack {
            Text("State").onTapGesture {
                self.endEditing()
            }
            TextField("int", value: $number, formatter: formatter).keyboardType(.numberPad)
            Text("Echo: \(number)")
            Text("Observable")
            TextField("int", value: $model.number, formatter: formatter).keyboardType(.numberPad)
            Text("Echo: \(model.number)")
        }
    }
}

struct TextFieldFormatter_Previews: PreviewProvider {
    static var previews: some View {
        TextFieldFormatter()
    }
}

extension View {
    func endEditing() {
        UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to:nil, from:nil, for:nil)
    }
}

2 Answers 2

12

You can create a custom Binding to pass in to the TextField.

This is how to construct a Binding<String> (what the TextField needs):

var string = ""

let myBinding = Binding(get: { string }) {
    string = $0
}

The first argument of the Binding constructor is the getter. This closure should return a value of the desired type.

The second argument is a setter. It is a closure that takes in the new value, and does something with it (usually set another value).

So, if you want to create a Binding<String> to pass in to the TextField that gets/sets appModel.intValue, you can create a Binding<String> and pass it into the TextField like this:

TextField("", text: .init(
    get: { self.appModel.intValue.map(String.init) ?? "" },
    set: { self.appModel.intValue = Int($0) }
))
.keyboardType(.numberPad)
Sign up to request clarification or add additional context in comments.

2 Comments

Got an error Generic parameter T could not be inferred on map function. Made a few changes and came up with something that compiles, and updates, but editing the text field is problematic with this approach. Can append a digit but can't backspace delete digits. Here's the code I used. TextField("int", text: myBinding) let myBinding = Binding( get: { String(self.appModel.intValue)}, set: { self.appModel.intValue = (Int($0) ?? 5)})
Didn't you want appModel.intValue to be an optional?
4

SwiftUI's TextField has several Constructors. One of them takes a Formatter parameter.

https://developer.apple.com/documentation/swiftui/textfield

import SwiftUI

struct TextFieldFormatter: View {
    @ObservedObject var model: Model = Model.shared

    let formatter: NumberFormatter = {
        let numFormatter = NumberFormatter()
        numFormatter.numberStyle = .none
        return numFormatter
    }()

    var body: some View {
        VStack {
            Text("State").onTapGesture {
                self.endEditing()
            }
            TextField("int", value: $model.number, formatter: formatter).keyboardType(.numberPad)
            Text("Echo: \(model.number)")
            Text("Observable")
            TextField("int", value: $model.number, formatter: formatter).keyboardType(.numberPad)
            Text("Echo: \(model.number)")
        }
    }
}

struct TextFieldFormatter_Previews: PreviewProvider {
    static var previews: some View {
        TextFieldFormatter()
    }
}

Also, if you are trying to disable the screen with the onTapGesture you might consider.

adding:

   @State var disabled: Bool = false

and changing:

       Form {
            Text("Disable").onTapGesture {
                //self.endEditing()
                self.disabled = true
            }
            TextField("int", value: $model.number, formatter: formatter).keyboardType(.numberPad)
            Text("Echo: \(model.number)")
            Text("Observable")
            TextField("int", value: $model.number, formatter: formatter).keyboardType(.numberPad)
            Text("Echo: \(model.number)")
        }.disabled($disabled.wrappedValue)

Giving feedback while the user is typing can be done a few different ways but a custom UITextView in a UIViewRepresentable class seems to be the most common.

3 Comments

I updated the sample code with your code and added in @Observable object. The bound variable using state nor the model using observedobject still appears not update when a change is made to the textfield.
I made a minor change. Removing the State var and referencing the model.number it all works after you press Return
But for example, the new TextEditor only provide the Binding<String> constructor. Which makes thing cumbersome.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.