8

I've seen some Catalyst apps add a search bar to the NSToolbar and was wondering how I could do the same. Would I have to import AppKit/Cocoa to get an NSSearchField and the actual NSToolbar? Or is there some way with UISearchBars that I am just not seeing? Any help would be great, thanks.

I have tried importing AppKit and Cocoa (Using a bundle loader), but I don't think I had done it right. If anyone has a solid guide on how to do this, please link it.

I also tried creating a UIBarButtonItem with a custom view of a UISearchBar, which doesn't end up rendering anything. (that can be found below)

This is found in a switch case on itemIdentifier.rawValue (in the toolbar itemForIdentifier function)

let search = UISearchBar()
search.sizeToFit()
let button = UIBarButtonItem(customView: search)
button.width = 100
let toolbarItem = NSToolbarItem(itemIdentifier: itemIdentifier, barButtonItem: button)
toolbarItem.isBordered = true
toolbarItem.isEnabled = true
return toolbarItem
rmaddy
  • 314,917
  • 42
  • 532
  • 579

3 Answers3

5

Thanks to mmackh, the search bar can be added in just as easily as any other toolbar item. Thank all of you who looked into this.

https://github.com/mmackh/Catalyst-Helpers


Swift Example

Once you add the files to your project (and in this case your bridging header), just return this as you would any other toolbar item.

let item = NSToolbarItem_Catalyst.searchItem(withItemIdentifier: "searchBar", textDidChangeHandler: { string in
    print(string)
})
  • I can't seam to get this to work. How do I import the files? If I drop the two files related to toolbar helper into my project, I get a lot of errors in the files. – Kurt L. Jul 25 '22 at 22:52
  • The files in Catalyst-Helpers repository do not compile. This is not a current solution anymore. – deniz Aug 27 '23 at 03:05
1

I found a soulution that works for me, maybe it will be good enough for you too.

While I can't create a search bar in toolbar, I can create the button that looks pretty much as a search bar. That button then toggles the visibility of search bar in Catalyst application.

Here's the (obj-c) code for it:

    UIImageSymbolConfiguration *conf = [UIImageSymbolConfiguration configurationWithPointSize:32 weight:UIImageSymbolWeightRegular];
    UIImage *backImage = [UIImage systemImageNamed:@"magnifyingglass" withConfiguration:conf];
    UIBarButtonItem *buttonItem = [[UIBarButtonItem alloc] initWithImage:backImage style:UIBarButtonItemStylePlain target:self action:@selector(searchButtonAction)];
    NSToolbarItem *item = [NSToolbarItem itemWithItemIdentifier:itemIdentifier barButtonItem:buttonItem];
    item.title = @"Search";
    return item;

And here are the images of how this looks in app: iOS search bar off iOS search bar on

As a bonus, you could use some non trimmable space character (like this one) to add space after "Search" string, making the button look even more like a proper search bar.

Here's how that looks:

final searchbar

duke4e
  • 31
  • 1
  • 4
  • Could you please share the setup code for positioning the elements up there on the toolbar? I'm facing a similar issue as OP, and I'd like to try your approach, but I don't even know how to keep the "group" in the center and set a button on the right side. – Xavi Moll Dec 29 '19 at 20:13
  • I would suggest to check out this tutorial - https://appventure.me/guides/catalyst/complete_book.html Most of the catalyst related stuff is explained there. – duke4e Jan 14 '20 at 19:08
  • Thanks for the link, after fighting a bit I've managed to get it there! – Xavi Moll Feb 12 '20 at 09:37
  • @duke4e how do you create a toolbar with picker like "Sort alphabatically"? – sarunw Aug 26 '20 at 17:33
  • 1
    @sarunw `NSToolbarItemGroup *group = [NSToolbarItemGroup groupWithItemIdentifier:itemIdentifier titles:@[@"Sort Alphabetically", @"Sort By Story Count"] selectionMode:NSToolbarItemGroupSelectionModeSelectOne labels:@[@"section1", @"section2"] target:self action:@selector(sortSelectionChanged:)]; group.controlRepresentation = NSPickerTouchBarItemControlRepresentationCollapsed; group.selectedIndex = 1;` – duke4e Aug 27 '20 at 21:18
  • @duke4e Thanks! That's works perfectly! Do you have any resources or documents about all the possibilities of NSToolbarItem in Catalyst? – sarunw Aug 28 '20 at 14:39
  • @sarunw Well, read the documentation or inspect the source code. Also check out the appventure link I posted few coments above, and browse through github projects by searching for relevant command (in this case NSToolbarItem). Also, trial and error. Sorry, but I don't have better solution. – duke4e Aug 28 '20 at 22:02
0

You have 2 options. If your catalyst app is targeting minimum macOS 13 or later, you can simply embed UISearchBar in NSToolbar using this method NSToolbarItem.init(itemIdentifier:barButtonItem:). If the first option is not feasible for your application or you don't like how UISearchBar is presented in your toolbar then you can embed a NSSearchBar from AppKit. However this option is a bit more involved so I will write the summary.

  1. Add a new macOS framework to your XCode project as a new target. Give a name to the framework - let's say macOSBridging. This name will be important later on. Make sure this new target is linked to your primary app/target correctly.

  2. In your macOS framework subclass NSSearchFieldCell as shown below:

    import AppKit
    
    class ToolbarSearchFieldCell: NSSearchFieldCell {
        //customize the NSSearchFieldCell according to your needs
    }
    
  3. In a separate file subclass NSSearchField in your macOS target.

     import AppKit
    
     class ToolbarSearchField: NSSearchField {
         //customize NSSearchField according to your needs
     }
    
  4. Again subclass NSToolbarItem in your macOS framework.

         import AppKit
    
         class SearchbarToolItem: NSToolbarItem, NSSearchFieldDelegate {
         private let searchField = ToolbarSearchField()
    
         override init(itemIdentifier: NSToolbarItem.Identifier) {
             super.init(itemIdentifier: itemIdentifier)
    
             //customize searchField according to your needs
    
             self.view = searchField
    
             visibilityPriority = .high
         }
    
         func controlTextDidChange(_ obj: Notification) {
             //according to Apple Docs, we can retrieve the text value from the userInfo dictionary in Notification object
             if let attachedTextView = obj.userInfo?["NSFieldEditor"] as? NSTextView {
                 //using notification center, we can relay this message back to the ViewController
                 NotificationCenter.default.post(name: "search-bar-text", object: attachedTextView.string)
             }
         }
     }
    
  5. Now head on over to your iOS target (your app) and in your toolbar delegate when it is time to insert a toolbar item, add this code.

     func toolbar(_ toolbar: NSToolbar, itemForItemIdentifier itemIdentifier: NSToolbarItem.Identifier, willBeInsertedIntoToolbar flag: Bool) -> NSToolbarItem? {
       var toolbarItem: NSToolbarItem?
       switch itemIdentifier {
         case .customSearchBar: //assumes that you have NSToolbarItem.Identifier.customSearchBar defined earlier  
    
             if let frameworksPath = Bundle.main.privateFrameworksPath {
                 let bundlePath = "\(frameworksPath)/macOSBridging.framework" //or whatever the name you picket in Step 1.
                 do {
                     //we want to check whether we can load this dependent framework
                     //make sure the name of the framework is correct
                     try Bundle(path: bundlePath)?.loadAndReturnError()
                     _ = Bundle(path: bundlePath)! //at this point, we can load this!
    
                     //we have to use some Objective-C trickery to load the SearchbarToolItem from AppKit
                     if let searchbarToolItemClass = NSClassFromString("macOSBridging.SearchbarToolItem") as? NSToolbarItem.Type {
                         let newItemToAdd = searchbarToolItemClass.init(itemIdentifier: itemIdentifier)
                         toolbarItem = newItemToAdd
                         print("Successfully loaded NSSearchBar into our toolbar!!!")
                     } else {
                         print("There is no class with the name SearchbarToolItem in macOSBridging.framework - make sure you spelled the name correctly")
                     }
                 } catch {
                     print("error while loading the dependent framework: \(error.localizedDescription)")
                 }
             }
         ...
         ... //configure other items in your toolbar
         ...
         return toolbarItem
     }
    
  6. Now in your view controller listen for the notifications from NSSearchBar like so:

     override func viewDidLoad() {
         super.viewDidLoad()
         // Do any additional setup after loading the view.
         ...
         ...
         NotificationCenter.default.addObserver(self, selector: #selector(userPerformedSearch(_:)), name: NSNotification.Name(rawValue: "search-bar-text"), object: nil)
         //removing observers are optional now.
         //see https://developer.apple.com/documentation/foundation/notificationcenter/1415360-addobserver#discussion
     }
    
     func userPerformedSearch(_ notification: NSNotification) {
         //NSNotification.object contains the typed text in NSSearchBar
         if let searchedText = notification.object as? String {
             //do something with the search text
         }
     }
    

Result with NSSearchBar in the toolbar:

working example of NSSearchBar in a sample project

If you want to see a working code sample I created an example project on Github. If you have trouble linking the framework to your app, additional guidance is available here.

deniz
  • 725
  • 10
  • 13