Use Binding<Int> with a TextField SwiftUI

21,597

Solution 1

Actually , you can binding manyTypes with TextField:

      @State var weight: Int = 0
      var body: some View {


       Group{
       Text("\(weight)")
       TextField("Weight", value: $weight, formatter: NumberFormatter())
    }}

Solution 2

Of course it is possible to use

TextField("", value: $value, formatter: NumberFormatter())
//    .keyboardType(UIKeyboardType.decimalPad) // << uncomment for num pad

and even with Numeric Pad, but this does not prevent to enter non-numeric characters into such TextField, and until commit formatter is not called to validate input. Maybe Apple will give us possibility to validate input on the fly in future, but not now, ... so I prefer different way

Here is my approach to have text field for numeric values (Int, Float, Double, etc.) which validates input and limits of specific type (say do not allow to enter values longer then fit into Int maximum allowed value). Hope it would be helpful for someone as well. (Of course configurations like font, size, colors, etc. are possible per usage needs)

struct NumberTextField<V>: UIViewRepresentable where V: Numeric & LosslessStringConvertible {
    @Binding var value: V

    typealias UIViewType = UITextField

    func makeUIView(context: UIViewRepresentableContext<NumberTextField>) -> UITextField {
        let editField = UITextField()
        editField.delegate = context.coordinator
        return editField
    }

    func updateUIView(_ editField: UITextField, context: UIViewRepresentableContext<NumberTextField>) {
        editField.text = String(value)
    }

    func makeCoordinator() -> NumberTextField.Coordinator {
        Coordinator(value: $value)
    }

    class Coordinator: NSObject, UITextFieldDelegate {
        var value: Binding<V>

        init(value: Binding<V>) {
            self.value = value
        }

        func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange,
                       replacementString string: String) -> Bool {

            let text = textField.text as NSString?
            let newValue = text?.replacingCharacters(in: range, with: string)

            if let number = V(newValue ?? "0") {
                self.value.wrappedValue = number
                return true
            } else {
                if nil == newValue || newValue!.isEmpty {
                    self.value.wrappedValue = 0
                }
                return false
            }
        }

        func textFieldDidEndEditing(_ textField: UITextField, reason: UITextField.DidEndEditingReason) {
            if reason == .committed {
                textField.resignFirstResponder()
            }
        }
    }
}

struct TestTextFieldWithNumbers: View {
    @State private var value = 0
    var body: some View {
        VStack {
            Text("Current value: \(value)")
            Divider()
            TextField("", value: $value, formatter: NumberFormatter())
//                .keyboardType(UIKeyboardType.decimalPad)
            Divider()
            NumberTextField(value: $value)
                .frame(height: 32)
        }
    }
}

struct TestTextFieldWithNumbers_Previews: PreviewProvider {
    static var previews: some View {
        TestTextFieldWithNumbers()
    }
}

Solution 3

Just to make it more observable.

change this:

TextField("", text: $intvalue)

to that

TextField("Value", value: $amount, formatter: NumberFormatter())
Share:
21,597
Bernard
Author by

Bernard

Updated on July 09, 2022

Comments

  • Bernard
    Bernard almost 2 years

    I am currently building a page to add player information to a local database. I have a collection of TextFields for each input which is linked to elements in a player struct.

    var body: some View {
            VStack {
                TextField("First Name", text: $player.FirstName)
                    .textFieldStyle(RoundedBorderTextFieldStyle())
                TextField("Last Name", text: $player.LastName)
                    .textFieldStyle(RoundedBorderTextFieldStyle())
                TextField("Email", text: $player.eMail)
                    .textFieldStyle(RoundedBorderTextFieldStyle())
                TextField("Shirt Number", text: $player.ShirtNumber)
                    .textFieldStyle(RoundedBorderTextFieldStyle())
                TextField("NickName", text: $player.NickName)
                .textFieldStyle(RoundedBorderTextFieldStyle())
                TextField("Height", text: $player.Height)
                    .textFieldStyle(RoundedBorderTextFieldStyle())
                TextField("Weight", text: $player.Weight)
                    .textFieldStyle(RoundedBorderTextFieldStyle())
    
                Button(action: {
                    submitPlayer(player: self.player)T
                }) {
                    Text("Submit")
                }
    
                Spacer()
            }
        }
    

    My player struct is

    struct Player: Hashable, Codable, Identifiable {
        var id: Int
        var FirstName: String
        var LastName: String
        var NickName: String
        var eMail: String
        var ShirtNumber: Int
        var Height: Int
        var Weight: Int
    }
    

    The issue is that ShirtNumber, Height, and Weight are all Int values. When I bind them to the TextField I get an error saying Cannot convert value of type 'Binding<Int>' to expected argument type 'Binding<String>'. Everything I have looked into about SwiftUI says it's impossible to have a TextField with an Int value bound to it.

    My question is, would it be possible to create a new class that extends TextField but allows for only Int inputs and that binds an Int variable, something like this?

    struct IntTextField: TextField {
    
        init(_ text: String, binding: Binding<Int>) {
    
        }
    }
    
    

    So far all I have been able to find is an answer to part of my question (accepting only Int input) from this question. I am looking for a way to combine this with the Binding<Int>.

    Thanks for the help.