kyle-n / HighlightedTextEditor

A SwiftUI view for dynamically highlighting user input
MIT License
716 stars 68 forks source link

AppKit and UIKit editors slow down when typing extremely long blocks of text #25

Open kyle-n opened 3 years ago

kyle-n commented 3 years ago

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.

Expected behavior

Screenshots

https://user-images.githubusercontent.com/13384477/104211288-1fc28e80-53f1-11eb-8627-a6a3a71cb3b3.mov

Environment Please include:

Additional context

kyle-n commented 3 years ago

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

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.

Parth commented 3 years ago

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.

Parth commented 3 years ago

cc: @raayan @tvanderstad

Parth commented 3 years ago

Worth noting that apple notes handles huge amounts of text flawlessly both on macOS and iOS

Parth commented 3 years ago

https://github.com/twostraws/Sourceful/issues/1

kyle-n commented 3 years ago

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

ceojosef commented 3 years ago

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.

kyle-n commented 3 years ago

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

Parth commented 2 years ago

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.

Parth commented 2 years ago

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.

kyle-n commented 2 years ago

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

Parth commented 2 years ago

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.