Search code examples
swiftswiftuiappkitcombine

How to make Swift Coordinator reflect parent's bindings changes?


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
        }
    }
    // ...

Solution

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