A spotlight-inspired quick action bar for macOS.

Overview

DSFQuickActionBar

A spotlight-inspired quick action bar for macOS.

Swift Package Manager

Swift Package Manager Swift Package Manager

Why?

I've seen this in other mac applications (particularly spotlight) and it's very useful. I have a project where it needs to allow the user to quickly access a large set of actions and hey presto!

Integration

Swift package manager

Add https://github.com/dagronf/DSFQuickActionBar to your project.

Features

  • macOS AppKit Swift Support
  • macOS AppKit SwiftUI Support

You can present a quick action bar in the context of a window (where it will be centered above and within the bounds of the window as is shown in the image above) or centered in the current screen (like Spotlight currently does).

Name Type Description
placeholderText String The placeholder text to display in the edit field
searchImage NSImage The image to display on the left of the search edit field, or nil for no image
initialSearchText String (optional) Provide an initial search string to appear when the bar displays
width CGFloat (optional) Force the width of the action bar

NOTE: Due to limitations of my understanding of integrating SwiftUI into macOS, the SwiftUI implementation doesn't support placing a quick action bar in front of a specific window. It (currently) only supports a global quick action bar (like Spotlight).

Process

  1. Present the quick action bar, automatically focussing on the edit field so your hands can stay on the keyboard
  2. User starts typing in the search field
  3. For each change to the search term -
    1. The contentSource will be asked for the identifiers that 'match' the search term (itemsForSearchTerm)
    2. For each item, the contentSource will be asked to provide a view which will appear in the result table for that identifier (viewForIdentifier)
    3. When the user either double-clicks on, or presses the return key on a selected identifier row, the contentSource will be provided with the identifier (didSelectIdentifier)
  4. The quick action bar will automatically dismiss if
    1. The user clicks outside the quick action bar (ie. it loses focus)
    2. The user presses the escape key
    3. The user double-clicks an item in the result table
    4. The user selects a row and presses 'return'

Implementing

The contentSource (DSFQuickActionBarContentSource/DSFQuickActionBarSwiftUIContentSource) provides the content and feedback for the quick action bar. The basic mechanism is similar to NSTableViewDataSource/NSTableViewDelegate in that the control will :-

  1. query the contentSource for items matching a search term (itemsForSearchTerm)
  2. ask the contentSource for a view to display each item (viewForIdentifier)
  3. indicate that the user has pressed/clicked a selection in the results.
  4. (optional) indicate to the contentSource that the quick action bar has been dismissed.

identifiersForSearchTerm

Returns an array of the unique identifiers for items that match the search term. The definition of 'match' is entirely up to you - you can perform any check you want

// Swift
func quickActionBar(_ quickActionBar: DSFQuickActionBar, identifiersForSearchTerm searchTerm: String) -> [DSFQuickActionBar.ItemIdentifier]

// SwiftUI
func identifiersForSearch(_ term: String) -> [DSFQuickActionBar.ItemIdentifier]

viewForIdentifier

Return the view to be displayed in the row for the item that matches the identifier. The search term is also provided to allow the view to be customized for the search term (eg. highlighting the match in the name)

// Swift
func quickActionBar(_ quickActionBar: DSFQuickActionBar, viewForIdentifier identifier: DSFQuickActionBar.ItemIdentifier, searchTerm: String) -> NSView?

// SwiftUI
func viewForIdentifier<RowContent: View>(_ identifier: DSFQuickActionBar.ItemIdentifier, searchTerm: String) -> RowContent?

didSelectIdentifier

Indicates the user activated an item in the result list.

// Swift
func quickActionBar(_ quickActionBar: DSFQuickActionBar, didSelectIdentifier identifier: DSFQuickActionBar.ItemIdentifier)

// SwiftUI
func didSelectIdentifier(_ identifier: DSFQuickActionBar.ItemIdentifier)

Examples

Swift Example

Swift Example

A simple AppKit example using Core Image Filters as the contentSource.

[DSFQuickActionBar.ItemIdentifier] { return self.actions .filter { $0.userPresenting.localizedCaseInsensitiveContains(term) } .sorted(by: { a, b in a.userPresenting < b.userPresenting }) .map { $0.identifier } } // Get the row's view for the action with the specified identifier func quickActionBar(_: DSFQuickActionBar, viewForIdentifier identifier: DSFQuickActionBar.ItemIdentifier, searchTerm: String) -> NSView? { // Find the item with the specified item identifier guard let filter = self.actions.filter({ $0.identifier == identifier }).first else { fatalError() } return FilterRowView(filter) // FilterRowView() is a NSView-derived class } // Perform a task with the selected action func quickActionBar(_: DSFQuickActionBar, didSelectIdentifier identifier: DSFQuickActionBar.ItemIdentifier) { guard let filter = self.actions.filter({ $0.identifier == identifier }).first else { fatalError() } self.performAction(filter) // Do something with the selected filter } } ">
class QuickActions: DSFQuickActionBarContentSource {

   /// DATA

   struct Filter {
      let identifier = DSFQuickActionBar.ItemIdentifier()
      let name: String
      var userPresenting: String { return CIFilter.localizedName(forFilterName: self.name) ?? self.name }
      var description: String { return CIFilter.localizedDescription(forFilterName: self.name) ?? "" }
   }

   let AllFilters: [Filter] = {
      let filterNames = CIFilter.filterNames(inCategory: nil).sorted()
      return filterNames.map { name in Filter(name: name) }
   }()

   /// ACTIONS

   let quickActionBar = DSFQuickActionBar()

   func displayQuickActionBar() {
      self.quickActionBar.contentSource = self
      self.quickActionBar.presentOnMainScreen(
         placeholderText: "Quick Actions",
         width: 600
      )
   }
   
   /// CALLBACKS

   // Get all the identifiers for the actions that 'match' the term
   func quickActionBar(_: DSFQuickActionBar, identifiersForSearchTerm searchTerm: String) -> [DSFQuickActionBar.ItemIdentifier] {
      return self.actions
         .filter { $0.userPresenting.localizedCaseInsensitiveContains(term) }
         .sorted(by: { a, b in a.userPresenting < b.userPresenting })
         .map { $0.identifier }
   }

   // Get the row's view for the action with the specified identifier
   func quickActionBar(_: DSFQuickActionBar, viewForIdentifier identifier: DSFQuickActionBar.ItemIdentifier, searchTerm: String) -> NSView? {
      // Find the item with the specified item identifier
      guard let filter = self.actions.filter({ $0.identifier == identifier }).first else {
         fatalError()
      }
      return FilterRowView(filter)  // FilterRowView() is a NSView-derived class
   }

   // Perform a task with the selected action
   func quickActionBar(_: DSFQuickActionBar, didSelectIdentifier identifier: DSFQuickActionBar.ItemIdentifier) {
      guard let filter = self.actions.filter({ $0.identifier == identifier }).first else {
         fatalError()
      }
      self.performAction(filter) // Do something with the selected filter
   }
}
SwiftUI Example

SwiftUI Example

A simple macOS SwiftUI example using Core Image Filters as the contentSource.

Data

struct Filter {
   let identifier = DSFQuickActionBar.ItemIdentifier()
   let name: String
   var userPresenting: String { return CIFilter.localizedName(forFilterName: self.name) ?? self.name }
   var description: String { return CIFilter.localizedDescription(forFilterName: self.name) ?? "" }
}

let AllFilters: [Filter] = {
   let filterNames = CIFilter.filterNames(inCategory: nil).sorted()
   return filterNames.map { name in Filter(name: name) }
}()

SwiftUI View

struct ContentView: View {

   // Binding to update when the user selects a filter
   @State var selectedFilter: Filter?

   // A quick action bar that uses FilterViewCell to display each row in the results
   let quickActionBar = DSFQuickActionBar.SwiftUI<FilterRowCell>()

   var body: some View {
      Button("Show Quick Action Bar") {
         self.quickActionBar.present(
            placeholderText: "Search Core Image Filters",
            contentSource: CoreImageFiltersContentSource(selectedFilter: $selectedFilter)
         )
      }
   }
}

Content Source Definition

class CoreImageFiltersContentSource: DSFQuickActionBarSwiftUIContentSource {

   @Binding var selectedFilter: Filter?

   init(selectedFilter: Binding
    ?>) {
      
    self.
    _selectedFilter 
    = selectedFilter
   }

   
    func 
    identifiersForSearch(
    _ 
    searchTerm: 
    String) 
    -> [DSFQuickActionBar.ItemIdentifier] {
      
    if term.
    isEmpty { 
    return [] }
      
    return AllFilters
         .
    filter { 
    $0.
    userPresenting.
    localizedCaseInsensitiveContains(term) }
         .
    sorted(
    by: { a, b 
    in a.
    userPresenting 
    < b.
    userPresenting } )
         .
    prefix(
    100)
         .
    map { 
    $0.
    id }
   }

   
    func 
    viewForIdentifier<
    RowContent>(
    _ 
    identifier: DSFQuickActionBar.ItemIdentifier, 
    searchTerm: 
    String) 
    -> RowContent
    ? 
    where RowContent
    : 
    View {
      
    guard 
    let filter 
    = AllFilters.
    filter({ 
    $0.
    id 
    == identifier }).
    first 
    else {
         
    return 
    nil
      }
      
    return 
    FilterViewCell(
    filter: filter) 
    as? RowContent
   }

   
    func 
    didSelectIdentifier(
    _ 
    identifier: DSFQuickActionBar.ItemIdentifier) {
      
    guard 
    let filter 
    = AllFilters.
    filter({ 
    $0.
    id 
    == identifier }).
    first 
    else {
         
    return
      }
      selectedFilter 
    = filter
   }

   
    func 
    didCancel() {
      selectedFilter 
    = 
    nil
   }
}
   

Filter Row View

struct FilterViewCell: View {
   let filter: Filter
   var body: some View {
      HStack {
         Image("filter-color").resizable()
            .frame(width: 42, height: 42)
         VStack(alignment: .leading) {
            Text(filter.userPresenting).font(.title)
            Text(filter.description).font(.callout).foregroundColor(.gray).italic()
         }
      }
   }
}

Screenshots


Releases

2.0.2

  • Updated demo for updated DSFAppKitBuilder

2.0.1

  • Updated demo for updated DSFAppKitBuilder

2.0.0

Note the delegate API has changed for this version, hence moving to 2.0.0 to avoid automatic breakages

  • Changed viewForIdentifier delegate method to also pass the current search term.
  • Changed the code to use searchTerm (instead of term) consistently throughout the library

1.1.1

  • Fixed silly runtime error for dynamic rows

1.1.0

  • Changed the demo apps data from using 'Mountains' to using Core Image Filter definitions.

1.0.0

  • Added initial SwiftUI support
  • Changed 'delegate' to 'contentSource'

0.5.1

  • Fixed bugs in documentation

0.5.0

  • Initial release

License

MIT. Use it and abuse it for anything you want, just attribute my work. Let me know if you do use it somewhere, I'd love to hear about it!

MIT License

Copyright (c) 2021 Darren Ford

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
You might also like...
A message bar for iOS written in Swift.
A message bar for iOS written in Swift.

Dodo, a message bar for iOS / Swift This is a UI widget for showing text messages in iOS apps. It is useful for showing short messages to the user, so

πŸ“Š A customizable gradient progress bar (UIProgressView).
πŸ“Š A customizable gradient progress bar (UIProgressView).

GradientProgressBar A customizable gradient progress bar (UIProgressView). Inspired by iOS 7 Progress Bar from Codepen. Example To run the example pro

A library, which adds the ability to hide navigation bar when view controller is pushed via hidesNavigationBarWhenPushed flag
A library, which adds the ability to hide navigation bar when view controller is pushed via hidesNavigationBarWhenPushed flag

HidesNavigationBarWhenPushed A library, which adds the ability to hide navigation bar when view controller is pushed via hidesNavigationBarWhenPushed

πŸ’ˆ Retro looking progress bar straight from the 90s
πŸ’ˆ Retro looking progress bar straight from the 90s

Description Do you miss the 90s? We know you do. Dial-up internet, flickering screens, brightly colored websites and, of course, this annoyingly slow

An easy to use UI component to help display a signal bar with an added customizable fill animation
An easy to use UI component to help display a signal bar with an added customizable fill animation

TZSignalStrengthView for iOS Introduction TZSignalStrengthView is an easy to use UI component to help display a signal bar with an added customizable

Infinite paging controller, scrolling through contents and title bar scrolls with a delay
Infinite paging controller, scrolling through contents and title bar scrolls with a delay

PageController PageController is infinite paging controller, scrolling through contents and title bar scrolls with a delay. Then it provide user inter

A dynamically flowing progress bar.
A dynamically flowing progress bar.

WWProgressView A dynamically flowing progress bar. δΈ€ε€‹ε‹•ζ…‹ζ΅ε‹•ηš„ι€²εΊ¦ζ’. Installation with Swift Package Manager dependencies: [ .package(url: "https://gith

Bar Button Item that can be moved anywhere in the screen, like Android's stickers button.
Bar Button Item that can be moved anywhere in the screen, like Android's stickers button.

FlowBarButtonItem Bar Button Item that can be moved anywhere in the screen, like Android's stickers button. [![CI Status](http://img.shields.io/travis

Super awesome Swift minion for Core Data (iOS, macOS, tvOS)

⚠️ Since this repository is going to be archived soon, I suggest migrating to NSPersistentContainer instead (available since iOS 10). For other conven

Comments
  • quickActionBarDidCancel not being called when user clicks outside the bar

    quickActionBarDidCancel not being called when user clicks outside the bar

    Hello, I hope you're well! I'm having an issue whereby after clicking outside the search bar, it stops showing up on subsequent calls, even though isPresenting is logging the correct values.

    In other words, quickActionBarDidCancel is called when the user pressed esc but not when clicking outside the bar. Currently being called as so:

    self.quickActionBar.present(placeholderText: "Search...", width: self.screenWidth)
    

    Otherwise when presenting the bar with the completion handler, the statement is logged, but the bar stop showing up on subsequent calls to present.

    self.quickActionBar.present(placeholderText: "Search...", width: self.screenWidth) {
    			Swift.print("Quick action bar closed")
    }
    

    Any help would be greatly appreciated!

    opened by cyrilzakka 6
  • How do I present a search bar after pressing a status menu bar icon?

    How do I present a search bar after pressing a status menu bar icon?

    I'm trying to recreate a Siri-like app where clicking a system menu bar item shows the quick action bar in the center of the screen but I can't get anything. Would you mind helping me out?

    Here's what I have so far:

    import Cocoa
    import DSFQuickActionBar
    
    class AppDelegate: NSObject, NSApplicationDelegate {
        
        private var statusItem: NSStatusItem!
        
        // Search bar
        lazy var quickActionBar: DSFQuickActionBar = {
            let b = DSFQuickActionBar()
            b.contentSource = self
            b.rowHeight = 48
            return b
        }()
    
        func applicationDidFinishLaunching(_ aNotification: Notification) {
            // Status item
            statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.squareLength)
            if let button = statusItem.button {
                button.image = NSImage(systemSymbolName: "books.vertical", accessibilityDescription: "books.vertical")
                button.action = #selector(toggleSearch(_:))
            }
        }
        
        @objc func toggleSearch(_ sender: Any?) {
            if quickActionBar.isPresenting {
                // Hide
                quickActionBar.cancel()
            } else {
                // Show
                self.quickActionBar.present(
                    parentWindow: nil,
                    placeholderText: "Search Filters",
                    searchImage: NSImage(systemSymbolName: "books.vertical", accessibilityDescription: "books.vertical"),
                    width: min(800, max(500, 200 + 50))
                ) {
                    Swift.print("Quick action bar closed")
                }
            }
        }
    }
    
    extension AppDelegate: DSFQuickActionBarContentSource {
    
        func quickActionBar(_: DSFQuickActionBar, itemsForSearchTerm searchTerm: String) -> [AnyHashable] {
            return ["1", "2"]
        }
        
        func quickActionBar(_ quickActionBar: DSFQuickActionBar, didActivateItem item: AnyHashable) {
        }
    
        func quickActionBar(_: DSFQuickActionBar, viewForItem item: AnyHashable, searchTerm: String) -> NSView? {
            return nil
        }
    
        func quickActionBar(_ quickActionBar: DSFQuickActionBar, canSelectItem item: AnyHashable) -> Bool {
            return true
        }
    
        func quickActionBarDidCancel(_: DSFQuickActionBar) {
            
        }
    
        @objc func performAdvancedSearch(_ sender: Any) {
            quickActionBar.cancel()
        }
    }
    
    opened by cyrilzakka 3
  • didActivateItem missing from SwiftUI implementation

    didActivateItem missing from SwiftUI implementation

    I might be missing something obvious, but I can't find how to use didActivateItem, didCancel etc. when following the Swift UI implementation.

    How do I tell DSFQuickActionBar what to do when an item is activated?

    opened by thomasferiero 2
Owner
Darren Ford
Darren Ford
Highly customizable Action Sheet Controller with Assets Preview written in Swift

PPAssetsActionController Play with me ▢️ ?? If you want to play with me, just tap here and enjoy! ?? ?? Show me ?? Try me ?? The easiest way to try me

Pavel Pantus 72 Feb 4, 2022
A replacement of default action sheet, but has very simple usage

KPActionSheet A replacement of default action sheet, but has very simple usage. Todo Add more custom affects and styles. Installation CocoaPods KPActi

Kenny 7 Jun 27, 2022
SpriteKit Floating Bubble Picker (inspired by Apple Music) 🧲

Magnetic Magnetic is a customizable bubble picker like the Apple Music genre selection. Demo Video $ pod try Magnetic Features Adding/Removing Nodes

Lasha Efremidze 1.4k Jan 6, 2023
Iridescent Effect View (inspired by Apple Pay Cash) ✨

Shiny Shiny is an iOS library that generates an iridescent effect view matched to the gyroscope, similar to the Apple Pay Cash card in the Wallet app.

Lasha Efremidze 768 Dec 2, 2022
Simple PhotoBrowser/Viewer inspired by facebook, twitter photo browsers written by swift

SKPhotoBrowser Simple PhotoBrowser/Viewer inspired by facebook, twitter photo browsers written by swift features Display one or more images by providi

keishi suzuki 2.4k Jan 6, 2023
A modern HUD inspired by Apple Music and Apple Podcasts

HUD A modern HUD inspired by Apple Music and Apple Podcasts. Appearance Light Dark HUD Activity Indicator HUD Requirements iOS 13+ Installation You ca

Bei Li 30 Nov 18, 2022
A micro UIStackView convenience API inspired by SwiftUI

Stacks A micro UIStackView convenience API inspired by SwiftUI. let stack: UIView = .hStack(alignment: .center, margins: .all(16), [ .vStack(spaci

Alexander Grebenyuk 74 Jul 27, 2022
SwiftUI: Components Library Inspired by Twitter's Bootstrap

bootswiftui SwiftUI: Components Library Inspired by Twitter's Bootstrap Warning This is just SwiftUI exercise. Please do not consider using this repo

Robert Sandru 1 Oct 27, 2022
Bubble pickers, inspired by Apple

AmazingBubbles Bubble pickers, inspired by Apple Requirements: iOS 9.1+ XCode 8.0+ Swift 3.0 Installation AmazingBubbles Library is available through

Gleb Radchenko 63 Oct 14, 2022
MUDownloadButton - a Progressive Download button written in pure swift and inspired by AppStore download button

MUDownloadButton is a Progressive Download button written in pure swift and inspired by AppStore download button . feel free to contribute and pull requests

Mohammad ShahibZadeh 2 Feb 20, 2022