1

I 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.

Below the code sample with some 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)
            }
        }
    }
}
3
  • could you provide the code related to this issue, for a better analysis? Commented Aug 25 at 21:16
  • sorry, just added the code piece Commented Aug 25 at 21:40
  • What is all that code in the init doing, you seem to initiate properties that doesn’t exist? Commented Aug 26 at 9:15

2 Answers 2

1

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

When I first tried to reproduce the issue with a smaller number of elements, I didn’t notice any lag — everything behaved fine, just like you mentioned it used to. But since you said the slowdown started appearing later, I tried seeding the database with a much larger dataset (around 10,000 rows), assuming your data may have gradually grown to a point where it became heavy enough to trigger the lag. After that, I was able to see the same laggy keystrokes.

What seemed to be happening was that each character edit invalidates the queries, and with thousands of rows that got expensive. What helped for me was giving the TextField its own local @State value and only saving back to the SwiftData model when the field loses focus or on submit. That way typing stayed smooth, and SwiftData only updated once instead of on every keystroke.

@State private var amountText: String = ""
@FocusState private var amountFocused: Bool

// some code here...

init(transaction: Transaction) {
    // some code here as well...
    _amountText = State(initialValue: Self.currencyString(
        from: transaction.amount,
        currencyCode: Locale.current.currency?.identifier ?? "USD"
    ))
}

TextField("Amount", text: $amountText)
    .keyboardType(.decimalPad)
    .focused($amountFocused)
    .onChange(of: amountFocused) { focused in
        if !focused, let dec = Self.parseCurrency(
            amountText,
            currencyCode: currencyCode
        ) {
            transaction.amount = dec
            
            amountText = Self.currencyString(from: dec, currencyCode: currencyCode)
        }
    }

// these function you can safely modify for your need
static func currencyString(from dec: Decimal, currencyCode: String) -> String {
    let nf = NumberFormatter()
    nf.numberStyle = .currency
    nf.currencyCode = currencyCode
    nf.maximumFractionDigits = 2
    nf.minimumFractionDigits = 0
    return nf.string(from: dec as NSDecimalNumber) ?? ""
}

static func parseCurrency(_ s: String, currencyCode: String) -> Decimal? {
    let c = NumberFormatter()
    c.numberStyle = .currency
    c.currencyCode = currencyCode
    c.isLenient = true
    if let n = c.number(from: s) {
        return n.decimalValue
    }
    
    let d = NumberFormatter()
    d.numberStyle = .decimal
    d.decimalSeparator = Locale.current.decimalSeparator
    if let n = d.number(from: s) {
        return n.decimalValue
    }
    
    let allowed = CharacterSet(charactersIn: "0123456789" + (Locale.current.decimalSeparator ?? "."))
    let cleaned = s.unicodeScalars.filter { allowed.contains($0) }
    if let n = d.number(from: String(String.UnicodeScalarView(cleaned))) {
        return n.decimalValue
    }
    return nil
}

Might be worth trying this approach to see if it makes things more responsive for you too.

Sign up to request clarification or add additional context in comments.

2 Comments

I tried that approach and put the update on the .onSubmit() of the TextField and indeed, the performance is back to normal. But now I get a 1s hang when that event is triggered. Totally beyond me how Apple believes how devs can use SwiftData if it already has performance issues with a couple thousand records ...
It definitely feels like an odd design decision on Apple’s part, since you’d expect it to handle way larger datasets without these kinds of hiccups :(
0

I'm not an expert, but I'm pretty sure you shouldn't use an initializer on a view. That code should be in the onAppear method. You can't count on the init method. You have the transaction in the binding.

2 Comments

The code in the init is working fine and moving the _childTransactions = Query... piece to onAppear will cause the compiler to crash with the usual "The compiler is unable to type-check this expression in reasonable time; try breaking up the expression into distinct sub-expressions "
Why not delete that code since you are not using the query in your view?

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.