若要在 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 實作:

  1. 建立集合檢視並指定現代版面,例如 NSCollectionViewFlowLayout。
  2. 提供資料來源,並註冊用來建立儲存格的項目類別或 nib。
  3. 將 isSelectable 設為 true,並將 allowsMultipleSelection 設為 true。
  4. 根據此操作是否允許沒有任何選取項目,設定 allowsEmptySelection。
  5. 讀取 selectionIndexPaths,再將每個有效索引路徑對應至相應的模型 ID。
  6. 使用項目檢視中的 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 負責表示成員關係,並由顯示陣列負責順序。明確劃分這些職責後,自訂資料列樣式、篩選與排序就能支援同一種易於理解的選取操作。

來源