ZeeZide/CodeEditor

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

stefanomondino opened this issue · 8 comments

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.

Registrazione.schermo.2022-05-15.alle.14.54.31.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!

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

@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)
    }
}

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

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

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

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.

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

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.