iosswiftuidata-bindinglazyvgrid

Performance issue with many Textfields


I need to have many textfields for something like a form. There is always something like a key textfield and a value textfield next to another. So I want to use a LazyVGrid with two columns. Each textfield uses at this time a custom binding, this is the textfield:

struct FocusField: View {
    
    @State var index: Int
    @Binding var focus: String?
    @Binding var content: [String]
    
    @FocusState var isFocused: String?
    var nextFocus: () -> Void
    
    @ObservedObject var player = Player.shared
    
    var body: some View {
        if index < content.endIndex {
            HStack{
                TextField(((index % 2 == 0) ? player.language1 : player.language2),
                          text:
                            Binding<String>(
                                get: {
                                    if content.endIndex > index {
                                        return content[index]
                                    }
                                    return ""
                                    
                                }, set: {newValue in
                                    if content.endIndex > index {
                                        content[index] = newValue
                                    }
                                }
                            )
                          //                          ,
                          //                          axis: .vertical
                )
                
#if os(iOS)
                .autocapitalization(UITextAutocapitalizationType.none)
#endif
                .multilineTextAlignment(.leading)
                .onSubmit {
                    self.nextFocus()
                }
                .onChange(of: focus, perform: { newValue in
                    self.isFocused = newValue
                })
                .focused(self.$isFocused, equals: "\(content[index])\(index)")
                if isFocused == "\(content[index])\(index)"{
                    if index % 2 != 0{
                        Button(role: .destructive) {
                            if content.count > 2 {
                                content.remove(at: index)
                                content.remove(at: index)
                            } else {
                                content = ["",""]
                            }
                        } label: {
                            Image(systemName: "trash.fill")
                        }
                    }
                }
                
            }
        }
    }
}

Here is how I use them. Inside a LazyVGrid with two columns:

struct EditViewInDetailView: View{
    @Binding var item: Item?
    @ObservedObject var viewModel = VocabDetailViewModel.shared
    @State var focus: String? = nil
    @Binding var isExpanded : PresentationDetent
    
    let columns : [GridItem] = Array(repeating: .init(.flexible()), count: 2)
    var body: some View{
        ScrollViewReader{scroll in
            ScrollView{
                LazyVGrid(columns: columns) {
                    ForEach(0..<(viewModel.content.count), id: \.self) { index in
                        LazyVStack{
                            Spacer()
                            FocusField(index: index, focus: $focus, content: $viewModel.content) {
                                if index % 2 != 0 {
                                    viewModel.content.insert("", at: (index % 2 == 0 ? (index+2) : (index + 1))  )
                                    viewModel.content.insert("", at: (index % 2 == 0 ? (index+2) : (index + 1))  )
                                }
                                DispatchQueue.main.asyncAfter(deadline: .now() + (0.01 * Double(index)) ) {
                                    focus = "\(viewModel.content[index+1])\(index+1)"
                                }
                            }
                            Spacer()
                            Divider()
                                .padding(.horizontal, -10)
                                .background(.gray)
                        }
                    }
                    
                    
                }
                
                Button("Zurück"){
                    isExpanded = .height(81)
                }
                .buttonStyle(BorderedProminentButtonStyle())
                Spacer()
                    .frame(height: 100)
            }
        }
    }
}

If there are many Textfields the scroll performance drops massively and it is not possible to type in fields which have a high index. The Keyboard shows and resigns immediately, when you tap in a textfield.

I tried using Text instead and changing the text to a textfield when focused, but it didn't change to a textfield.

Do someone know what the part of the code is that makes it so slow?


Solution

  • I think you should use a list which iterates over key, value pairs. Then you don't need to check for index % 2 and don't need a custom binding. For deleting you can use .onDelete modifier instead of the custom delete button. Because of the much simpler code you can directly use focusstate in the view containing the list. So you don't need the isFocused property. Also it should look much better ;-).