若要在 Mac app 中建立自訂的單欄集合,請使用 SwiftUI 的 List,並以一組穩定的項目 ID 作為選取繫結的資料來源。自訂資料列外觀,同時讓原生控制項處理範圍選取與 Command 點按選取。
本教學會建立一組合成標籤,從 Tile 01 到 Tile 12。讀者要選取相鄰項目、加入或移除個別項目,並在篩選或反轉顯示順序時,讓選取狀態易於理解。後續章節會說明需要格狀或其他自訂版面的集合所適用的 AppKit 作法。
範圍選取與 Command 點按選取應如何運作?
Apple 的 Mac 使用手冊介紹了常見操作方式:按一下第一個項目,再按住 Shift 點按最後一個項目,即可選取相鄰項目,包括兩者之間的項目。Command 點按可選取彼此不相鄰的個別項目;若 Command 點按已選取的項目,則會將該項目從多重選取中移除。這些是此集合應保留的基本操作。在 Mac 螢幕上選取項目。
初始實作請使用單一排序列表。如此一來,「之間」就有明確含義:目前顯示順序中兩個端點之間的資料列。空間型集合會帶來另一項決策:範圍要依照資料序列,還是版面中的區域;這需要另外測試。
根據使用者要選取的內容來選擇控制項:
| 讀者的操作 | 合適的起點 | 選取狀態表示方式 |
|---|---|---|
| 選取完整的自訂資料列 | SwiftUI List |
項目 ID 集合 |
| 在自訂版面中選取完整圖塊 | AppKit NSCollectionView |
選取索引路徑,再對應至模型 ID |
| 選取標籤中的字元 | SwiftUI 文字選取 | 由文字檢視提供的文字選取狀態 |
| 選取文字文件中的範圍 | AppKit 文字系統 | 字元或文字範圍 |
Apple 將 List 說明為可選擇性支援選取的單欄容器,並將 NSCollectionView 說明為具有可自訂版面的排序集合。這些是支援項目選取的合適起點。List、NSCollectionView。
建立完整的 SwiftUI 範例
在 Xcode 中使用 SwiftUI 建立 macOS app 專案。此範例的部署目標設為 macOS 14 或更新版本:下方使用的雙參數 onChange 閉包需要 macOS 14。以這段程式碼取代產生的 app 宣告,並確保目標中恰好只有一個 @main 進入點。onChange(of:initial:_:)、App。
以下是包含合成資料與視窗的完整小型 app,不使用外部資產。程式碼已對照文件中的 API 檢視,但此處未在 macOS 上編譯或執行。
import Foundation
import SwiftUI
struct Tile: Identifiable {
let id: Int
let title: String
}
@main
struct CollectionSelectionDemoApp: App {
var body: some Scene {
WindowGroup {
TileCollectionView()
}
}
}
struct TileCollectionView: View {
private let tiles: [Tile] = (1...12).map {
Tile(id: $0, title: String(format: "Tile %02d", $0))
}
@State private var selectedIDs: Set<Int> = []
@State private var query = ""
@State private var reversed = false
private var visibleTiles: [Tile] {
let filtered = tiles.filter {
query.isEmpty ||
$0.title.localizedCaseInsensitiveContains(query)
}
return reversed ? Array(filtered.reversed()) : filtered
}
private var selectionDescription: String {
let names = visibleTiles
.filter { selectedIDs.contains($0.id) }
.map(\.title)
return names.isEmpty ? "None selected" : names.joined(separator: ", ")
}
var body: some View {
VStack(alignment: .leading, spacing: 12) {
TextField("Filter tiles", text: $query)
.textFieldStyle(.roundedBorder)
HStack {
Button("Reverse order") {
reversed.toggle()
}
Button("Clear selection") {
selectedIDs.removeAll()
}
.disabled(selectedIDs.isEmpty)
}
List(visibleTiles, selection: $selectedIDs) { tile in
HStack(spacing: 12) {
RoundedRectangle(cornerRadius: 4)
.fill(Color.gray.opacity(0.2))
.frame(width: 32, height: 32)
VStack(alignment: .leading) {
Text(tile.title)
Text("Sample item \(tile.id)")
.font(.caption)
.foregroundStyle(.secondary)
}
Spacer()
}
.padding(.vertical, 4)
.tag(tile.id)
}
Text("\(selectedIDs.count) selected")
Text(selectionDescription)
.font(.caption)
}
.padding()
.frame(minWidth: 440, minHeight: 480)
.onChange(of: query) { _, _ in
let visibleIDs = Set(visibleTiles.map(\.id))
selectedIDs.formIntersection(visibleIDs)
}
}
}
選取繫結的元素型別與 Tile.ID 相符:兩者都使用 Int。Apple 文件指出,將列表的選取繫結至與其資料 ID 型別相同的集合,即可啟用多重選取。在 Mac 上,多重選取不需要進入編輯模式。在列表中支援選取。
資料列仍可自訂:其中包含中性色彩的形狀、標題和次要標籤。沒有資料列層級的點按處理器,也沒有獨立的切換操作。讓列表負責選取,可避免同一次操作由兩段獨立程式碼更新選取狀態。
將項目識別與顯示順序分開
此範例刻意將三個概念分開:排序後的項目陣列、已選取 ID 的集合,以及該陣列經篩選後的呈現結果。Swift 將陣列定義為有順序的集合,將集合定義為不重複且沒有固定順序的值集合。因此,集合適合表示成員關係,而陣列則提供呈現時使用的順序。集合型別。
此合成資料集使用固定的整數 ID。反轉陣列只會改變項目的顯示位置,不會改變其識別。在實際模型中,請在建立或載入項目時指派穩定識別碼,並在後續檢視更新時保留該識別碼。避免每次評估資料列時都產生新的識別碼。
選取摘要會依照 visibleTiles 的順序,並檢查 selectedIDs 中是否包含該項目。這是根據上述集合特性做出的實作選擇:摘要會依顯示順序產生,而不依賴集合的迭代順序。
篩選需要明確的產品決策。此範例採用僅保留可見項目的政策:每當篩選條件變更,就將選取狀態與仍可見的 ID 取交集。隱藏項目會取消選取,清除篩選條件後也不會還原先前的選取狀態。標籤與來源陣列則維持不變。
例如,先選取 Tile 02、Tile 03 和 Tile 04。在 Filter tiles 中輸入 04。依照此範例的政策,只有 Tile 04 會保持選取。移除篩選條件後,另外兩個項目會重新出現,但不會被選取。這是對程式碼的示範說明,不是實際執行結果的報告。
其他 app 可能需要在篩選期間保留選取狀態。遇到這種情況,請將選取狀態的管理移至模型,定義隱藏選取項目在計數與摘要中的呈現方式,並測試其與顯示控制項的同步處理。單純移除取交集的程式碼,並不能構成完整的隱藏選取政策。
何時應使用 AppKit 集合檢視?
當集合需要以多欄圖塊呈現,或使用單一列表以外的版面時,請使用 NSCollectionView。Apple 將集合的資料來源、項目檢視控制器、委派物件與版面物件分開。如此可自訂外觀與位置,同時保留由集合控制項負責互動。NSCollectionView。
請依照下列順序設定 AppKit 實作:
- 建立集合檢視並指定現代版面,例如
NSCollectionViewFlowLayout。 - 提供資料來源,並註冊用來建立儲存格的項目類別或 nib。
- 將
isSelectable設為true,並將allowsMultipleSelection設為true。 - 根據此操作是否允許沒有任何選取項目,設定
allowsEmptySelection。 - 讀取
selectionIndexPaths,再將每個有效索引路徑對應至相應的模型 ID。 - 使用項目檢視中的
isSelected狀態呈現選取情形。
多重選取預設為停用。因此,啟用它是明確的設定步驟,不能只因圖塊看起來可選取就視為已啟用。項目的 isSelected 屬性會回報該項目是否已選取。allowsMultipleSelection、isSelected。
請將索引路徑視為目前資料來源中的位置。Apple 文件指出,互動選取期間 selectionIndexPaths 會更新,也可以透過程式設定。若指定的索引路徑未對應至有效的資料來源項目,便會引發例外。重新排序或篩選後,請根據穩定 ID 重建選取位置,而不要沿用過時的位置。selectionIndexPaths。
請遵守文件所載的界線:allowsMultipleSelection 可啟用多項選取,但其文件未針對所有自訂版面或修飾器組合,規定完整的範圍錨點演算法。請在實際版面與支援的 macOS 版本上確認 Shift 點按行為。如果需要精確的線性範圍,以上單欄實作是更清楚的起點。
若 SwiftUI app 需要使用 AppKit 控制項,可透過 NSViewRepresentable 嵌入。在 makeNSView 中建立 AppKit 檢視階層,在 updateNSView 中協調模型變更,並使用協調器將委派事件傳遞至 SwiftUI 狀態。SwiftUI 會控制被呈現檢視的框架與邊界。NSViewRepresentable。
程式化更新也需要明確的同步路徑。Apple 指出,透過程式設定選取狀態時,會略過 collectionView(_:shouldSelectItemsAt:) 和 collectionView(_:didSelectItemsAt:)。請在共用操作中處理模型更新,不要依賴每次設定後都呼叫這些回呼。collectionView(_:shouldSelectItemsAt:)、collectionView(_:didSelectItemsAt:)。
文字選取能提供相同功能嗎?
SwiftUI 的 textSelection(.enabled) 控制檢視內文字的選取。套用至容器時,容器內的每個文字檢視都會變得可選取。文件並未說明它能提供集合項目選取繫結,或在任意卡片之間共用範圍選取模型。若操作目標是從標籤中選取字元,請使用此功能。textSelection(_:)。
對於文字文件介面,AppKit 透過 NSTextView.selectedRanges 提供字元選取狀態。其範圍會依序排列、互不重疊,且可彼此不相連。TextKit 的 NSTextSelection.textRanges 同樣表示在邏輯上依序排列的文字範圍,並可從 macOS 12 起使用。這些 API 描述的是文字位置,因此若要將它們轉換成圖塊 ID,需由 app 自行設計。selectedRanges、textRanges。
啟用標籤內的文字選取前,先決定資料列標籤應支援哪種操作。就本教學的操作目標而言,點按標籤應參與完整資料列的選取,因此範例不會設定文字選取。
用小型選取矩陣確認互動
請在 app 自己的視窗中執行以下檢查。前四項檢查 Mac 的基本選取慣例;其餘項目則測試範例對識別與篩選所採用的決策。Apple 文件中的操作方式是基準,而合成標籤則讓預期的選取項目容易檢視。在 Mac 螢幕上選取項目。
| 操作 | 預期結果 |
|---|---|
點按 Tile 02 |
選取一個項目 |
按住 Shift 點按 Tile 05 |
選取 Tile 02 到 Tile 05 |
按住 Command 點按 Tile 08 |
將 Tile 08 加入選取範圍 |
按住 Command 點按已選取的 Tile 03 |
將 Tile 03 從選取範圍移除 |
點按 Clear selection |
清空選取狀態,摘要顯示 None selected |
選取 Tile 04,再反轉順序 |
已選取的 ID 仍對應 Tile 04 |
選取數個資料列,再篩選為 04 |
只有先前已選取且仍可見的項目會保持選取 |
| 清除篩選條件 | 隱藏的資料列會重新出現,但已捨棄的選取狀態不會還原 |
也請檢查點按列表後的鍵盤導覽、將已選取的資料列捲出檢視範圍再捲回,以及篩選後開始新的範圍選取。若使用 AppKit 格狀版面,請增加跨越列邊界的檢查。如果結果與操作目標不符,請先將介面簡化為純原生控制項,再一次引入一種自訂資料列內容或手勢處理器。
讓原生控制項負責互動、穩定 ID 負責表示成員關係,並由顯示陣列負責順序。明確劃分這些職責後,自訂資料列樣式、篩選與排序就能支援同一種易於理解的選取操作。
來源
- 在 Mac 螢幕上選取項目 — Apple 支援。
- List — Apple 開發者文件。
- App — Apple 開發者文件。
- onChange(of:initial:_:) — Apple 開發者文件。
- 集合型別 — Swift 程式語言,Swift 專案。
- NSCollectionView — Apple 開發者文件。
- allowsMultipleSelection — Apple 開發者文件。
- selectionIndexPaths — Apple 開發者文件。
- isSelected — Apple 開發者文件。
- NSViewRepresentable — Apple 開發者文件。
- collectionView(_:shouldSelectItemsAt:) — Apple 開發者文件。
- collectionView(_:didSelectItemsAt:) — Apple 開發者文件。
- textSelection(_:) — Apple 開發者文件。
- selectedRanges — Apple 開發者文件。
- textRanges — Apple 開發者文件。