Thanks for being a part of WWDC25!

How did we do? We’d love to know your thoughts on this year’s conference. Take the survey here

The NSTextViewDelegate method textViewDidChangeSelection(:) will not fire, while all other text view delegate methods do.

I am trying to implement the NSTextViewDelegate function textViewDidChangeSelection(_ notification: Notification). My text view's delegate is the Coordinator of my NSViewRepresentable. I've found that this delegate function never fires, but any other delegate function that I implement, as long as it doesn't take a Notification as an argument, does fire (e.g., textView(:willChangeSelectionFromCharacterRange:toCharacterRange:), fires and is called on the delegate exactly when it should be).

For context, I've verified all of the below:

  • textView.isSelectable = true
  • textView.isEditable = true
  • textView.delegate === my coordinator
  • I can call textViewDidChangeSelection(:) directly on the delegate without issue.
  • I can select and edit text without issues. I.e., the selections are being set correctly. But the delegate method is never called when they are.

I am able to add the intended delegate as an observer for the selector textViewDidChangeSelection via NotificationCenter. If I do this, the function executes when it should, but fires for every text view in my view hierarchy, which can number in the hundreds. I'm using an NSLayoutManager, so I figure this should only fire once. I've added a check within my code:

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

    // Any code I want to execute...
}

But the above guard check lets through every notification, so, no matter what, my closure executes hundreds of times if I have hundreds of text views, all of them being sent by textView === layoutManager.firstTextView, but once for each and every text view managed by that layoutManager.

Does anyone know why this method isn't ever called on the delegate, while seemingly all other delegate methods are? I could go the NotificationCenter route, but I'd love to know why this won't execute as a delegate method when documentation says that it should, and I don't want to have to implement a counter to make sure my code only executes once per selection update. And for more reasons than that, implementing via delegate method is preferable to using notifications for my use case.

Thanks for any help!

Please ignore the bit about repeat notifications from NotificationCenter: they were due to mistakenly adding the Coordinator as an observer each time a text view was created. I didn't realize that each addObserver call is treated as unique and will result in one notification being sent per addObserver call.

firstTextView is the recipient of various NSText and NSTextView notifications. You'd want to compare against the NSTextView instance stored in the Coordinator. For example:

struct NSTextViewRepresentable : NSViewRepresentable {
    @Binding var text: String
    let onSelectionChange: () -> Void

    func makeCoordinator() -> Coordinator {
        Coordinator(text: $text, onSelectionChange: onSelectionChange)
    }

    func makeNSView(context: Context) -> NSTextView {
        let textView = NSTextView()
        textView.isEditable = true
        textView.isSelectable = true
        textView.isRichText = false
        textView.delegate = context.coordinator
        textView.string = text
        context.coordinator.textView = textView
        return textView
    }

    func updateNSView(_ nsView: NSTextView, context: Context) {
        if let textView = context.coordinator.textView, textView.string != text {
            textView.string = text
        }
    }

    class Coordinator: NSObject, NSTextViewDelegate {
        @Binding var text: String
        var textView: NSTextView?
        let onSelectionChange: () -> Void

        init(text: Binding<String>, onSelectionChange: @escaping () -> Void) {
            _text = text
            self.onSelectionChange = onSelectionChange
        }

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

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

            onSelectionChange()
        }
    }
}

Thank you for responding. The firstTextView is the only text view that is ever passed as the notification.object when a series of text views are managed by an NSLayoutManager, so checking against layoutManager.firstTextView or coordinator.textViews.first or coordinator.textViews[0] are functionally equivalent for me -- whichever I use, the check works in my code and isn't my issue. I shouldn't have muddied my post by talking about the multiple notifications, sorry for the confusion.

My primary issue is that the delegate function is never called. I can get textViewSelectionDidChange(:) to fire if I add my coordinator as an observer to the notification. But I would like to understand why it won't fire at all via a delegate call when I set my coordinator as the view's delegate.

Thanks for any further help with this!

The NSTextViewDelegate method textViewDidChangeSelection(:) will not fire, while all other text view delegate methods do.
 
 
Q