NSTextView: When Did the Find Bar Disappear?

For whatever reason, my current app project’s find bar does not make the text view firstResponder again when you hit Escape or click the “Done” button to close it. This is very uncomfortable for users: they type away, hit ⌘F to find a phrase, then hit Esc – and now they’re in limbo.

To my astonishment, the NSTextFinderAction called hideFindInterface is not triggered when you make the find bar disappear. Its opposite, showFindInterface, is triggered when the find bar slides back in, though. Intercepting in NSTextView.performTextFinderAction(_:) does not help, then.

I embed my text view in a NSScrollView, which conforms to NSTextFinderBarContainer. There, you have a mutable (!) isFindBarVisible property. Since it’s mutable, you can write a Swift property observer in a subclass:

class MyScrollView: NSScrollView {
    override var isFindBarVisible: Bool {
        didSet {
            if oldValue == false && isFindBarVisible == true {
                // find bar is closed
            }
        }
    }
}

You can also use key–value-observation for this property if you want to avoid the subclass.

I am also using RxSwift in this project, so writing a KVO property observer is as “simple” as this:

class AmazingViewController: NSViewController {

    @IBOutlet var textView: NSTextView!
    @IBOutlet var scrollView: NSScrollView!
    
    fileprivate let disposeBag = DisposeBag()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        wireFindBarDisappearanceToCursorFocus()
    }
    
    fileprivate func wireFindBarDisappearanceToCursorFocus() {

        scrollView.rx.observe(Bool.self, "findBarVisible")
            .filterNil()  // from RxOptional
            .skip(1)      // Don't need the initial value
            .map { !$0 }  // So I don't have to write `!didBecomeVisible`
            .distinctUntilChanged()
            .asDriver(onErrorJustReturn: false)
            .drive(onNext: { [weak self] didDisappear in
                guard didDisappear else { return }
                self?.textView.makeFirstResponder()
            })
            .disposed(by: disposeBag)
    }
}

// I was tired of unwrapping all the view?.window?.makeFirstResponder(view) calls:
extension NSView {
    func makeFirstResponder() {
        self.window?.makeFirstResponder(self)
    }
}

This helps. But I don’t trust all this NSTextFinder related stuff, I admit. It’s a bit too finicky for my taste, overall.