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()
}
}
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)
}
}