I’ve seen this in other mac applications (particularly Spotlight and Boop) and it’s very useful and convenient.
Features
macOS AppKit Swift Support
macOS AppKit SwiftUI Support
Completely keyboard navigable
Optional keyboard shortcuts
Asynchronous API to avoid beachballing on complex queries.
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).
Demos
You can find macOS demo apps in the Demos subfolder.
Simple Demo - a simple AppKit application demonstrating a synchronous quick action bar using AppKit, SwiftUI and custom cell types
Doco Demo - AppKit demo used for generating images for the website
Faux Spotlight - An AppKit demo showing asynchronous searching support using MDItemQuery()
SwiftUI Demo - A SwiftUI demonstration
StatusBar Item Demo - Demonstrates displaying a quick action bar from a statusbar item (in the menu).
Process
Present the quick action bar, automatically focussing on the edit field so your hands can stay on the keyboard
User starts typing in the search field
For each change to the search term -
The contentSource will be asked for the item(s) that ‘match’ the search term (itemsForSearchTerm). The items request is asynchronous, and can be completed at any point in the future (as long as it hasn’t been cancelled by another search request)
For each item, the contentSource will be asked to provide a view which will appear in the result table for that item (viewForItem)
When the user either double-clicks on, or presses the return key on a selected item row, the contentSource will be provided with the item (didActivateItem)
The quick action bar will automatically dismiss if
The user clicks outside the quick action bar (ie. it loses focus)
The user presses the escape key
The user double-clicks an item in the result table
The user selects a row and presses ‘return’
Implementing for AppKit
You present a quick action bar by :-
creating an instance of DSFQuickActionBar
set the content source on the instance
call the present method.
Presenting
Call the present method on the quick action bar instance.
Name
Type
Description
parentWindow
NSWindow
The window to present the quick action bar over, or nil to display for the current screen (ala Finder Spotlight)
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. If nil, uses the default magnifying glass image
initialSearchText
String
Provide an initial search string to appear when the bar displays
width
CGFloat
Force the width of the action bar
showKeyboardShortcuts
Bool
Display keyboard shortcuts (↩︎, ⌘1 -> ⌘9) for the first 10 selectable items
didClose
callback
Called when the quick action bar closes
Content Source
The contentSource (DSFQuickActionBarContentSource) provides the content and feedback for the quick action bar. The basic mechanism is similar to NSTableViewDataSource/NSTableViewDelegate in that the control will :-
query the contentSource for items matching a search term (itemsForSearchTerm)
ask the contentSource for a view for each displayed item (viewForItem)
indicate that the user has pressed/clicked a selection in the results.
(optional) indicate to the contentSource that the quick action bar has been dismissed.
Called when the control needs a array of items to display within the control that match a search term.
The definition of ‘match’ is entirely up to you - you can perform any check you want.
The task object contains the search term and a completion block to call when the search results become
available. If the search text changes during an asynchronous search call the task is marked as invalid and the
result will be ignored.
Simple synchronous example
If you have code using the old synchronous API, it’s relatively straightforward to convert your existing code
to the new api.
Return the view to be displayed in the row for the item. The search term is also provided to allow the view to be customized for the search term (eg. highlighting the match in the name)
Called if the user cancels the quick action bar (eg. by hitting the esc key or clicking outside the bar)
Swift Example
Swift Example
A simple AppKit example using Core Image Filters as the contentSource.
class ViewController: NSViewController {
let quickActionBar = DSFQuickActionBar()
override func viewDidLoad() {
super.viewDidLoad()
// Set the content source for the quick action bar
quickActionBar.contentSource = self
}
@IBAction func selectFilter(_ sender: Any) {
// Present the quick action bar
quickActionBar.present(placeholderText: "Search for filters…")
}
}
// ContentSource delegate calls
extension ViewController: DSFQuickActionBarContentSource {
func quickActionBar(_ quickActionBar: DSFQuickActionBar, itemsForSearchTerm searchTerm: String) -> [AnyHashable] {
return Filter.search(searchTerm)
}
func quickActionBar(_ quickActionBar: DSFQuickActionBar, viewForItem item: AnyHashable, searchTerm: String) -> NSView? {
guard let filter = item as? Filter else { fatalError() }
// For the demo, just return a simple text field with the filter's name
return NSTextField(labelWithString: filter.userPresenting)
}
func quickActionBar(_ quickActionBar: DSFQuickActionBar, didActivateItem item: AnyHashable) {
Swift.print("Activated item \(item as? Filter)")
}
func quickActionBarDidCancel(_ quickActionBar: DSFQuickActionBar) {
Swift.print("Cancelled!")
}
}
// the datasource for the Quick action bar. Each filter represents a CIFilter
struct Filter: Hashable, CustomStringConvertible {
let name: String // The name is unique within our dataset, thus the default equality will be enough to uniquely identify
var userPresenting: String { return CIFilter.localizedName(forFilterName: self.name) ?? self.name }
var description: String { name }
// All of the available filters
static var AllFilters: [Filter] = {
let filterNames = CIFilter.filterNames(inCategory: nil).sorted()
return filterNames.map { name in Filter(name: name) }
}()
// Return filters matching the search term
static func search(_ searchTerm: String) -> [Filter] {
if searchTerm.isEmpty { return AllFilters }
return Filter.AllFilters
.filter { $0.userPresenting.localizedCaseInsensitiveContains(searchTerm) }
.sorted(by: { a, b in a.userPresenting < b.userPresenting })
}
}
SwiftUI interface
The SwiftUI implementation is a View. You ‘install’ the quick action bar just like you would any other SwiftUI view.
The QuickActionBar view is zero-sized, and does not display content within the view its installed on.
IdentifyingObject is the type of the object (eg. URL)
IdentifyingObjectView is the type of View used to represent IdentifyingObject in the results list (eg. Text)
You present the quick action bar by setting the visible parameter to true.
For example :-
@State var quickActionBarVisible = false
@State var selectedItem: URL = URL(...)
...
VStack {
Button("Show Quick Action Bar") {
quickActionBarVisible = true
}
QuickActionBar<URL, Text>(
location: .window,
visible: $quickActionBarVisible,
selectedItem: $selectedItem,
placeholderText: "Open Quickly",
itemsForSearchTerm: { searchTask in
let results = /* array of matching URLs */
searchTask.complete(with: results)
},
viewForItem: { url, searchTerm in
Text(url.path)
}
)
.onChange(of: selectedItem) { newValue in
Swift.print("Selected item \(newValue)")
}
}
...
Parameter
Description
location
Where to locate the quick action bar (.window, .screen)
visible
If true, presents the quick action bar on the screen
showKeyboardShortcuts
Display keyboard shortcuts for the first 10 selectable items
requiredClickCount
If .single, only requires the user to single-click a row to activate it (defaults to .double)
barWidth
The width of the presented bar
searchTerm
The search term to use, updated when the quick action bar is closed
selectedItem
The item selected by the user
placeholderText
The text to display in the quick action bar when the search term is empty
itemsForSearchTerm
A block which returns the item(s) for the specified search term
viewForItem
A block which returns the View to display for the specified item
SwiftUI Example
SwiftUI Example
A simple macOS SwiftUI example using Core Image Filters as the content.
SwiftUI View
struct DocoContentView: View {
// Binding to update when the user selects a filter
@State var selectedFilter: Filter?
// Binding to show/hide the quick action bar
@State var quickActionBarVisible = false
var body: some View {
VStack {
Button("Show Quick Action Bar") {
quickActionBarVisible = true
}
QuickActionBar<Filter, Text>(
location: .screen,
visible: $quickActionBarVisible,
selectedItem: $selectedFilter,
placeholderText: "Open Quickly...",
itemsForSearchTerm: { searchTask in
let results = filters__.search(searchTask.searchTerm)
searchTask.complete(with: results)
},
viewForItem: { filter, searchTerm in
Text(filter.userPresenting)
}
)
}
}
}
Data
/// The unique object used as the quick action bar item
struct Filter: Hashable, CustomStringConvertible {
let name: String // The name is unique within our dataset, therefore it will be our identifier
var userPresenting: String { return CIFilter.localizedName(forFilterName: self.name) ?? self.name }
var description: String { name }
}
class Filters {
// If true, displays all of the filters if the search term is empty
var showAllIfEmpty = true
// All the filters
var all: [Filter] = {
let filterNames = CIFilter.filterNames(inCategory: nil).sorted()
return filterNames.map { name in Filter(name: name) }
}()
// Return filters matching the search term
func search(_ searchTerm: String) -> [Filter] {
if searchTerm.isEmpty && showAllIfEmpty { return all }
return all
.filter { $0.userPresenting.localizedCaseInsensitiveContains(searchTerm) }
.sorted(by: { a, b in a.userPresenting < b.userPresenting })
}
}
let filters__ = Filters()
Screenshots
Releases
5.0.0
[BREAKING API] Move to using an asynchronous API for the item search to allow background searching without locking up the UI.
4.1.0
Added optional keyboard shortcuts. Defaults to off to make sure existing projects don’t change their behaviour.
4.0.0
Added ability to mark a row as not selectable (eg. if you want to add a separator item in the results). canSelectItem added to the delegate callback. By default, all rows are selectable.
UI changes to move the UX implementation closer to that of Spotlight/Open Quickly (Xcode).
First search result is selected by default.
Typing in the edit field and using the up/down arrows does not change the UI focus to the result view, the result selection moves but the focus is not removed from the edit field
Set the number of clicks required to ‘activate’ an item (requiredClickCount). Default to double-click.
3.0.5 BREAKING CHANGES
There have been some breaking changes for those moving up from v2 or earlier to v3. This changes were to make the codebase more generic and to improve the SwiftUI support.
Please be aware your existing code will need changes to support the new v3 codebase.
The UUID identifier has been replaced with a Hashable type. This allows other types to be used as an identifier (eg. URL or even structs/classes).
‘Identifier’ used within the API has been changed to Item (eg. viewForIdentifier -> viewForItem)
The didSelectItem delegate callback is now used when the item is selected, not activated. didActivateItem is called when the user ‘activates’ (ie. double-clicks or keyboard selects) a row
The SwiftUI implementation has been changed to be much more SwiftUI-y. Please see the Implementing for SwiftUI section for details.
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 viewForItem 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!
DSFQuickActionBar
A spotlight-inspired quick action bar for macOS.
Why?
I’ve seen this in other mac applications (particularly Spotlight and Boop) and it’s very useful and convenient.
Features
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).
Demos
You can find macOS demo apps in the
Demos
subfolder.Simple Demo
- a simple AppKit application demonstrating a synchronous quick action bar using AppKit, SwiftUI and custom cell typesDoco Demo
- AppKit demo used for generating images for the websiteFaux Spotlight
- An AppKit demo showing asynchronous searching support using MDItemQuery()SwiftUI Demo
- A SwiftUI demonstrationStatusBar Item Demo
- Demonstrates displaying a quick action bar from a statusbar item (in the menu).Process
itemsForSearchTerm
). Theitems
request is asynchronous, and can be completed at any point in the future (as long as it hasn’t been cancelled by another search request)viewForItem
)didActivateItem
)Implementing for AppKit
You present a quick action bar by :-
DSFQuickActionBar
present
method.Presenting
Call the
present
method on the quick action bar instance.NSWindow
String
NSImage
String
CGFloat
Bool
Content Source
The contentSource (
DSFQuickActionBarContentSource
) provides the content and feedback for the quick action bar. The basic mechanism is similar toNSTableViewDataSource
/NSTableViewDelegate
in that the control will :-Delegate style content source
itemsForSearchTermTask
Called when the control needs a array of items to display within the control that match a search term. The definition of ‘match’ is entirely up to you - you can perform any check you want.
The
task
object contains the search term and a completion block to call when the search results become available. If the search text changes during an asynchronous search call the task is marked as invalid and the result will be ignored.Simple synchronous example
If you have code using the old synchronous API, it’s relatively straightforward to convert your existing code to the new api.
Simple asynchronous example
viewForItem
Return the view to be displayed in the row for the item. The search term is also provided to allow the view to be customized for the search term (eg. highlighting the match in the name)
canSelectItem
Called when a item will be selected (eg. by keyboard navigation or clicking). Return false if this row should not be selected (eg. it’s a separator)
didSelectItem
Called when an item is selected within the list.
didActivateItem
Indicates the user activated an item in the result list. The ‘item’ parameter is the item that was selected by the user
didCancel
Called if the user cancels the quick action bar (eg. by hitting the
esc
key or clicking outside the bar)Swift Example
Swift Example
A simple AppKit example using Core Image Filters as the contentSource.
SwiftUI interface
The SwiftUI implementation is a View. You ‘install’ the quick action bar just like you would any other SwiftUI view. The
QuickActionBar
view is zero-sized, and does not display content within the view its installed on.The QuickActionBar template parameters represent
IdentifyingObject
is the type of the object (eg.URL
)IdentifyingObjectView
is the type of View used to representIdentifyingObject
in the results list (eg.Text
)You present the quick action bar by setting the
visible
parameter to true.For example :-
location
visible
showKeyboardShortcuts
requiredClickCount
.single
, only requires the user to single-click a row to activate it (defaults to.double
)barWidth
searchTerm
selectedItem
placeholderText
itemsForSearchTerm
viewForItem
SwiftUI Example
SwiftUI Example
A simple macOS SwiftUI example using Core Image Filters as the content.
SwiftUI View
Data
Screenshots
Releases
5.0.0
4.1.0
4.0.0
canSelectItem
added to the delegate callback. By default, all rows are selectable.UI changes to move the UX implementation closer to that of Spotlight/Open Quickly (Xcode).
requiredClickCount
). Default to double-click.3.0.5 BREAKING CHANGES
There have been some breaking changes for those moving up from v2 or earlier to v3. This changes were to make the codebase more generic and to improve the SwiftUI support. Please be aware your existing code will need changes to support the new v3 codebase.
Hashable
type. This allows other types to be used as an identifier (eg.URL
or even structs/classes).Item
(eg.viewForIdentifier
->viewForItem
)didSelectItem
delegate callback is now used when the item is selected, not activated.didActivateItem
is called when the user ‘activates’ (ie. double-clicks or keyboard selects) a row2.0.2
2.0.1
2.0.0
Note the delegate API has changed for this version, hence moving to 2.0.0 to avoid automatic breakages
viewForItem
delegate method to also pass the current search term.searchTerm
(instead ofterm
) consistently throughout the library1.1.1
1.1.0
1.0.0
0.5.1
0.5.0
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!