För en anpassad samling med en kolumn i din Mac-app använder du SwiftUI:s List med en markeringsbindning som bygger på en mängd stabila objekt-ID:n. Anpassa raderna och låt den inbyggda kontrollen hantera intervallmarkering och markering med kommandoklick.

Den här handledningen skapar en samling syntetiska etiketter, från Tile 01 till Tile 12. Läsaren ska markera intilliggande objekt, lägga till eller ta bort enskilda objekt och hålla markeringen begriplig när visningen filtreras eller sorteras i omvänd ordning. Ett senare avsnitt förklarar AppKit-vägen för en samling som behöver ett rutnät eller en annan anpassad layout.

Vad ska intervallmarkering och markering med kommandoklick göra?

I Apples Mac-användarhandbok beskrivs det välbekanta beteendet: klicka på det första objektet och Skift-klicka på det sista för att markera intilliggande objekt, inklusive objekten däremellan. Med kommandoklick markerar du enskilda objekt som inte behöver ligga intill varandra. Om du kommandoklickar på ett markerat objekt tas det bort från en flermarkering. Det här är de grundläggande interaktionerna som ska bevaras i den här samlingen. Markera objekt på din Mac-skärm.

Använd en enda ordnad lista i den första implementeringen. Då får ”däremellan” en entydig betydelse: raderna mellan två ändpunkter i den aktuella visningsordningen. En samling med rumslig layout innebär ytterligare ett beslut – om ett intervall följer dataserien eller en del av layouten – vilket bör testas separat.

Välj kontroll utifrån vad användaren markerar:

Läsarens uppgift Lämplig utgångspunkt Representation av markering
Markera hela anpassade rader SwiftUI List Mängd med objekt-ID:n
Markera hela brickor i en anpassad layout AppKit NSCollectionView Markerade indexsökvägar, mappade till modellens ID:n
Markera tecken i en etikett SwiftUI-textmarkering Textmarkering som tillhandahålls av textvyn
Markera intervall i ett textdokument AppKits textsystem Tecken- eller textintervall

Apple beskriver List som en behållare med en kolumn och valfri markering, och NSCollectionView som en ordnad samling med anpassningsbar layout. Det här är de understödda utgångspunkterna för objektmarkering. List, NSCollectionView.

Bygg det fullständiga SwiftUI-exemplet

Skapa ett macOS-approjekt i Xcode med SwiftUI. Ange macOS 14 eller senare som distributionsmål för det här exemplet: tvåparametersstängningen onChange som används nedan kräver macOS 14. Ersätt den genererade appdeklarationen med den här koden och se till att målet innehåller exakt en @main-startpunkt. onChange(of:initial:_:), App.

Följande kod är en komplett liten app med syntetiska data och ett fönster. Den använder inga externa resurser. Den har granskats mot de dokumenterade API:erna, men har inte kompilerats eller körts på macOS här.

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

Markeringens bindningstyp motsvarar Tile.ID: båda använder Int. Apple dokumenterar att en bindning av listans markering till en mängd av datans ID-typ möjliggör flermarkering. På en Mac kräver flermarkering inte att redigeringsläge aktiveras. Stöd för markering i listor.

Raden kan fortfarande anpassas: den innehåller en neutral form, en titel och en sekundär etikett. Det finns ingen klickhanterare på radnivå eller separat växlingsåtgärd. Genom att låta listan sköta markeringen undviker du att två oberoende koddelar uppdaterar markeringen efter samma interaktion.

Håll objektens identitet åtskild från visningsordningen

Exemplet använder medvetet tre separata begrepp: en ordnad array med objekt, en mängd med markerade ID:n och en filtrerad vy av arrayen. Swift definierar arrayer som ordnade samlingar och mängder som samlingar med unika värden utan bestämd ordning. Därför passar en mängd för medlemskap, medan en array tillhandahåller ordningen som används vid visning. Samlingstyper.

Heltals-ID:na är fasta för den här syntetiska datamängden. Om arrayen vänds ändras objektets plats, men inte dess identitet. Tilldela varje objekt en stabil identifierare när du skapar eller läser in det i en riktig modell och behåll den identifieraren vid senare uppdateringar av vyn. Undvik att skapa en ny identitet varje gång en rad utvärderas.

Sammanfattningen av markeringen följer visibleTiles och kontrollerar medlemskap i selectedIDs. Det här är ett implementeringsval som bygger på egenskaperna hos dessa samlingar: sammanfattningen visas i visningsordning i stället för att förlita sig på mängdens iterationsordning.

Filtrering kräver ett uttryckligt produktbeslut. Det här exemplet använder en policy där endast synliga objekt är markerade: varje gång filtret ändras begränsas markeringen till de ID:n som fortfarande är synliga. Dolda objekt förlorar sin markering, och när filtret tas bort återställs inte deras tidigare markering. Etiketterna och källarrayen förblir oförändrade.

Börja till exempel med Tile 02, Tile 03 och Tile 04 markerade. Skriv 04 i Filter tiles. Enligt policyn i det här exemplet förblir bara Tile 04 markerad. Ta bort filtret så visas de andra två objekten igen, omarkerade. Det här är en illustrativ genomgång av koden, inte ett rapporterat körningsresultat.

En annan app kan behöva behålla markeringen mellan olika filter. Flytta i så fall ägarskapet över markeringen till modellen, definiera hur dolda markeringar visas i antalet och sammanfattningen och testa att avstämningen fungerar med den visade kontrollen. Att bara ta bort begränsningen ger inte en fullständig policy för dolda markeringar.

När bör du använda en AppKit-samlingsvy?

Använd NSCollectionView när samlingen behöver brickor i flera kolumner eller en layout som går utöver en enda lista. Apple skiljer mellan samlingens datakälla, objektvyskontroller, delegat och layoutobjekt. Det gör att du kan anpassa utseende och placering samtidigt som en samlingskontroll ansvarar för interaktionen. NSCollectionView.

Konfigurera AppKit-implementeringen i den här ordningen:

  1. Skapa samlingsvyn och tilldela den en modern layout, till exempel NSCollectionViewFlowLayout.
  2. Ange dess datakälla och registrera objektklassen eller nib-filen som används för att skapa celler.
  3. Sätt isSelectable till true och allowsMultipleSelection till true.
  4. Sätt allowsEmptySelection efter om uppgiften tillåter att inget är markerat.
  5. Läs selectionIndexPaths och mappa sedan varje giltig indexsökväg till motsvarande modell-ID.
  6. Visa markeringen i objektvyn med dess tillstånd isSelected.

Flermarkering är inaktiverad som standard. Därför måste den aktiveras uttryckligen som ett konfigurationssteg; det räcker inte att rita brickor som ser markerbara ut. Objektets egenskap isSelected anger om objektet är markerat. allowsMultipleSelection, isSelected.

Behandla indexsökvägar som platser i den aktuella datakällan. Apple dokumenterar att selectionIndexPaths uppdateras vid interaktiv markering och också kan tilldelas programmatiskt. Om en indexsökväg tilldelas som inte identifierar ett giltigt objekt i datakällan utlöses ett undantag. Återskapa markeringsplatser från stabila ID:n efter omordning eller filtrering i stället för att återanvända inaktuella positioner. selectionIndexPaths.

Följ den dokumenterade avgränsningen: allowsMultipleSelection möjliggör markering av flera objekt. Dokumentationen beskriver inte en fullständig algoritm för intervallankare för varje anpassad layout eller kombination av modifierare. Verifiera beteendet för Skift-klick i den faktiska layouten och i de macOS-versioner som stöds. Om uppgiften kräver ett exakt linjärt intervall är implementeringen med en kolumn ovan en tydligare utgångspunkt.

Om en SwiftUI-app behöver AppKit-kontrollen bäddar du in den via NSViewRepresentable. Skapa AppKit-hierarkin i makeNSView, stäm av modelländringar i updateNSView och använd en koordinator för att föra delegathändelser vidare till SwiftUI-tillståndet. SwiftUI styr den representerade vyns ram och gränser. NSViewRepresentable.

Programmerade uppdateringar behöver också en uttrycklig synkroniseringsväg. Apple anger att både collectionView(_:shouldSelectItemsAt:) och collectionView(_:didSelectItemsAt:) hoppas över när markeringen ställs in programmatiskt. Samla modelluppdateringar i en gemensam åtgärd i stället för att förlita dig på att dessa återanrop körs efter varje tilldelning. collectionView(_:shouldSelectItemsAt:), collectionView(_:didSelectItemsAt:).

Ger textmarkering samma funktion?

SwiftUI:s textSelection(.enabled) styr markering av text i vyer. Om du använder den på en behållare blir varje textvy i behållaren markerbar. Den dokumenterar ingen bindning för objektmarkering i en samling eller gemensam modell för intervallmarkering över godtyckliga kort. Använd den när uppgiften är att markera tecken i en etikett. textSelection(_:).

För ett textdokumentgränssnitt exponerar AppKit teckenmarkeringar via NSTextView.selectedRanges. Intervallen är ordnade, överlappar inte varandra och behöver inte ligga intill varandra. TextKits NSTextSelection.textRanges representerar också logiskt ordnade textintervall och är tillgängligt från macOS 12. Dessa API:er beskriver textpositioner, så att översätta dem till brick-ID:n kräver en programspecifik utformning. selectedRanges, textRanges.

Bestäm vilken funktion en rads etikett ska ha innan du aktiverar textmarkering i den. För uppgiften i den här handledningen ska ett klick på en etikett bidra till att markera hela raden, så exemplet konfigurerar inte textmarkering.

Verifiera interaktionen med en liten markeringstabell

Kör dessa kontroller i appens eget fönster. De fyra första kontrollerar grundläggande markeringsbeteenden på Mac; de återstående kontrollerna testar exempelprogrammets beslut om identitet och filtrering. Apples dokumenterade interaktion är utgångspunkten, medan de syntetiska etiketterna gör den förväntade markeringen enkel att granska. Markera objekt på din Mac-skärm.

Åtgärd Förväntat resultat
Klicka på Tile 02 Ett objekt markerat
Skift-klicka på Tile 05 Tile 02 till och med Tile 05 markerade
Kommandoklicka på Tile 08 Tile 08 läggs till i markeringen
Kommandoklicka på markerade Tile 03 Tile 03 tas bort från markeringen
Klicka på Clear selection Tom markering och sammanfattningen None selected
Markera Tile 04 och vänd sedan ordningen Det markerade ID:t identifierar fortfarande Tile 04
Markera flera rader och filtrera sedan på 04 Endast ett tidigare markerat och fortfarande synligt objekt förblir markerat
Ta bort filtret Dolda rader visas igen utan att tidigare borttagna markeringar återställs

Kontrollera också tangentbordsnavigering efter att du klickat på listan, att markerade rader kan rullas ut ur vyn och tillbaka igen samt att ett nytt intervall kan påbörjas efter filtrering. Lägg till kontroller där markeringen går över en radgräns för ett AppKit-rutnät. Om resultatet avviker från den avsedda uppgiften kan du först förenkla gränssnittet till en vanlig inbyggd kontroll och sedan lägga till anpassat radinnehåll och gesthanterare en i taget.

Låt den inbyggda kontrollen ansvara för interaktionen, stabila ID:n för medlemskapet och den visade arrayen för ordningen. När ansvarsområdena är tydliga kan anpassad radformatering, filtrering och sortering stödja samma begripliga markeringsuppgift.

Källor