AppKit and UIKit editors slow down when typing extremely long blocks of text
Describe the bug If I type extremely long blocks of text, the UIKit and AppKit editors slow down. If I type >52,000 characters in either editor, the editor takes a noticeable amount of time to catch up with whatever I'm typing.
To Reproduce See gist with sample project.
- Set
@Statevar for HighlightedTextEditor to a string of >50,000 characters - Attempt to type in the editor
Expected behavior
- The editor accepts and displays text from new keypresses at the same fast speed, no matter how many characters I have typed previously.
Screenshots
https://user-images.githubusercontent.com/13384477/104211288-1fc28e80-53f1-11eb-8627-a6a3a71cb3b3.mov
Environment Please include:
- macOS 11.2 Beta (20D5029f) and iOS 14.2 (simulator)
Additional context
- Happening on current
mainbranch
After investigation, I have narrowed down the slowness to one line of code in both editors.
public func textViewDidChange(_ textView: UITextView) {
// For Multistage Text Input
guard textView.markedTextRange == nil else { return }
self.parent.text = textView.text // <--------------------
selectedTextRange = textView.selectedTextRange
}
For whatever reason, passing UITextView's content back up to SwiftUI causes an operation that takes, by my testing, 80x longer than the other operations I tested. I'm not sure why.
Background
Right now, here's what happens when the user presses a key in a HighlightedTextEditor (this example uses UIKit but it also applies to the AppKit editor).
- The keypress changes the
UITextViewcontent, which triggerstextViewDidChange(). - That updates
parent.text, which passes the new editor content back up to SwiftUI via the@Binding text. - Updating this variable triggers
updateUIView(), which runs whenever SwiftUI needs to update a wrapped UIKit view with new information. -
updateUIView()creates a new attributed string, highlighted according to what the user has declared, and assigns that highlighted string to the wrappedUITextView'sattributedText.
Things I've tried
I have tried removing the part of updateUIView() where we create a new highlighted string, to see if that improves performance, and no luck. That whole function runs in a fraction of a millisecond, vs. ~80ms for just the line where we update self.parent.text.
I have also tried updating self.parent.text on a background thread after a quarter-second debounce. This alleviated the moment-to-moment lag while typing, but the editor would store incorrect text that did not match what you typed.
Is this SwiftUI system behavior? I have no idea what to do here.
Hi! This library is great, we're probably going to be using it within github.com/lockbook/lockbook. I'm also highly motivated to sort this out. I ran into a pretty high amount of latency editing a document in SwiftUI's TextEditorView. I made a stackoverflow. While trying various libraries, this was the only one which didn't have a very high latency on macOS for large documents. Still very high latency on the iOS (UIKit) side of things however.
Would be happy to support sorting this out in any way we can, reading through the code now.
cc: @raayan @tvanderstad
Worth noting that apple notes handles huge amounts of text flawlessly both on macOS and iOS
https://github.com/twostraws/Sourceful/issues/1
@Parth Thank you for the kind words! Let me know when HLTE ships in your app, we'll add Lockbook as a featured app.
Re: performance, yeah, there are many UIKit/AppKit editors with great performance. Do they just not use SwiftUI? The fact that you hit high latency using TextEditor makes me think it's a SwiftUI problem, but I don't know.
iOS Notes might use SwiftUI for some screens but that app seems too old to be on SwiftUI for the editor, but, again, guesswork.
Better way for very big texts - implement a LazyUITextView. Wrap text to parts (ex 10k characters) and work with it separately. And when update -> compile saved in memory parts with changed one. Only one problem - need to recognize scroll events to dynamically change visible part of text in editor.
@ceojosef This is the frustrating thing - I've tried builds with all highlighting turned off, and it still slows down with long blocks of text. If highlighting zero characters does not solve the problem, I don't think highlighting 10k at a time would either. It really seems to be when the data is passed back up to SwiftUI via that binding, like I was saying in this comment.
Gone down a deep rabbit-hole regarding all this.
I think what lockbook really needs doesn't quite exist out there (maybe we could hop on a call and discuss our ambition and see if there's an opportunity for mutual collaboration?).
But regarding this issue, I think exposing a Binding back to the user is probably the core issue here. Exposing a binding will cause swiftui to attempt to redraw atleast one component, perhaps this is what's expensive.
I believe if the exposed API were closer to an explicit management of setting, and getting current document status it would eliminate these performance issues.
Of-course this would result in a non-swifutui-friendly API, but I think it would be an API that reflects the idea that you shouldn't be updating swiftui components on every keystroke.
Would require some validation to be sure. But I think this API, along with some level of debouncing would more closely reflect what most people (like lockbook) want to do with a component like this one.
So something like lockbook could hand you the initial state of the document, be notified anytime we should save (configurable debounce) and be able to inform the editor of new changes that come from the outside world (say a file changed on disk).
Then you would have, as far as I can tell the only swiftui markdown library that can scale to reasonably sized markdown documents.
Would be happy to discuss specifically what lockbook would need out of a swiftui component over a call if you're interested in contributing to our project. It's basically what I've laid out above ^ + more specific considerations for the ideal markdown experience.
I retested this bug with the reproduction project and found a new piece of evidence: The lag does not happen if the user is typing at the of the text, no matter how long the text. Happens on iOS and macOS. Maybe SwiftUI renders the text from start to finish?
@Parth Thank you for the offer to collaborate, but I must pass. I have previous work and personal commitments.
The version of HLTE you describe makes sense given the requirements of your app. However, I think most people's documents are not 50,000 characters (If I am wrong, please comment!). So any change contributed to HLTE should not place a breaking change or any extra burden on those users.
It may be easier for you to just fork this library. I will not be offended - it makes sense given your requirements 😄 .
Great to hear from you, totally understand, it's a great library for normal sized docs. I think I'm going to give a ground up approach a go.