DSFQuickActionBar

一个受 Spotlight 启发的 macOS 快速操作栏。

Swift Package Manager

Swift Package Manager Swift Package Manager Swift Package Manager

为什么?

我在其他 Mac 应用程序中见过这种操作栏(尤其是 Spotlight 和 Boop),它非常有用和方便。

特性

您可以在窗口上下文中显示快速操作栏(它将位于窗口上方并在窗口边界内居中,如上图所示)或在当前屏幕中居中(就像 Spotlight 当前所做的那样)。

演示

您可以在 Demos 子文件夹中找到 macOS 演示应用程序。

流程

  1. 显示快速操作栏,自动聚焦到编辑字段,以便您的手可以停留在键盘上
  2. 用户开始在搜索字段中输入内容
  3. 对于搜索词的每次更改 -
    1. 将要求 contentSource 提供与搜索词“匹配”的条目 (itemsForSearchTerm)。 items 请求是异步的,可以在将来的任何时间点完成(只要它没有被另一个搜索请求取消)
    2. 对于每个条目,将要求 contentSource 提供一个视图,该视图将显示在该条目的结果表格中 (viewForItem)
    3. 当用户双击或按下所选条目行的回车键时,将向 contentSource 提供该条目 (didActivateItem)
  4. 快速操作栏将在以下情况下自动关闭
    1. 用户单击快速操作栏外部(即,它失去焦点)
    2. 用户按下 Esc 键
    3. 用户双击结果表格中的某个条目
    4. 用户选择一行并按下“回车”键

为 AppKit 实现

您可以通过以下方式显示快速操作栏:-

  1. 创建 DSFQuickActionBar 的实例
  2. 在该实例上设置内容源
  3. 调用 present 方法。

显示

调用快速操作栏实例上的 present 方法。

名称 类型 描述
parentWindow NSWindow 在其上显示快速操作栏的窗口,如果为 nil,则为当前屏幕显示(类似于 Finder Spotlight)
placeholderText String 在编辑字段中显示的占位符文本
searchImage NSImage 显示在搜索编辑字段左侧的图像。如果为 nil,则使用默认的放大镜图像
initialSearchText String 提供在工具栏显示时出现的初始搜索字符串
width CGFloat 强制操作栏的宽度
showKeyboardShortcuts Bool 为前 10 个可选项目显示键盘快捷键(↩︎、⌘1 -> ⌘9)
didClose 回调 快速操作栏关闭时调用

内容源

内容源 (DSFQuickActionBarContentSource) 提供快速操作栏的内容和反馈。基本机制类似于 NSTableViewDataSource/NSTableViewDelegate,控件将:-

  1. 查询内容源以查找与搜索词匹配的条目 (itemsForSearchTerm)
  2. 要求内容源为每个显示的条目提供一个视图 (viewForItem)
  3. 指示用户已按下/单击结果中的选择。
  4. (可选)向内容源指示快速操作栏已关闭。

委托风格的内容源

itemsForSearchTermTask

func quickActionBar(_ quickActionBar: DSFQuickActionBar, itemsForSearchTermTask task: DSFQuickActionBar.SearchTask)

当控件需要一个条目数组以显示在控件中并与搜索词匹配时调用。 “匹配”的定义完全由您决定 - 您可以执行任何您想要的检查。

task 对象包含搜索词和一个完成块,用于在搜索结果可用时调用。 如果在异步搜索调用期间搜索文本发生更改,则该任务将被标记为无效,并且结果将被忽略。

简单的同步示例

如果您有使用旧的同步 API 的代码,则将现有代码转换为新的 API 相对简单。

func quickActionBar(_ quickActionBar: DSFQuickActionBar, itemsForSearchTermTask task: DSFQuickActionBar.SearchTask)
   let results = countryNames.filter { $0.name.startsWith(task.searchTerm) }
   task.complete(with: results)
}
简单的异步示例
var currentSearch: SomeRemoteSearchMechanism?
func quickActionBar(_ quickActionBar: DSFQuickActionBar, itemsForSearchTermTask task: DSFQuickActionBar.SearchTask)
   currentSearch?.cancel()
   currentSearch = SomeRemoteSearchMechanism(task.searchTerm) { [weak self] results in
      task.complete(with: results)
      self?.currentSearch = nil
   }
}

viewForItem

func quickActionBar(_ quickActionBar: DSFQuickActionBar, viewForItem item: AnyHashable, searchTerm: String) -> NSView?

返回要在条目的行中显示的视图。 还提供了搜索词,以允许为搜索词自定义视图(例如,突出显示名称中的匹配项)


canSelectItem

func quickActionBar(_ quickActionBar: DSFQuickActionBar, canSelectItem item: AnyHashable) -> Bool

当要选择一个条目时调用(例如,通过键盘导航或单击)。 如果不应选择此行,则返回 false(例如,它是一个分隔符)


didSelectItem

func quickActionBar(_ quickActionBar: DSFQuickActionBar, didSelectItem item: AnyHashable)

当在列表中选择一个条目时调用。


didActivateItem

// Swift
func quickActionBar(_ quickActionBar: DSFQuickActionBar, didActivateItem item: AnyHashable)

指示用户激活了结果列表中的一个条目。 “item”参数是用户选择的条目


didCancel

func quickActionBarDidCancel(_ quickActionBar: DSFQuickActionBar)

如果用户取消了快速操作栏(例如,通过按 esc 键或单击栏外部)则调用


Swift 示例

Swift 示例

一个简单的 AppKit 示例,使用 Core Image Filters 作为 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 })
   }
}

Screenshot for the sample data

SwiftUI 界面

SwiftUI 实现是一个视图。 您像安装任何其他 SwiftUI 视图一样“安装”快速操作栏。 QuickActionBar 视图大小为零,并且不会在其安装的视图中显示内容。

QuickActionBar<IdentifyingObject, IdentifyingObjectView>

QuickActionBar 模板参数表示

您可以通过将 visible 参数设置为 true 来显示快速操作栏。

例如:-

@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)")
   }
}
...
参数 描述
location 快速操作栏的放置位置 (.window, .screen)
visible 如果为 true,则在屏幕上显示快速操作栏
showKeyboardShortcuts 为前 10 个可选项目显示键盘快捷键
requiredClickCount 如果为 .single,则仅要求用户单击一行即可激活它(默认为 .double
barWidth 显示的栏的宽度
searchTerm 要使用的搜索词,在快速操作栏关闭时更新
selectedItem 用户选择的条目
placeholderText 当搜索词为空时,在快速操作栏中显示的文本
itemsForSearchTerm 一个块,用于返回指定搜索词的条目
viewForItem 一个块,用于返回要为指定条目显示的视图
SwiftUI 示例

SwiftUI 示例

一个简单的 macOS SwiftUI 示例,使用 Core Image Filters 作为内容。

SwiftUI 视图

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)
            }
         )
      }
   }
}

数据

/// 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()

屏幕截图


许可证

MIT 许可证。 您可以随意使用和滥用它,只需署名我的作品即可。 如果您在某个地方使用它,请告诉我,我很乐意听到!

MIT License

Copyright © 2022 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.