3

Let's say I have:

  • structure Document, which represents text document.
  • EditorView — an NSTextView, wrapped with Combine, which binds to Document.content<String>.

Document is a part of complex store:ObservableObject, so it can be bouneded to EditorView instance.

When I first create binding, it works as expected — editing NSTextView changes value in Document.content.

let document1 = Document(...)
let document2 = Document(...)
var editor = EditorView(doc: document1)

But if change binding to another Document...

editor.doc = document2

...then updateNSView can see new document2. But inside Coordiantor's textDidChange has still refrence to document1.

func textDidChange(_ notification: Notification) {
    guard let textView = notification.object as? NSTextView else {
        return
    }

    self.parent.doc.content = textView.string
    self.selectedRanges = textView.selectedRanges
}

So, initially, when i set new bindint, NSTextView changes it content to document2, but as I type, coordinator sends changes to document1.

Is it true, that Coordiantor keeps it's own copy of parent, and even if parent changes (@Binding doc is updated), it still references to old one?

How to make Coordinator reflect parent's bindings changes?

Thank you!

struct Document: Identifiable, Equatable {
    let id: UUID = UUID()
    var name: String
    var content: String
}

struct EditorView: NSViewRepresentable {
    @Binding var doc: Document

    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }

    func makeNSView(context: Context) -> CustomTextView {
        let textView = CustomTextView(
            text: doc.content,
            isEditable: isEditable,
            font: font
        )
        textView.delegate = context.coordinator

        return textView
    }

    func updateNSView(_ view: CustomTextView, context: Context) {
        view.text = doc.content
        view.selectedRanges = context.coordinator.selectedRanges

    }
}


// MARK: - Coordinator

extension EditorView {

    class Coordinator: NSObject, NSTextViewDelegate {
        var parent: EditorView
        var selectedRanges: [NSValue] = []

        init(_ parent: EditorView) {
            self.parent = parent
        }

        func textDidBeginEditing(_ notification: Notification) {
            guard let textView = notification.object as? NSTextView else {
                return
            }

            self.parent.doc.content = textView.string
            self.parent.onEditingChanged()
        }

        func textDidChange(_ notification: Notification) {
            guard let textView = notification.object as? NSTextView else {
                return
            }

            self.parent.doc.content = textView.string
            self.selectedRanges = textView.selectedRanges
        }

        func textDidEndEditing(_ notification: Notification) {
            guard let textView = notification.object as? NSTextView else {
                return
            }

            self.parent.doc.content = textView.string
            self.parent.onCommit()
        }
    }
}

// MARK: - CustomTextView

final class CustomTextView: NSView {
    private var isEditable: Bool
    private var font: NSFont?

    weak var delegate: NSTextViewDelegate?

    var text: String {
        didSet {
            textView.string = text
        }
    }
    // ...
f1nn
  • 6,989
  • 24
  • 69
  • 92

1 Answers1

3

Is it true, that Coordiantor keeps it's own copy of parent, and even if parent changes (@Binding doc is updated), it still references to old one?

A parent, ie EditorView here, is struct, so answer is yes, in general it can be copied.

How to make Coordinator reflect parent's bindings changes?

Instead of (or additional to) parent, inject binding to Coordinator (via constructor) explicitly and work with it directly.

Asperi
  • 228,894
  • 20
  • 464
  • 690
  • Thank you! I'm not quite sure, how to do it property: new to Swift. Could you please take a look, how propetly assign data to bound value? Swift tells me binding is immutable structure, so it's value can't be assigned directly. – f1nn May 31 '20 at 14:16
  • @f1nn, see [this answer](https://stackoverflow.com/a/59509242/12299030) for example of passing binding to coordinator and work with it. – Asperi May 31 '20 at 14:52
  • @Asperi - I find this is not working - I have the same issue and am using the same approach as 'this answer' above. However the binding change is not reflected and the edits in the text field are still being sent to the original object. I have a master detail arrangement and any edits are always sent to the first document that is selected since the Coordinator appears to never change the binding when a new document is selected from the list. – Duncan Groenewald Dec 21 '21 at 22:23
  • @DuncanGroenewald, see example here https://stackoverflow.com/a/63761738/12299030. – Asperi Dec 22 '21 at 05:21
  • @Asperi - thanks but say what !! why are you building the whole stack from scratch in that example ? Is there some issue with using the approach above ? – Duncan Groenewald Dec 22 '21 at 06:11
  • Asperi - on closer inspection I can see it is working for plain text strings but it is not working when using an Attributed String – Duncan Groenewald Dec 22 '21 at 07:06