4

I want a list with rows, with each row having 2 Textfields inside of it. Those rows should be saved in an array, so that I can use the data in an other view for further functions. If the text in the Textfield is changed, the text should be saved inside the right entry in the array. You also can add new rows to the list via a button, which should also change the array for the rows.

The goal is to have a list of key value pairs, each one editable and those entries getting saved with the current text.

Could someone help me and/or give me hint for fixing this problem?

So far I have tried something like this:

// the array of list entries     
@State var list: [KeyValue] = [KeyValue()]
// the List inside of a VStack  
List(list) { entry in
    KeyValueRow(list.$key, list.$value)
}
// the single item
struct KeyValue: Identifiable {
    var id = UUID()

    @State var key = ""
    @State var value = ""
}
// one row in the list with view elements
struct KeyValueRow: View {

    var keyBinding: Binding<String>
    var valueBinding: Binding<String>

    init(_ keyBinding: Binding<String>, _ valueBinding: Binding<String>){
        self.keyBinding = keyBinding
        self.valueBinding = valueBinding
    }

    var body: some View {
        HStack() {
            TextField("key", text: keyBinding)
            Spacer()
            TextField("value", text: valueBinding)
            Spacer()
        }
    }
}

Also, about the button for adding new entries. Problem is that if I do the following, my list in the view goes blank and everything is empty again (maybe related: SwiftUI TextField inside ListView goes blank after filtering list items ?)

Button("Add", action: {
    self.list.append(KeyValue())
})

4 Answers 4

6

Working code example for iOS 15

In SwiftUI, Apple recommends passing the binding directly into the List constructor and using a @Binding in the ViewBuilder block to iterate through with each element.

Apple recommends this approach over using the Indices to iterate over the collection since this doesn't reload the whole list every time a TextField value changes (better efficiency).

The new syntax is also back-deployable to previous releases of SwiftUI apps.

struct ContentView: View {
  @State var directions: [Direction] = [
    Direction(symbol: "car", color: .mint, text: "Drive to SFO"),
    Direction(symbol: "airplane", color: .blue, text: "Fly to SJC"),
    Direction(symbol: "tram", color: .purple, text: "Ride to Cupertino"),
    Direction(symbol: "bicycle", color: .orange, text: "Bike to Apple Park"),
    Direction(symbol: "figure.walk", color: .green, text: "Walk to pond"),
    Direction(symbol: "lifepreserver", color: .blue, text: "Swim to the center"),
    Direction(symbol: "drop", color: .indigo, text: "Dive to secret airlock"),
    Direction(symbol: "tram.tunnel.fill", color: .brown, text: "Ride through underground tunnels"),
    Direction(symbol: "key", color: .red, text: "Enter door code:"),
  ]

  var body: some View {
    NavigationView {
      List($directions) { $direction in
        Label {
          TextField("Instructions", text: $direction.text)
        }
      }
      .listStyle(.sidebar)
      .navigationTitle("Secret Hideout")
    }
  }
}

struct Direction: Identifiable {
  var id = UUID()
  var symbol: String
  var color: Color
  var text: String
}
Sign up to request clarification or add additional context in comments.

1 Comment

This is awesome. Exactly what I was looking for, and way easier than expected! The same technique works in a ForEach also.
3

I am not sure what the best practice is keep a view up to date with state in an array like this, but here is one approach to make it work.

For the models, I added a list class that conforms to Observable object, and each KeyValue item alerts it on changes:

class KeyValueList: ObservableObject {

    @Published var items = [KeyValue]()

    func update() {
        self.objectWillChange.send()
    }

    func addItem() {
        self.items.append(KeyValue(parent: self))
    }
}
class KeyValue: Identifiable {

    init(parent: KeyValueList) {
        self.parent = parent
    }

    let id = UUID()
    private let parent: KeyValueList

    var key = "" {
        didSet { self.parent.update() }
    }
    var value = "" {
        didSet { self.parent.update() }
    }
}

Then I was able to simply the row view to just keep a single piece of state:

struct KeyValueRow: View {

    @State var item: KeyValue

    var body: some View {
        HStack() {
            TextField("key", text: $item.key)
            Spacer()
            TextField("value", text: $item.value)
            Spacer()
        }
    }
}

And for the list view:

struct TextFieldList: View {

    @ObservedObject var list = KeyValueList()

    var body: some View {

        VStack {
            List(list.items) { item in
                HStack {
                    KeyValueRow(item: item)
                    Text(item.key)
                }
            }
            Button("Add", action: {
                self.list.addItem()
            })

        }
    }
}

I just threw an extra Text in there for testing to see it update live.

I did not run into the Add button blanking the view as you described. Does this solve that issue for you as well?

4 Comments

Wow thanks for your help. I can't figure out how to get the Text(item.key) element you added to show up in the app/emulation(just for testing) . And it seems that even nothing is changing inside the items after typing inside a TextField (I printed print(self.list.items) to the console and only the parent is set and the rest is just empty string "") Also I got no problem with the view going blank after I tapped the button anymore.
Hm, this code is working for me. Did you see I changed KeyValue from a struct to a class?
Thanks so much for your work. It is working now. I feel so stupid that i missed this change.
I'm glad to hear it is working for you! Working with arrays as state in SwiftUI is tricky and often takes some fiddling with for me.
2

No need to mess up with classes, Observable, Identifiable. You can do it all with structs.

Note, that version below will do fine for insertions, but fail if you try to delete array elements:

import SwiftUI

// the single item
struct KeyValue {
    var key: String
    var value: String
}

struct ContentView: View {
    @State var boolArr: [KeyValue] = [KeyValue(key: "key1", value: "Value1"), KeyValue(key: "key2", value: "Value2"), KeyValue(key: "key3", value: "Value3"), KeyValue(key: "key4", value: "Value4")]

    var body: some View {
        NavigationView {
            // id: \.self is obligatory if you need to insert
            List(boolArr.indices, id: \.self) { idx in
                HStack() {
                    TextField("key", text: self.$boolArr[idx].key)
                    Spacer()
                    TextField("value", text: self.$boolArr[idx].value)
                    Spacer()
                }
            }
            .navigationBarItems(leading:
                Button(action: {
                    self.boolArr.append(KeyValue(key: "key\(UInt.random(in: 0...100))", value: "value\(UInt.random(in: 0...100))"))
                    print(self.boolArr)
                })
                { Text("Add") }
                , trailing:
                Button(action: {
                    self.boolArr.removeLast() // causes "Index out of range" error
                    print(self.boolArr)
                })
                { Text("Remove") })
        }
    }
}

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

Update: A little trick to make it work with deletions as well.

import SwiftUI

// the single item
struct KeyValue {
    var key: String
    var value: String
}

struct KeyValueView: View {
    @Binding var model: KeyValue
    var body: some View {
        HStack() {
            TextField("Key", text: $model.key)
            Spacer()
            TextField("Value", text: $model.value)
            Spacer()
        }
    }
}

struct ContentView: View {
    @State var kvArr: [KeyValue] = [KeyValue(key: "key1", value: "Value1"), KeyValue(key: "key2", value: "Value2"), KeyValue(key: "key3", value: "Value3"), KeyValue(key: "key4", value: "Value4")]

    var body: some View {
        NavigationView {
            List(kvArr.indices, id: \.self) { i in
                KeyValueView(model: Binding(
                  get: {
                    return self.kvArr[i]
                },
                set: { (newValue) in
                    self.kvArr[i] = newValue
                }))
            }
            .navigationBarItems(leading:
                Button(action: {
                    self.kvArr.append(KeyValue(key: "key\(UInt.random(in: 0...100))", value: "value\(UInt.random(in: 0...100))"))
                    print(self.kvArr)
                })
                { Text("Add") }
                , trailing:
                Button(action: {
                    self.kvArr.removeLast() // Works like a charm
                    print(self.kvArr)
                })
                { Text("Remove") })
        }
    }
}

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

Comments

1

Swift 5.5

This version of swift enables one line code for this by passing the bindable item directly from the array.

struct DirectionsList: View {
    @Binding var directions: [Direction]

    var body: some View {
        List($directions) { $direction in
            Label {
                TextField("Instructions", text: $direction.text)
            } icon: {
                DirectionsIcon(direction)
            }
        }
    }
}

2 Comments

adding a image containing codes is not suitable !
this will cause a problem if you are using a SwiftData class to make your directions whose text is not optional, the TextFeild will not allow you to clear your text because it is bound directly to a Swiftdata object

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.