💾 Archived View for gemini.6px.eu › jimmy › devlog › 2022-02-28-making-search-work.gmi captured on 2022-07-16 at 13:59:16. Gemini links have been rewritten to link to archived content

View Raw

More Information

-=-=-=-=-=-=-

🔎 Making search work

SwiftUI provides the .searchable extension for Views, which look like this:

HStack {
    Text("Text to display")
    .searchable(text: $text)
}

This looks nice, but it will basically do nothing, just show a search button and/or search field in the top toolbar.

Initially, I thought the `text` argument was the text that was displayed and that searching and display of results would occuer automatically. But oh no, you really have to work for your search functionality...

The way to do it is actually to pass a @State variable (in our case text) to the .searchable and then with the following code you can pass it to your model which will do the actual searching:

.onChange(of: text, perform: { newValue in
    textRanges = tab.search(text)
})

This triggers when the text variable changes, and calls the search() function on the tab model.

The search function itself looks for the search string in the textual content and highlights it. The whole content is one single NSAttributedString, so this is fairly easy.

The following snippet of code basically adds a grey background for every range in the string where the search text has been detected.

for range in ranges! {
    content.addAttribute(.backgroundColor, value: NSColor.systemGray.blended(withFraction: 0.5, of: NSColor.textBackgroundColor) ?? NSColor.gray, range: range.nsRange(in: content.string))
}

Next up was to find a way to highlight in green and scroll to the next item when enter was pressed in the search field. Detecting the Enter keypress is as easy as this:

.onSubmit(of: .search) {
    tab.enterSearch()
}

Getting the highlight color to change and move to the next item when pressing enter was also fairly easy.

However, actually getting the textview to scroll to the correct place to show the highlighted snippet was the real challenge. I have a custom NSVIewRepresentable which provides access to all of the NSTextViewDelegate functions. However none of them seemed to do anything when changing the background color of parts of the content.

In the end I ended up with a giant hack which works surprisingly well:

Basically when the highlight color of one of the snippets in changed from the tab model, the updateNSView function from my NSVIewRepresentable subclass is called. And in there, I locate which range has the "selected" highlight color and scroll to it with nsView.scrollRangeToVisible(range)

Here is the whole code for this functionality:

//Find green range and scroll to it
guard let storage = nsView.textStorage else { return }
let wholeRange = NSRange(nsView.string.startIndex..., in: nsView.string)
storage.enumerateAttribute(.backgroundColor, in: wholeRange, options: []) { (value, range, pointee) in
    if let v = value as? NSColor {
        if v == NSColor.green {
            nsView.scrollRangeToVisible(range)
        }
    }
}

And this works perfectly! Hopefully I or someone better at SwiftUI than I am will find a better way to do it, but for now it works.