對於 Mac 項目清單,請將選取狀態繫結至 SwiftUI List,並透過會感知選取狀態的情境選單提供主要動作。單擊會選取資料列;雙擊會開啟它。請將選取狀態與呈現狀態分開。
本教學會在你自己的 macOS App 中,使用三個合成的記憶體內項目實作這種互動方式。也會說明自訂 SwiftUI 表面與 AppKit 檢視的適用範圍,讓你選擇做法時,不會讓多個層級爭相處理同一次點擊。
使用可選取容器的主要動作
Apple 明確記錄了 macOS 可選取容器的單擊選取與雙擊主要動作。SwiftUI 支援的進入點是 contextMenu(forSelectionType:menu:primaryAction:),從 macOS 13 起提供。將它附加至支援選取的容器,例如 List 或 Table。Apple 關於會感知選取狀態的情境選單文件。
這裡有三項彼此獨立的職責:模型用來識別每個項目,清單透過繫結維護選取狀態,而主要動作處理常式則決定開啟的含義。在這個範例中,開啟會顯示項目詳細資料工作表。選取項目只會變更反白的資料列與選取狀態標籤。
請依據負責處理互動的檢視選擇實作方式:
| 你的介面 | 合適的起點 | 你仍須負責的事項 |
|---|---|---|
| macOS 13 或更新版本上的 SwiftUI 項目清單或表格 | 選取繫結加上會感知選取狀態的主要動作 | 解析動作 ID 並呈現對應內容 |
| 自訂 SwiftUI 磁貼或形狀 | 明確組合手勢 | 選取外觀、啟用策略及其他輸入路徑 |
| 嵌入 SwiftUI 的 AppKit 表格 | NSTableView 選取狀態與 doubleAction |
將選取狀態與啟用動作橋接至 SwiftUI 狀態 |
| 自訂 AppKit 繪圖表面 | 自行處理滑鼠事件 | 命中測試、項目識別與點擊序列策略 |
第一種方式最符合這裡的需求。若介面確實需要不同的表面,自訂方式就很有用;但你也必須自行實作更多互動行為。Apple 在手勢組合、AppKit 表格雙擊動作與按下滑鼠按鈕的處理文件中說明了底層的替代方案。
建立完整的單選範例
在 Xcode 中建立 macOS App 專案,使用 SwiftUI 介面,並將最低部署目標設為 macOS 13 或更新版本。將產生的 App 與內容檢視宣告替換為以下單一檔案。目標中只保留一個 @main App 宣告。
程式使用固定的整數 ID、三個有顏色的圓形,以及本機狀態。它不會讀取檔案,也不會開啟外部資源。工具列樣式的 Open 按鈕和情境式 Open 指令,會與雙擊啟用動作呼叫相同函式。
import SwiftUI
struct SampleItem: Identifiable {
let id: Int
let name: String
let tint: Color
}
@main
struct ClickSelectionDemoApp: App {
var body: some Scene {
WindowGroup {
ItemBrowser()
}
}
}
struct ItemBrowser: View {
private let items = [
SampleItem(id: 1, name: "Circle A", tint: .blue),
SampleItem(id: 2, name: "Circle B", tint: .orange),
SampleItem(id: 3, name: "Circle C", tint: .purple)
]
@State private var selectedID: SampleItem.ID? = nil
@State private var openedItem: SampleItem? = nil
@State private var message = "Select an item."
private var selectedItem: SampleItem? {
items.first { $0.id == selectedID }
}
var body: some View {
VStack(alignment: .leading, spacing: 12) {
HStack {
Text(selectedItem.map {
"Selected: \($0.name)"
} ?? "No selection")
Spacer()
Button("Open") {
if let id = selectedID {
openItems([id])
}
}
.disabled(selectedItem == nil)
}
List(items, selection: $selectedID) { item in
HStack(spacing: 10) {
Circle()
.fill(item.tint)
.frame(width: 18, height: 18)
Text(item.name)
}
.padding(.vertical, 4)
}
.contextMenu(forSelectionType: SampleItem.ID.self) { ids in
if ids.count == 1 {
Button("Open") {
openItems(ids)
}
}
} primaryAction: { ids in
openItems(ids)
}
Text(message)
.font(.caption)
}
.padding()
.frame(minWidth: 360, minHeight: 280)
.sheet(item: $openedItem) { item in
VStack(spacing: 16) {
Circle()
.fill(item.tint)
.frame(width: 80, height: 80)
Text(item.name)
.font(.title2)
Button("Close") {
openedItem = nil
}
}
.padding(32)
.frame(minWidth: 260, minHeight: 220)
}
}
private func openItems(_ ids: Set<SampleItem.ID>) {
guard ids.count == 1, let id = ids.first else {
message = "Choose exactly one item to open."
return
}
guard let item = items.first(where: { $0.id == id }) else {
message = "That item is no longer available."
return
}
openedItem = item
message = "Opened: \(item.name)"
}
}
我已檢查這個範例中的宣告與參照,但此處尚未編譯或執行:目前環境沒有 Swift 工具鏈或 macOS 圖形介面。請在 Xcode 中建置,並執行下方的互動檢查。
將識別、選取與開啟分開處理
SampleItem 遵循 Swift 的 Identifiable 協定。其固定的 id 代表同一個項目在檢視更新前後保持一致。Apple 將此協定定義為依據穩定識別值識別項目;顯示名稱則是獨立屬性。在較大型的 App 中,項目名稱或外觀變更時,請保留其 ID。Apple 的 Identifiable 文件。
選用的 selectedID 會建立單選清單。若改用 ID 集合,就能啟用多選。SwiftUI 的清單文件說明了這兩種繫結形式,以及容器提供的選取外觀。Apple 的 List 文件。
openedItem 獨立控制呈現狀態。將它設為某個項目時會呈現工作表;設為 nil 時則會關閉工作表。這符合文件中以項目驅動的工作表 API。由於選取資料列時不會指定 openedItem,因此在此實作中,單靠選取不會觸發工作表。Apple 關於以項目驅動工作表的文件。
解析傳入動作的 ID
情境選單 API 接受容器的識別碼型別,而其動作會收到一組 ID。因此,範例使用 SampleItem.ID.self,與清單的選取狀態相符。它會解析回呼傳入的引數,而不是以另外讀取的選取值取代該引數。Apple 關於情境選單參數的文件。
這個處理常式刻意只接受一個 ID。空集合會顯示一則訊息;無法解析的 ID 會顯示另一則訊息。如果之後啟用多選,請先定義開啟策略,再修改這項檢查:呈現群組詳細資料檢視、開啟個別視窗,或要求只能選取一個項目。任意取用第一個成員會掩蓋尚未定義的策略。
為何兩個點擊處理常式需要不同的設計決策
自訂磁貼可能需要自己的手勢。SwiftUI 的 onTapGesture(count:perform:) 可指定所需的點擊或輕點次數。然而,文件並未說明在檢視上放置兩個獨立修飾器,就能建立互斥的單擊與雙擊契約。Apple 關於輕點動作的文件。
先決定雙擊期間選取的意義。對一般項目瀏覽器而言,在開啟項目之前先選取它很實用,也與容器式方案一致。若有另一項需求,規定只有在點擊序列最後沒有變成雙擊時才執行單擊動作,就需要仲裁。
對自訂表面,SwiftUI 提供 exclusively(before:):只有一個組成手勢會成功,並優先採用第一個。以下是手勢運算式片段,並非清單範例的完整替代方案:Apple 關於互斥手勢的文件。
TapGesture(count: 2)
.exclusively(before: TapGesture(count: 1))
這會將雙擊辨識放在優先位置。將組合後的手勢附加至自訂項目,並依據選取與開啟策略處理結果。TapGesture 文件說明了如何依所需的輕點次數進行辨識。Apple 的 TapGesture 文件。
這項策略會帶來時間上的影響:若要判定某個序列是純粹的單擊,就必須先確認它不會成為雙擊。這是互動需求造成的結果,並非文件所規定的固定 SwiftUI 延遲。若同時要求單擊動作立即完成,且不得在雙擊序列期間發生,就不要承諾能滿足這兩項要求。
對自訂磁貼,也要定義預期可點擊的區域。contentShape(Rectangle()) 可以讓有內距的矩形區域參與命中測試,但不會設定手勢優先順序。診斷點擊未觸發的問題時,請將這兩件事分開處理。Apple 關於內容形狀的文件。
使用 AppKit 時,維持事件處理權的明確歸屬
如果既有的 AppKit 表格負責管理資料列,就使用該表格支援的啟用機制。NSTableView.doubleAction 會在雙擊時對目標傳送選擇子。Apple 也記錄了 clickedRow 與 clickedColumn,可用來判斷啟用發生的位置,包括欄標題。開啟項目之前,請確認該位置確實對應到某個項目。Apple 的 doubleAction 文件。
對自訂 AppKit 表面,mouseDown(with:) 會接收滑鼠左鍵按下事件,而 NSEvent.clickCount 會回報與滑鼠按下或放開事件相關的點擊次數。在無關的事件上讀取該屬性,不符合其文件所載的契約,且可能引發例外。請只在處理適當滑鼠事件的檢視中解讀這個值。Apple 關於按下滑鼠按鈕的文件、Apple 關於點擊次數的文件。
使用 NSViewRepresentable 將自行管理的 AppKit 檢視嵌入 SwiftUI。在 makeNSView 中建立檢視,在 updateNSView 中同步目前設定;若目標動作或委派通訊需要橋接,請使用協調器。Apple 將建立、更新、協調與拆除都列為此整合的一部分。Apple 的 NSViewRepresentable 文件。
會接收滑鼠事件的覆層可能改變處理點擊的檢視。AppKit 會透過命中測試決定接收滑鼠按下事件的對象,因此不應將會攔截事件的覆層視為不可見的觀察者。對於此需求,請在可選取容器或實際負責項目互動的自訂表面中進行辨識。Apple 關於命中測試的文件。
將選取與開啟視為不同結果來驗證
建置範例後,請依序執行以下步驟:
- 單擊
Circle A。其資料列應變成選取狀態,選取標籤應顯示它的名稱,且不應出現工作表。 - 雙擊
Circle B。確認詳細資料工作表顯示Circle B。 - 關閉工作表,再單擊
Circle C。確認選取狀態會變更,且不會開啟另一個工作表。 - 啟用畫面上可見的
Open按鈕。確認它透過相同處理常式開啟選取的項目。 - 呼叫某個資料列的情境式
Open指令,並確認它開啟的項目正確。 - 在清單的未使用空白處單擊或雙擊。確認不會開啟無關項目。
- 如果你加入了內嵌控制項、拖曳功能或覆層,請在那些確切區域重複檢查。
如果選取狀態意外開啟詳細資料,請檢查呈現狀態在哪裡被指定。如果啟用動作開啟錯誤的項目,請檢查回呼 ID 與模型查找。如果自訂磁貼的部分區域無法觸發點擊,請先檢查其版面配置與命中測試形狀,再變更手勢優先順序。這些檢查會分別找出狀態、動作目標與事件傳遞問題,避免同時變更三者。
對於一般的 Mac 項目清單,請以選取繫結和容器的主要動作為核心進行實作。只有在表面或互動確實需要時才加入自訂辨識,並確保每條開啟路徑都透過同一個明確的處理常式解析預期項目。
來源
- contextMenu(forSelectionType:menu:primaryAction:) — Apple
- List — Apple
- Identifiable — Apple
- sheet(item:onDismiss:content:) — Apple
- onTapGesture(count:perform:) — Apple
- exclusively(before:) — Apple
- TapGesture — Apple
- contentShape(_:eoFill:) — Apple
- doubleAction — Apple
- mouseDown(with:) — Apple
- clickCount — Apple
- NSViewRepresentable — Apple
- hitTest(_:) — Apple