I'm creating a document-based application where data is represented by TextFields in a TableView (it could also be a List, the same issue occurs). When the app SwiftUI app on an Intel MacBook Air, I get a lot of keyboard lag whenever there are more than a dozen rows in my table. It's present on the Apple Studio too, but less noticeable. I've tried changing the table into a List and LazyVStack, but it doesn't seem to make much difference. Using the Swift UI instrument, it looks to me like every TextField on the page is being redrawn on every keystroke, even though their values haven't changed.
I also tried using a custom TextField with a debounce added in (with this as a starting point). This works well for reducing the lag, but I don't think this is how debouncing is intended to be used and I ended up with some strange behaviour.
I suspect that it is rather the case that I've misunderstood how to using @Binding variables in a Document Based application, or possibly I have misconfigured the Struct where I store the data. So here are the essential parts of my code, which will hopefully make it clear where I have gone wrong without having to run anything.
struct ClaraApp: App {
@StateObject var globalViewModel = GlobalViewModel()
var body: some Scene {
DocumentGroup(newDocument: ClaraDocument(claraDoc:GroupVocab())) { file in
MainContentView(data: file.$document)
.environmentObject(self.globalViewModel)
}
}
struct MainContentView: View {
@Binding var data: ClaraDocument // Binding to the document
@EnvironmentObject var globalViewModel : GlobalViewModel
@StateObject var viewModel: ViewModel = ViewModel()
var body: some View {
HostingWindowFinder { window in
if let window = window {
self.globalViewModel.addWindow(window: window)
print("New Window", window.windowNumber)
self.globalViewModel.addViewModel(self.viewModel, forWindowNumber: window.windowNumber)
window.becomeKey()
}
}
.frame(width:0, height: 0)
VStack{
TabView(selection: $viewModel.activeTab){
VocabView(vocabs: $data.claraDoc.vocabs, selectedVocab: $viewModel.selectedVocab)
.tabItem{
Label("Vocabulary", systemImage: "tablecells")
}
.tag(TabType.vocab)
//more tabs here
}
}
}
}
struct VocabView: View{
@Binding var vocabs: [Vocab] // Binding to just the part of the document concerned by this view
@Binding var selectedVocab: Vocab.ID?
var body: some View{
VStack(){
VocabTable(vocabs: $vocabs, selection: $selectedVocab)
.padding([.top, .leading, .trailing])
HStack{
Button("-"){
if selectedVocab != nil{
let oldSelectionIndex = vocabs.firstIndex(where: {$0.id == selectedVocab!})
if oldSelectionIndex != nil{
if oldSelectionIndex! > 0{
selectedVocab = vocabs[oldSelectionIndex! - 1].id
} else {
selectedVocab = nil
}
vocabs.remove(at: oldSelectionIndex!)
}
}
}
.disabled(selectedVocab == nil)
Text("\(String(vocabs.count)) entries")
Button("+"){
let newVocab = Vocab(id: UUID(), word: "", def: "", trans: "", visNote: "", hidNote: "", link: Link(linked: false), date: Date())
vocabs.append(newVocab)
selectedVocab = newVocab.id
}
}
}
}
}
struct VocabTable: View{
@Binding var vocabs: [Vocab]
@Binding var selection: Vocab.ID?
var body: some View{
Table($vocabs, selection: $selection){
TableColumn("Word") {vocab in
TextField("Word", text: vocab.word)
}
TableColumn("Definition") {vocab in
TextField("Definition", text: vocab.def)
}
TableColumn("Translation") {vocab in
TextField("Translation", text: vocab.trans)
}
TableColumn("Visible note") {vocab in
TextField("Visible note", text: vocab.visNote)
}
TableColumn("Hidden note") {vocab in
TextField("Hidden note", text: vocab.hidNote)
}
TableColumn("Created") {vocab in
HStack{
Text(vocab.date.wrappedValue, style: .date)
Text(vocab.date.wrappedValue, style: .time)
}
}
}
.onDeleteCommand{
if selection != nil{
let oldSelectionIndex = vocabs.firstIndex(where: {$0.id == selection!})
if oldSelectionIndex != nil{
if oldSelectionIndex! > 0{
selection = vocabs[oldSelectionIndex! - 1].id
} else {
selection = nil
}
vocabs.remove(at: oldSelectionIndex!)
}
}
}
}
}
// vocab struct which is contained as an array [Vocab] inside the GroupVocab struct
struct Vocab: Identifiable, Codable, Equatable, Hashable {
let id: UUID
var word: String
var def: String
var trans: String
var visNote: String
var hidNote: String
var date: Date
init(id: UUID = UUID(), word: String? = "", def: String? = "", trans: String? = "", visNote: String? = "", hidNote: String? = "", date: Date? = Date()){
self.id = id
self.word = word ?? ""
self.def = def ?? ""
self.trans = trans ?? ""
self.visNote = visNote ?? ""
self.hidNote = hidNote ?? ""
self.date = date ?? Date()
}
static func == (lhs: Vocab, rhs: Vocab) -> Bool {
return lhs.id == rhs.id && lhs.word == rhs.word && lhs.def == rhs.def && lhs.trans == rhs.trans && lhs.visNote == rhs.visNote && lhs.date == rhs.date
}
}
struct GroupVocab: Codable{
var appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String
var groupName: String = ""
var vocabs = [Vocab]()
var learners = [Learner]()
var lessons = [Lesson]()
var startDate = Date()
var endDate = Date()
}
If that doesn't shed any light, here's my attempt at making a minimal example. It isn't nearly as laggy as the actual app, but from what I can tell it exhibits the same problems. Of course, it could be something in my actual app, which is not present in this minimal example, that I have overlooked. For example, I know that the menu bar is redrawn when editing the document, but removing the menu bar doesn't improve performance. So, I'm assuming that the reduced (albeit still present) lag is due to the general baggage of the program and not one specific element that I haven't taken into account. Obviously if there are no obvious problems in the above, I will need to go back and check everything again, but to my knowledge I have already tried removing and readding each part of the application individually to no avail.
Finally, this is what the actual app looks like in use: