SwiftUI TextField input is super laggy for SwiftData object

have a SwiftUI View where I can edit financial transaction information. The data is stored in SwiftData. If I enter a TextField element and start typing, it is super laggy and there are hangs of 1-2 seconds between each input (identical behaviour if debugger is detached). On the same view I have another TextField that is just attached to a @State variable of that view and TextField updates of that value work flawlessly. So somehow the hangs must be related to my SwiftData object but I cannot figure out why.

This used to work fine until a few months ago and then I could see the performance degrading.

I have noticed that when I use a placeholder variable like

@State private var transactionSubject: String = ""

and link that to the TextField, the performance is back to normal. I am then using

.onSubmit {
                            self.transaction.subject = self.transactionSubject
                        }

to update the value in the end but this again causes a 1 s hang. :/

Below the original code sample with some unnecessary stuff removed:

struct EditTransactionView: View {
    @Environment(\.modelContext) var modelContext
    @Environment(\.dismiss) var dismiss
    
    @State private var testValue: String = ""
    
    @Bindable var transaction: Transaction
    
    init(transaction: Transaction) {
        self.transaction = transaction
        let transactionID = transaction.transactionID
        let parentTransactionID = transaction.transactionMasterID
        
        _childTransactions = Query(filter: #Predicate<Transaction> {item in
            item.transactionMasterID == transactionID
        }, sort: \Transaction.date, order: .reverse)
        
        _parentTransactions = Query(filter: #Predicate<Transaction> {item in
            item.transactionID == parentTransactionID
        }, sort: \Transaction.date, order: .reverse)
        
        
        print(_parentTransactions)
    }
    
    //Function to keep text length in limits
    func limitText(_ upper: Int) {
        if self.transaction.icon.count > upper {
            self.transaction.icon = String(self.transaction.icon.prefix(upper))
        }
    }
    
    var body: some View {
        ZStack {
            Form{
                Section{
                    //this one hangs
                    TextField("Amount", value: $transaction.amount, format: .currency(code: Locale.current.currency?.identifier ?? "USD"))
                    
                    //this one works perfectly
                    TextField("Test", text: $testValue)
                    
                    HStack{
                        TextField("Enter subject", text: $transaction.subject)
                        .onAppear(perform: {
                            UITextField.appearance().clearButtonMode = .whileEditing
                        })
                        
                        Divider()
                        TextField("Select icon", text: $transaction.icon)
                            .keyboardType(.init(rawValue: 124)!)
                            .multilineTextAlignment(.trailing)
                    }
                }                
            }
            .onDisappear(){
                if transaction.amount == 0 {
                    //                modelContext.delete(transaction)
                }
            }
            .onChange(of: selectedItem, loadPhoto)
            .navigationTitle("Transaction")
            .navigationBarTitleDisplayMode(.inline)
            .toolbar{
                Button("Cancel", systemImage: "trash"){
                    modelContext.delete(transaction)
                    dismiss()
                }
            }
            .sheet(isPresented: $showingImagePickerView){
                ImagePickerView(isPresented: $showingImagePickerView, image: $image, sourceType: .camera)
            }
            .onChange(of: image){
                let data = image?.pngData()
                if !(data?.isEmpty ?? false) {
                    transaction.photo = data
                }
            }
            .onAppear(){
                cameraManager.requestPermission()
                
                setDefaultVendor()
                setDefaultCategory()
                setDefaultGroup()
            }
            .sheet(isPresented: $showingAmountEntryView){
                AmountEntryView(amount: $transaction.amount)
            }
        }
    }
}
Answered by DTS Engineer in 855859022

To investigate a performance issue, I typically start with profiling with Instruments.app, which gives me an idea about what really triggers a hang, and provides a good starting point of my investigation.

In your case, I can see that every key stroke changes transaction, which changes the result sets of your queries, which refreshes your SwiftUI view (EditTransactionView). Because transaction is a binding, the change refreshes the other views that rely on the model as well.

It doesn't seem that transaction.amount is used as a filter in any place, and so one easy improvement is to bind the text field with a state, as you did with testValue, and only update transaction.amount in the .onSubmit of the text field:

@State private var transactionAmount: String = ""

TextField("Amount", value: $transactionAmount, format: .currency(code: Locale.current.currency?.identifier ?? "USD"))
.onSubmit {
    transaction.amount = transactionAmount
}

This should avoid updating transaction while typing.

If changing the value of transaction.amount triggers a hang, you might indeed need to profile your app to figure out the performance bottleneck, and go from there.

Best,
——
Ziqiao Chen
 Worldwide Developer Relations.

To investigate a performance issue, I typically start with profiling with Instruments.app, which gives me an idea about what really triggers a hang, and provides a good starting point of my investigation.

In your case, I can see that every key stroke changes transaction, which changes the result sets of your queries, which refreshes your SwiftUI view (EditTransactionView). Because transaction is a binding, the change refreshes the other views that rely on the model as well.

It doesn't seem that transaction.amount is used as a filter in any place, and so one easy improvement is to bind the text field with a state, as you did with testValue, and only update transaction.amount in the .onSubmit of the text field:

@State private var transactionAmount: String = ""

TextField("Amount", value: $transactionAmount, format: .currency(code: Locale.current.currency?.identifier ?? "USD"))
.onSubmit {
    transaction.amount = transactionAmount
}

This should avoid updating transaction while typing.

If changing the value of transaction.amount triggers a hang, you might indeed need to profile your app to figure out the performance bottleneck, and go from there.

Best,
——
Ziqiao Chen
 Worldwide Developer Relations.

And isn't the whole point of the bindings, that all views get refreshed automatically?

Yes, it is. Changing a binding triggers an update on the views that render with the binding.

How is updating that value changing the result sets of my queries?

Oh, I assumed that changing the transaction would impact the result set of your queries in some way. I can be wrong though, especially with another look into your predicates. You can check if your query does a fetch every time your SwiftUI view updates by adding -com.apple.CoreData.SQLDebug 1 to your launch argument list, as described here, and looking into the Xcode console log.

If you can provide a runnable minimal project that demonstrates the issue, I'd interested in taking a look as well.

Best,
——
Ziqiao Chen
 Worldwide Developer Relations.

SwiftUI TextField input is super laggy for SwiftData object
 
 
Q