Skip to content

About

A Cmd-K command palette for SwiftUI on Mac and iPad: fuzzy search, keyboard navigation, sections, recent commands and shortcuts.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

CommandPalette: a Cmd-K command palette for SwiftUI on Mac and iPad

CI Swift 6.2 macOS 14+ iPadOS 17+ Swift Package Manager MIT License

CommandPalette adds the ⌘K command palette people know from Xcode, Raycast and Linear to your SwiftUI app on the Mac and iPad: fuzzy search with highlighted matches, sections, recent commands at the top, shortcut hints, and full keyboard navigation (↑ ↓ to move, ↩ to run, ⎋ to close). It floats on Liquid Glass on macOS 26 and iPadOS 26 and on a material before; everything runs on macOS 14 and iOS 17.

NotesView()
    .commandPalette(isPresented: $showsPalette, commands: [
        Command("New Note", section: "Notes", symbol: "square.and.pencil",
                shortcut: CommandShortcut("⌘N")) { model.newNote() },
        Command("Toggle Sidebar", section: "View", symbol: "sidebar.left") { model.toggleSidebar() },
    ])
// ⌘K opens it.

Screenshots

Captured from the example app on macOS 26 and an iPad simulator with iPadOS 26 by CI.

Mac: recent commands and sections Mac: searching "new"
The command palette open over a notes app on the Mac: an empty search field, three recent commands, then the Notes section with New Note, New Checklist and Duplicate Note, each with an icon and its keyboard shortcut The palette searching for new: New Note and New Checklist under Notes, the selected New Folder under Folders, New Window and Sort by Newest First under View, with the matched letters highlighted
iPad: recent commands and sections iPad: searching "new"
The command palette over the notes app on iPad, with recent commands and sections The palette on iPad searching for new, with highlighted matches and New Folder selected

Features

  • One modifier (.commandPalette(isPresented:commands:)): the palette floats near the top over a dimmed backdrop; ⌘K toggles it, a click or tap on the backdrop, ⎋ or running a command closes it.
  • Fuzzy search (FuzzyMatcher): the letters of the query in order, ignoring case, diacritics and spaces. Word starts, consecutive letters and prefixes score higher, and the best fit is highlighted ("note" in "Rename Note" highlights the word "Note"). Keywords find a command too: "create" finds "New Note".
  • Sections: commands are grouped under their section; while searching, the section with the best match comes first.
  • Recent commands (RecentCommandsStore): the last five commands you ran, listed first while the search field is empty. Stored in UserDefaults by default, or wherever your RecentCommandsStorage puts them.
  • Keyboard navigation (CommandPaletteSelection): ↑ and ↓ skip section headers and wrap around, ↩ runs, ⎋ closes. A click or tap runs a command too.
  • Shortcut hints (CommandShortcut): "⇧⌘N" or "cmd+shift+n", shown as key caps in menu order, convertible to SwiftUI's KeyboardShortcut.
  • ⌘K anywhere (.commandPaletteShortcut): the shortcut on its own, for a palette you present yourself. Works on the Mac and on iPad with a hardware keyboard.
  • Liquid Glass on macOS 26 and iPadOS 26, a thick material on earlier systems. Every 26 API is behind an availability check.
  • Swift 6 strict concurrency, zero dependencies, the search, ranking, selection and recents logic tested with Swift Testing.

Installation

In Xcode choose File › Add Package Dependencies… and enter:

https://github.com/halilozel1903/CommandPalette

Or add it to Package.swift:

dependencies: [
    .package(url: "https://github.com/halilozel1903/CommandPalette", from: "1.0.0")
]

Usage

Commands

import CommandPalette

let newNote = Command(
    "New Note",                                  // the title, searched first
    id: "new-note",                              // stable: recents are remembered by it
    subtitle: "Start a blank note in this folder",
    section: "Notes",                            // the section header
    symbol: "square.and.pencil",                 // an SF Symbol
    keywords: ["create", "add"],                 // other words that find it
    shortcut: CommandShortcut("⌘N")              // shown as a hint
) {
    model.newNote()                              // runs on the main actor after the palette closes
}

id defaults to "<section>/<title>". Give commands a fixed id when their title can change, for example with localization. The order of the array is the order of the list while the search field is empty.

Show the palette

struct NotesView: View {
    @StateObject private var model = NotesModel()
    @State private var showsPalette = false

    var body: some View {
        EditorView(model: model)
            .toolbar {
                Button("Commands", systemImage: "command") { showsPalette = true }  // for touch
            }
            .commandPalette(
                isPresented: $showsPalette,
                commands: model.commands,
                recents: nil,                     // RecentCommandsStore.shared
                shortcut: .commandK,              // nil for no shortcut
                placeholder: "Search for a command…"
            )
    }
}

The commands are read every time the view updates, so build them from your current state: show "Pin Note" or "Unpin Note", or leave out commands that do not apply.

Configure the palette view

To start with a query, select a command or skip the focus, put your own CommandPaletteView in the same presentation:

.commandPalette(isPresented: $showsPalette) {
    CommandPaletteView(
        commands: model.commands,
        recents: recents,
        placeholder: "Go to…",
        initialQuery: "",
        initialSelection: "new-folder",       // a command id
        autofocus: true,
        onDismiss: { showsPalette = false }
    )
}

CommandPaletteView also works on its own, for example in a sheet or a popover.

Only the ⌘K shortcut

ContentView()
    .commandPaletteShortcut(isPresented: $showsPalette)        // ⌘K toggles it
    .commandPaletteShortcut(CommandShortcut("⇧⌘P")!) { openPalette() }

Recent commands

let recents = RecentCommandsStore()                           // UserDefaults.standard, 5 commands
let recents = RecentCommandsStore(storage: UserDefaultsRecentCommandsStorage(key: "recents", defaults: groupDefaults), limit: 8)
let recents = RecentCommandsStore(storage: InMemoryRecentCommandsStorage(["new-note"]))   // tests, previews

recents.record(command)       // the palette does this when it runs a command
recents.ids                   // newest first
recents.clear()

Store them anywhere else by conforming to RecentCommandsStorage:

@MainActor
final class CloudRecents: RecentCommandsStorage {
    func loadRecentCommandIDs() -> [String] { … }
    func saveRecentCommandIDs(_ ids: [String]) { … }
}

Fuzzy matching on its own

FuzzyMatcher.match("nn", in: "New Note")         // score 98, ranges [0..<1, 4..<5]
FuzzyMatcher.match("note", in: "Rename Note")    // ranges [7..<11]: the word, not the "n" of "Rename"
FuzzyMatcher.match("cafe", in: "Café Recipes")   // diacritics and case are ignored
FuzzyMatcher.match("wen", in: "New Note")        // nil: the letters are out of order

Ranges are character offsets, easy to turn into an AttributedString. The weights are listed in FuzzyMatcher.Weight:

Points
Each matched character 16
… at the start of a word +30
… at a lower-to-upper case change (tableView) +20
… right after the previous match +32
… the first character of the title +15
Each skipped character between matches −3
Each character before the first match −1 (at most −10)
The title starts with the query +50
The title is the query +100

CommandPaletteResults.sections(for:in:recentIDs:) turns a query into the palette's sections. Results are ordered by score, then shorter title, then the order of your array. A keyword match counts half and highlights nothing.

Shortcuts

CommandShortcut("⇧⌘N")?.displayString           // "⇧⌘N"
CommandShortcut("cmd+shift+n")?.keyCaps         // ["⇧", "⌘", "N"]
CommandShortcut("ctrl+option+space")            // ⌃⌥Space
CommandShortcut("n", modifiers: [.command, .option])
CommandShortcut.commandK.keyboardShortcut       // KeyboardShortcut("k", modifiers: .command)

Shortcuts in the palette are hints. To make a command's shortcut work while the palette is closed, give the same shortcut to a menu item or button: .keyboardShortcut(shortcut.keyboardShortcut).

How it works

macOS 26, iPadOS 26 macOS 14 and 15, iPadOS 17 and 18
Palette background .glassEffect(.regular) .thickMaterial with a hairline border
  • Search is a small dynamic program over the query and the title, so the highlighted letters are the best-scoring fit, not the first letters that happen to match. It runs on every keystroke and is fast for hundreds of commands with short titles.
  • Keys come from onKeyPress on the search field (↑, ↓, ⎋) and onSubmit (↩); on the Mac onExitCommand handles ⎋ as well.
  • ⌘K is an invisible button with a keyboardShortcut, so it works whenever the view is on screen, without a menu item.
  • Running a command records it as recent, closes the palette, then calls the command's action.

Example app

The Example folder contains Inkwell, a made-up notes app for the Mac and iPad with 21 commands in five sections. It uses XcodeGen so no project file has to live in the repo:

brew install xcodegen
cd Example && xcodegen generate
open CommandPaletteDemo.xcodeproj    # schemes CommandPaletteDemoMac and CommandPaletteDemoPad

Press ⌘K, or use the search field in the toolbar.

The screenshots come from scripts/screenshots-mac.sh and scripts/screenshots-ipad.sh. Launched with -screenshot <scene> (palette or search), the app opens the palette with fixed content: fixed recent commands, and for search the query "new" with New Folder selected. On the Mac the scene is shown in a borderless window and captured with screencapture -l; the app can also render that window into a PNG itself (-render-screenshot <file>), which is the fallback. On iPad the simulator is captured with simctl io screenshot. Both scripts fail instead of saving a blank capture.

Requirements

  • Xcode 26 or later (Swift 6.2 toolchain)
  • macOS 14+ or iOS/iPadOS 17+ (Liquid Glass automatically on 26)

Contributing

Issues and pull requests are welcome. Please run swift test before opening a PR.

License

CommandPalette is available under the MIT license. See LICENSE.

About

A Cmd-K command palette for SwiftUI on Mac and iPad: fuzzy search, keyboard navigation, sections, recent commands and shortcuts.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages