🦁

[SwiftUI]List + TextFieldで文字を編集可能なリストを実装

2021/07/09に公開
1

Listの中でTextFieldを使ってデータを更新しようとした時、つまずいたのでまとめました。

参考にしていた記事

https://yutailang0119.hatenablog.com/entry/delatable-table-with-textfield-on-swiftui
https://stackoverflow.com/questions/57576128/editable-textfield-in-swiftui-list

こちらの実装方法では、Listに使用されたデータがString配列であったため、実際にプロダクトで使うstructなどの形式にはなっていないのでそのままでは使えませんでした。

実装方法

Model

struct SampleModel {
    var text: String
}

textのみを定義します。letではなくvarにします。

Listの中のRow

struct SampleRow: View {
    
    @Binding var model: SampleModel
    var onCommit: () -> Void
    
    var body: some View {
        
        TextField.init("tap to edit text", text: self.$model.text, onEditingChanged: { _ in
            //nop
        }, onCommit: {
            self.onCommit()
        })
    }
}

変更を伝播させたいので、SampleModel@Bindingしています。このViewを使うときに@Bindingに適用できる形式にする必要があります。

ContentView

実際にListを使う部分です。

struct ContentView: View {
    
    @State var data: [SampleModel] = [
        SampleModel(text: "abc"),
        SampleModel(text: "def"),
        SampleModel(text: "")
    ]
    
    var body: some View {
        
        NavigationView {
            List {
                //ForEach(self.data.indices) { index in
                ForEach(self.data.indices, id: \.self) { index in /*2021/07/14 修正*/
                    SampleRow.init(model: self.$data[index]) {
                        debugPrint("onCommit")
                        debugPrint(self.data)
                    }
                }.onDelete(perform: { indexSet in
                    self.data.remove(atOffsets: indexSet)
                }).onMove(perform: { indices, newOffset in
                    self.data.move(fromOffsets: indices, toOffset: newOffset)
                })
            }.navigationBarTitle(Text("editable list"), displayMode: .inline)
            .navigationBarItems(trailing: EditButton())
        }
        
        
    }
}

よくあるForEach(data, \.self)などを使うと、SampleRowBindingとして値を渡すことができません。
なので、下記を使用します。

ForEach.init(_ data: Range<Int>, content: @escaping (Int) -> Content)
ForEach(self.data.indices) { index in
  SampleRow.init(model: self.$data[index]) {
    debugPrint("onCommit")
    debugPrint(self.data)                    
  }                
}

data.indicesで取り出したindexを使い、SampleRowBindingを渡しています。
変更が終わった後(returnした後)に値を書き出してみると、変更された後の値になっています。

サンプル

https://github.com/usk-sample/ListWithTextFieldSample

Discussion

YuusukeYuusuke

<修正>
ForEach(self.data.indices) { index in

ForEach(self.data.indices, id: .self) { index in

以下のエラーが出ていたため。

ForEach(_:content:) should only be used for constant data. Instead conform data to Identifiable or use ForEach(_:id:content:) and provide an explicit id!