ZeeZide / CodeEditor

A SwiftUI TextEditor with syntax highlighting using Highlight.js
MIT License
448 stars 56 forks source link

Multiple instances in custom views are messing up the text state on macOS #15

Open stefanomondino opened 2 years ago

stefanomondino commented 2 years ago

Hello and first of all thank you for this great library :)

I'm trying to replicate something similar to Xcode tabs, with some kind of top "tab bar" selecting each code file I want to edit.

Since SwiftUI's TabView doesn't (currently) allow for tab bar customization, I've created my own using buttons in a HStack. When I click a button, the "main view" (containing the CodeEditor for selected file) changes and shows me file contents.

The bug I've found is very strange, after changing one of two tabs the global "state" starts to mixup, showing up either wrong file content (the previous one) or resetting contents after window loses focus or when my tab changes.

System's TextEditor seems to work fine.

I have a strong suspect this is somehow related to SwiftUI.

I hope attached video is "self explaining" (window always has focus, but something strange also happens when you focus any other application).

you can find it here https://github.com/stefanomondino/CodeEditor in the Demo folder.

https://user-images.githubusercontent.com/1691903/168474027-aedb3b73-5bcb-4183-b755-d745c11941cc.mov

Also, it's worth nothing that also CodeEditorView (a very similar project) has the same issue.

Let me know if you have any idea, thanks!

helje5 commented 2 years ago

Are you actually switching the subviews, or just replace their contents?

stefanomondino commented 2 years ago

@helje5 sorry didn't push the demo code to my fork ;)

This is my "SheetView"

https://github.com/stefanomondino/CodeEditor/blob/main/Demo/Sources/SheetView.swift

I'm passing my contents as a view builder


struct SheetView<Content: View>: View {

    @State private var currentIndex: Int? = nil

    let items: [SheetElement]
    @ViewBuilder let contents: (Int) -> Content

    init(items: [SheetElement], @ViewBuilder contents: @escaping (Int) -> Content) {
        self.items = items
        self.contents = contents
        currentIndex = items.isEmpty ? nil : 0
    }

    var body: some View {
        VStack(spacing: 0) {
            ScrollView(.horizontal, showsIndicators: false) {
                HStack(alignment: .center, spacing: 0) {
                    ForEach(items.indices, id: \.self) { index in
                        SheetButton(index: index, selection: $currentIndex, item: items[index])
                        Divider()
                    }
                    Spacer()
                }
                .padding(0)
            }
            .frame(height: 30)
            Divider()
            if let index = currentIndex {
                contents(index)
            } else {
                Color(.clear)
            }
        }
        .layoutPriority(1)
    }
}
stefanomondino commented 2 years ago

And this is how i use it (Editor is an observable object holding the binding)

let items = ["Test file 1", "Another test", "and yet again"].map { Editor.init($0)}
 SheetView(items: items) { index in
                    CodeEditor(source: items[index].binding,
                               language: .swift)

                }
helje5 commented 2 years ago

Maybe try:

contents(index)
  .id(index)

This should create a new View for each index. (but even if that works, it is still a bug that should be fixed, not quite sure how/why this is happening).

stefanomondino commented 2 years ago

@helje5 woah! and where did that id came from? :D

seems to work fine for the moment! Also you just teached me something really important I didn't know

Maybe it's worth adding a .id(UUID()) modifier to every CodeEditor view? Or maybe it's too much.

helje5 commented 2 years ago

No, that would recreate the backing View on every single run, not recommended (in general, not just here, the UUID thing is just a hack).

I still think the thing is a bug proper. Maybe diffing gets confused w/ representables.

stefanomondino commented 2 years ago

From a very quick debug session on my project, seems like the source.projectedValue in the representable is keeping somehow the old value.

But from my point of view it makes total sense to add an .id to my custom tab view, it would be interesting to know if TextEditor and/or. TabView are doing something similar in their internal implementations.

Maybe it's something we can find out with Debug Memory Graph

helje5 commented 2 years ago

TabView has the tag, which presumably is similar to id.

As mentioned, the thing should work w/o the id and TextEditor can't attach an own id either as this is bound to the hierarchy. So I suspect TextEditor just does something right in the reload, which CodeEditor is doing wrong. No idea, needs debugging.