Synced Realm SwiftUI typing freeze

I’ve been running into an issue on realm-swift 10.25.0 where editing a field with the bindings such as:

@ObservedRealmObject var address: Address

TextField("City", text: $address.city)

If I type too fast (meaning, still slower than regular speed) the whole app freezes with an error like this:

Binding action tried to update multiple times per frame.

I’m currently moving toward storing strings in state variables and updating Realm objects on completion just to get around it, but wondering if anyone else is seeing this happen or if there is a way to make sure that it doesn’t happen?

1 Like

I’ve seen this happen, too, @Kurt_Libby1 , but haven’t been able to reproduce with consistency, so I (perhaps naively) assumed I was doing something wrong. I did the same workaround - stored the strings in state variables to sidestep the issue. I’ll flag this with the SDK team and see if they have any info around this.

2 Likes

Following up, a member of the Swift SDK team recommends debounce to reduce the update frequency, @Kurt_Libby1 . We don’t currently have anything in our docs or example projects showing this, so I don’t have anything to point you at to show you how best to implement this. I’m making a ticket to provide some docs/code guidance around this, though. Meanwhile, if you try this in your project and it resolves your issue, I’d love to hear about it.

I’ve been experiencing a similar problem and it’s pretty inconsistent. In testing on an attached iPad Pro, I’m watching my debug panel, and after freezing the CPU hits 100% and the Memory climbs to over 1GB before Xcode eventually halts the simulator - see attached.

I’m using a debounce Observable:

class DebounceTextObserver : ObservableObject {
    
    @Published var bounced = ""
    @Published var tmp = ""
        
    init(delay: DispatchQueue.SchedulerTimeType.Stride) {
        $tmp
            .debounce(for: delay, scheduler: DispatchQueue.main)
            .assign(to: &$bounced)
    }
}

Setup like so:

@StateObject var bounceDescrip = DebounceTextObserver(delay: 0.75)
...
TextField("", text: $bounceDescrip.tmp, onCommit: { focusedField = .cost })
                           ...
                            .onChange(of: bounceDescrip.bounced, perform: { value in
                                if estimateItem.realm == nil {
                                    estimateItem.descrip = value
                                } else {
                                    guard let estimateItem = estimateItem.thaw(), let realm = estimateItem.realm else { return }
                                    
                                    try! realm.write {
                                        estimateItem.descrip = value
                                    }
                                }
                            })

At a loss here for how to deal with this.
Also (and forgive my ignorance here), I’m not on a production DB yet and still using the ‘Sandbox’ (General) cluster, and so curious if my backend is having trouble syncing? Could my move to a more robust cluster solve this?

Any help if appreciated.

Thanks,
Ron

Hey @Ron_Dyck,

I can confirm that debouncing did not work for me either. I did it differently, but for whatever reason (and I do think that @Dachary_Carey said that they’re at least looking into the reason and a permanent solution) it still freezes.

I have been loading the values into separate state variables onAppear and then if the state value != realm value, I’m showing a save button so that nothing is updated until the end in one write.

It is a very annoying issue that I’ve had to use on quite a few screens, but not all. I haven’t been able to isolate the cause. And the workaround is a worse experience for sure. I do wish they would add some debounce timing into the standard way that the implicit writes work.

@Jason_Flax, do you know if this is on the roadmap?

Not Jason here (obviously), but everyone is pretty busy right now getting things ready for MongoDB World next week and I didn’t want to leave you folks hanging.

Yes, the SDK team has some time earmarked a bit later to look into SwiftUI performance issues, and this is definitely one of them. One of the things I’ve discussed with them was a POC that includes debounce so folks don’t have to implement it themselves, but I think they’re also looking bigger-picture into a handful of performance-related things. Addressing this issue is on the roadmap. Sorry you’re having to use workarounds in the meantime.

2 Likes

Thanks @Dachary_Carey! I’ll be in NYC for the MDB World, so hopefully will get to meet some of the realm team. Really looking forward to the sessions and seeing what is coming for Realm!

1 Like

Any updates on this? Still struggling with freezes even with bounce set at 1.25 seconds.

2 Likes

Same; I experience this 100% of the time on every view I’ve implemented. I was assuming there was something I hadn’t learned yet that would easily fix it.

Alongside this issue, I am also experiencing where characters in strings that are typed are lost.

So, if the user types:
“This is a sentence.”

Their UI may show:
“This is a sentence.”

And the db actually has:
“This s a sntence”

Also, upon editing strings (opposed to typing in an empty field), then the cursor bounces from where it is to the end of the string and then all the user input ends up at the end of the string. An example when fixing a typo in:

“This is s sentence.”

If the cursor is placed at the “s” and a backspace is typed to remove the “s” and then an “a” is typed, you end up with:
“This is sentence.a”

All this behavior is occurring while the console log is flooded with:
“Binding action tried to update multiple times per frame.”

This will be a blocker to our app going thru apple review in a couple months time. Currently, the dev env is on the free/shared instance.

@Ron_Dyck @Kurt_Libby1 @Dachary_Carey

I can’t imagine that every customer is having this issue. I wonder if it is a specific IOS version issue?

I’m on macos 12.4, xcode 13.4.1
Simulator v13.4.1 (977.2)
SimulatorKit 618
CoreSimulator 802.6.1
Realm Cocoa v10.28.4 Platform Version Version 15.5 (Build 19F70)

@Joseph_Bittman I have been using Realm Swift 10.28.6 with swift package manager and although I do get the Binding<String> action tried to update multiple times per frame. warning in the console log, I am not experiencing any freezes anymore.

Maybe try updating to the newest version and see if you see an improvement?

This issue has resurfaced for me (I’m on Realm Swift 10.41.0). Not nearly as bad as before, but writing straight to the binding like in the quickstart example does cause noticeable delays and issues. My users are noticing and complaining.

For anyone else who is seeing this issue, I ended up using DebounceTextField from this medium post. I used an ObservedRealmObject still and thawed it to write after the debounce. Annoying workaround, but it actually works with no noticeable lag.

@ObservedRealmObject var room: Room
@State private var name: String = ""
...

  DebounceTextField(label: "Name", value: $name) { value in
     let thawedRoom = room.thaw()!
     let realm = thawedRoom.realm!
       do {
         try! realm.write({
           thawedRoom.name = value
         })
       }
     }
     .onAppear(perform: {
       self.name = room.name
     }

1 Like

nice tip. i’ve found success in a similar pattern that i think results in less boilerplate, you can avoid the thaw: GitHub - Tunous/DebouncedOnChange: SwiftUI onChange View extension with debounce time but on second thought i’m not sure it’s as good as yours at avoiding view reloads

edit: I’m trying this out, feel free to use under MPLv2 license

import SwiftUI
import Combine
import RealmSwift

@available(macOS 13.0, *)
public struct RealmTextField<ObjectType>: View where ObjectType: RealmSwift.Object & Identifiable {
    @ObservedRealmObject var object: ObjectType
    @Binding var objectValue: String
    
    @State var realtimeValue = ""
    @State var publisher = PassthroughSubject<String, Never>()
    var label: String
    
    var valueChanged: ((_ value: String) -> Void)?
    
    @State private var debounceSeconds = 1.110
    
    public var body: some View {
        TextField(label, text: $realtimeValue,  axis: .vertical)
            .disableAutocorrection(true)
            .task {
                Task { @MainActor in
                    realtimeValue = objectValue
                }
            }
            .onChange(of: realtimeValue) { value in
                publisher.send(value)
            }
            .onReceive(
                publisher.debounce(
                    for: .seconds(debounceSeconds),
                    scheduler: DispatchQueue.main
                )
            ) { value in
                if let valueChanged = valueChanged {
                    valueChanged(value)
                }
            }
    }
    
    public init(_ title: String, object: ObjectType, objectValue: Binding<String>) {
        self.object = object
        _objectValue = objectValue
        self.label = title
    }
}