Can I bind to Int but edit with TextField

My question is slightly broader, but hopefully this simple use case states the point.


I ofthen run in to the problem where my source of truth is a custom type (struct, array of bytes, int, etc.) that I want to edit. I would like to write a view that has a binding to this type so that all the editing logic lives inside this view without the views outside having to worry how it is edited.


The simplest example of this is a view that takes a binding to an Int where the user edits it with a TextField. Up to now I've only been able to do this with a custom UI- or NSViewRepresentable implementation. I thought I had a solution today, but it too does not work. It is close enough, that I thought maybe someone can see a path going further.


Below is my code. It has an IntView that takes a binding to an Int, and uses state for the strValue. When it first apears, it updates the local state. It then has a method that can be called to update the binding value (this because I cannot get updates as the text field is changing).


My contentView creates this view and stores updateValue so that it can be called when the button is pressed. The problem is that view is a value (not a reference), so the intView created in body() is not the same one as that in the .async call (I double-checked this with the print that prints the pointer to each). When updateValue() is called, I get an exception, because I am using the wrong view. The reason for using an .async call is so that I do not change state when body is computed.


Can this code somehow be made to work, or is there another solution to writing IntView that takes a binding to an Int?


My problem is often more complicated for example when I want to bind to a struct. If I write a custom __ViewRepresentable solution, then I lose a lot of the ease-of-use of swift, and goes back to manual layout, etc.


struct IntView: View {
  @Binding var value: Int
  
  @State private var strValue: String = ""
  
  var body: some View {
    return TextField("Type here", text: $strValue)
      .onAppear(perform: { self.strValue = "\(self.value)" })
  }
  
  func updateValue() {
    value = Int(strValue)!
  }
}

struct ContentView: View {
  @State var intValue: Int = 12
  @State var updateValue: (() -> Void)?
  
  var body: some View {
    let intView = IntView(value: $intValue)
    withUnsafePointer(to: intView) { print("value at first @\($0)") }
    DispatchQueue.main.async {
      withUnsafePointer(to: intView) { print("value in async @\($0)") }
      self.updateValue = intView.updateValue
    }
    
    return VStack {
      Text("\(intValue)")
      intView
      Button(action: {
        self.updateValue!()
      }, label: { Text("Get Value") })
    }
      .padding()
      .frame(maxWidth: .infinity, maxHeight: .infinity)
  }
}

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

Accepted Reply

My problem is often more complicated for example when I want to bind to a struct.


Assume you have a struct below:

struct MyStruct { 
    var a: Int 
    var b: Int 
}

and want to edit it in TextField in format 'mmm.nnn' where mmm for a, and nnn for b.


You can define a writable property of String and bind TextField to it.

Example:

extension MyStruct { 
    var textValue: String { 
        get { 
            return "\(a).\(b)" 
        } 
        set { 
            let items = newValue.components(separatedBy: ".") 
            guard items.count == 2, let a = Int(items[0]), let b = Int(items[1]) else { 
                return 
            } 
            self.a = a 
            self.b = b 
        } 
    } 
}

And use it in a View with TextField like this:

struct ContentView: View { 
    @State var myValue: MyStruct = MyStruct(a: 12, b: 34) 
    var body: some View { 
        VStack { 
            Text(myValue.textValue) 
            TextField("Type here", text: $myValue.textValue) 
        } 
        .padding() 
        .frame(maxWidth: .infinity, maxHeight: .infinity) 
    } 
}

Replies

TextField can take a number of native Formatter objects, such as NumberFormatter()


@State private var myInt = 0

...

TextField("", value: self.$myInt, formatter: NumberFormatter())


Any entered values that are deemed invalid by the formatter print as such to the terminal and are ignored.


(I'm new here so if any more experienced people think my answer needs a slight correction please let me know, just trying to help)

  • Works, TYVM.

Add a Comment

strValue is never updated unless when you create a new IntView.

You should not redeclare a IntView in the body, but have it as a property of the ContentView.


Creating this IntView struct seems overkill (but interesting exercise).

My problem is often more complicated for example when I want to bind to a struct.


Assume you have a struct below:

struct MyStruct { 
    var a: Int 
    var b: Int 
}

and want to edit it in TextField in format 'mmm.nnn' where mmm for a, and nnn for b.


You can define a writable property of String and bind TextField to it.

Example:

extension MyStruct { 
    var textValue: String { 
        get { 
            return "\(a).\(b)" 
        } 
        set { 
            let items = newValue.components(separatedBy: ".") 
            guard items.count == 2, let a = Int(items[0]), let b = Int(items[1]) else { 
                return 
            } 
            self.a = a 
            self.b = b 
        } 
    } 
}

And use it in a View with TextField like this:

struct ContentView: View { 
    @State var myValue: MyStruct = MyStruct(a: 12, b: 34) 
    var body: some View { 
        VStack { 
            Text(myValue.textValue) 
            TextField("Type here", text: $myValue.textValue) 
        } 
        .padding() 
        .frame(maxWidth: .infinity, maxHeight: .infinity) 
    } 
}

That is brilliant, thanks, and scales nicely for various different types.


It does have a slight side-effect thought that I have not been able to get around. I also cannot see that it is possible to get around.


Take the slightly different example below where MyStruct does not contain Ints, but Doubles. When you start off, the text will be "1.2 3.4". As soon as you delete the "4", the text does not change to "1.2 3." as a user would expect, but to "1.2 3.0" because "3." is converted to a valid double.


Any ideas of how to get around it?


struct MyStruct {
  var a: Double
  var b: Double
}

extension MyStruct {
  var strValue: String {
    get {
      return "\(a) \(b)"
    } set {
      let parts = newValue.components(separatedBy: " ")
      guard parts.count == 2, let aValue = Double(parts[0]), let bValue = Double(parts[1]) else {
        // Not valid.
        return
      }
      a = aValue
      b = bValue
    }
  }
}

struct MyStructView: View {
  @Binding var value: MyStruct
  
  var body: some View {
    TextField("", text: $value.strValue)
  }
}

struct ContentView: View {
  @State var myStruct = MyStruct(a: 1.2, b: 3.4)
  
  var body: some View {
    return VStack {
      Text("xValue = \(myStruct.a)")
      Text("yValue = \(myStruct.b)")
      Divider()
      MyStructView(value: $myStruct)
    } .padding()
      .frame(maxWidth: .infinity, maxHeight: .infinity)
  }
}

Take the slightly different example below where MyStruct does not contain Ints, but Doubles. When you start off, the text will be "1.2 3.4". As soon as you delete the "4", the text does not change to "1.2 3." as a user would expect, but to "1.2 3.0" because "3." is converted to a valid double.

That is a common issue when you bind TextField to a numeric type. In my example, if you input "12.001", you'll see "12.1" in the Text.

The best solution for this, I think, is make users not expect such output.

Or else, give up binding to numeric values.

I found a much better solution. Not only does it solve the original problem, but it also solves the problem when editing doubles (see previous comments) and it allows a single value to be edited by multiple TextFields (or sliders, or any other visual element).

The key is in not binding the TextField to a string, but creating a new Binding with a getter and setter. A local private string is used to keep the string being edited by the TextField in synch with the value (or partial value in this case).

The example below shows a struct with two members. A value of this type can be passed to the custom view that is used to edit it. The value is shown at the top of the view (for information only). The view has two TextFields that is used to edit the two members of the value.

This is just a proof of concept and can be improved further by using Formatters. This example also scales nicely in that a custom view can be created to edit a custom type and the complexity can be hidden inside the custom view.

Code Block
struct MyStruct {
var a: Int
var b: Double
}
struct MyStructView: View {
@Binding var myStruct: MyStruct
@State private var lastValidValue = MyStruct(a: 10, b: 20)
@State private var isEditingA = false
@State private var isEditingB = false
@State private var aStrValue = ""
@State private var bStrValue = ""
var body: some View {
VStack {
Text("myStruct.a = \(myStruct.a) .b = \(myStruct.b)")
Divider()
HStack {
Text("a = ")
TextField("Value a", text: Binding(
get: {
if self.isEditingA {
return self.aStrValue
} else {
return "\(self.myStruct.a)"
}
}, set: { str in
self.aStrValue = str
if let tmp = Int(str) {
self.myStruct.a = tmp
self.lastValidValue = self.myStruct
} else {
self.myStruct.a = self.lastValidValue.a
}
}), onEditingChanged: { editing in
self.isEditingA = editing
})
}
HStack {
Text("b = ")
TextField("Value b", text: Binding(
get: {
if self.isEditingB {
return self.bStrValue
} else {
return "\(self.myStruct.b)"
}
}, set: { str in
self.bStrValue = str
if let tmp = Double(str) {
self.myStruct.b = tmp
self.lastValidValue = self.myStruct
} else {
self.myStruct.b = self.lastValidValue.b
}
}), onEditingChanged: { editing in
self.isEditingB = editing
})
}
}
.frame(width: 300, height: 100)
.padding()
.onAppear {
self.lastValidValue = self.myStruct
}
}
}
struct ContentView: View {
@State private var myStruct = MyStruct(a: 1, b: 2.0)
var body: some View {
MyStructView(myStruct: $myStruct)
}
}
struct ContentView2_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}