Wil je dat een samengestelde kaart tijdens een native macOS-sleepactie intact blijft, geef de buitenste container dan één sleepbron en gebruik de volledige kaart als voorbeeldweergave. Gebruik AppKit wanneer je expliciete controle nodig hebt over het beginframe van de voorbeeldweergave en de offset ten opzichte van de aanwijzer.

Deze gids gaat over het slepen van één kaart in het geheugen tussen twee panelen in je eigen Mac-app. Het praktische doel is de vormen, tekst, tussenruimten en identiteit van de kaart te behouden, terwijl de afhandeling van gebaren en modelupdates duidelijk belegd blijven.

Welk onderdeel van de sleepactie moet veranderen?

Apple maakt onderscheid tussen de overgedragen gegevens en de visuele weergave die tijdens het slepen wordt getoond. SwiftUI’s draggable(_:preview:) genereert een weergave van je voorbeeldweergave wanneer het slepen begint. Volgens de documentatie wordt de voorbeeldweergave gecentreerd boven de bronweergave. De bronweergave zelf wordt niet overgedragen. Zie Apple’s draggable(_:preview:) documentatie.

Gebruik dit beslisschema om te bepalen welke aanpassing je nodig hebt:

Gewenst gedrag Gedocumenteerd uitgangspunt
De tekst, vormen en achtergrond van de kaart bij elkaar houden Eén sleepbron op de buitenste container met één complete aangepaste voorbeeldweergave
De omlijning tijdens de overgang bij het oppakken aanpassen contentShape(.dragPreview, shape)
Bepalen welk deel van de kaart het slepen kan starten contentShape(.interaction, shape)
Een kaart naar een ander paneel verplaatsen De payload valideren en het model in de bestemming bijwerken
De beginrechthoek van de sleepafbeelding en de relatie tot de aanwijzer expliciet instellen Een AppKit-sessie met NSDraggingItem
Een element continu verplaatsen binnen een canvas zonder native dropbestemmingen Een lokale interactie met DragGesture

Het onderscheid tussen de twee contentvormen is belangrijk: Apple documenteert dat .dragPreview de vorm van de voorbeeldweergave bepaalt, terwijl .interaction de hit-testing regelt. Alleen de omtrek van de voorbeeldweergave aanpassen vergroot het gebied dat het slepen start niet. Zie ContentShapeKinds.dragPreview.

Een lokale DragGesture rapporteert een sleepbeweging via een gebarenreeks. Kies die aanpak wanneer je de positie op een canvas wilt bijwerken. Voor de native drop tussen panelen in dit artikel laat je draggable de sleepgebaren installeren. Zie Apple’s DragGesture-referentie en gids voor sleepbronnen.

Maak één voorbeeldweergave voor de volledige samengestelde kaart

Plaats de sleepmodifier op de kleinste container die alles omvat wat samen moet worden verplaatst. Voor een kaart die bestaat uit een cirkel, twee tekstregels en een afgeronde achtergrond, is dat de volledige kaart. Afzonderlijke sleepbronnen aan de onderliggende elementen zouden afzonderlijke interacties definiëren.

Gebruik voor de bron en de voorbeeldweergave dezelfde presentatieweergave. Geef die weergave expliciete afmetingen en een eigen achtergrond, in plaats van te vertrouwen op overgenomen stijlen van een omringend paneel. Dit is een ontwerpaanbeveling op basis van het gedrag van de gerenderde voorbeeldweergave: met een zelfstandige presentatie is het eenvoudiger de twee weergaven op elkaar af te stemmen.

Compleet SwiftUI-voorbeeld

Maak een macOS SwiftUI-app met een implementatiedoel van macOS 13 of hoger. Vervang het gegenereerde app-entrypoint en de contentweergave door dit ene bestand en zorg dat het doel precies één @main-declaratie bevat. Zowel de modifier voor aangepaste voorbeeldweergaven als de bestemmingsoverload die hier wordt gebruikt, is volgens de documentatie beschikbaar vanaf macOS 13. Zie draggable(_:preview:) en dropDestination(for:action:isTargeted:).

import Foundation
import SwiftUI

@main
struct IntactDragDemoApp: App {
    var body: some Scene {
        WindowGroup {
            BoardView()
        }
    }
}

private struct CardFace: View {
    var body: some View {
        HStack(spacing: 14) {
            Circle()
                .fill(Color.orange)
                .frame(width: 36, height: 36)

            VStack(alignment: .leading, spacing: 6) {
                Text("Tile A")
                    .font(.headline)
                Text("Circle and label")
                    .font(.caption)
            }
            .foregroundStyle(Color.white)
        }
        .padding(16)
        .frame(width: 220, height: 100, alignment: .leading)
        .background(Color.indigo)
        .clipShape(RoundedRectangle(cornerRadius: 16))
    }
}

private struct BoardView: View {
    @State private var token = UUID().uuidString
    @State private var isOnRight = false

    var body: some View {
        VStack(spacing: 20) {
            Text("Drag the card between panels")
                .font(.headline)

            HStack(spacing: 24) {
                panel(right: false)
                panel(right: true)
            }
        }
        .padding(24)
    }

    private func panel(right: Bool) -> some View {
        VStack(spacing: 16) {
            Text(right ? "Right" : "Left")
                .font(.headline)

            ZStack {
                if isOnRight == right {
                    CardFace()
                        .contentShape(
                            .interaction,
                            RoundedRectangle(cornerRadius: 16)
                        )
                        .contentShape(
                            .dragPreview,
                            RoundedRectangle(cornerRadius: 16)
                        )
                        .draggable(token) {
                            CardFace()
                        }
                } else {
                    Text("Drop here")
                        .foregroundStyle(.secondary)
                }
            }
            .frame(width: 240, height: 120)
        }
        .frame(width: 260, height: 180)
        .background(
            RoundedRectangle(cornerRadius: 20)
                .fill(Color.gray.opacity(0.12))
        )
        .contentShape(Rectangle())
        .dropDestination(for: String.self) { items, _ in
            guard items == [token] else {
                return false
            }
            isOnRight = right
            return true
        }
    }
}

Dit voorbeeld gebruikt alleen synthetische afbeeldingen en een gegenereerde identifier in het geheugen. String voldoet al aan Transferable en heeft dus geen aangepaste overdrachtsrepresentatie nodig. Apple documenteert die ingebouwde conformiteit in Een weergave omzetten in een sleepbron.

De verwachte interactie is eenvoudig: sleep Tile A naar het andere paneel, laat de kaart los en sleep hem daarna terug. De volledige CardFace levert de bewegende voorbeeldweergave. De bestemming accepteert precies één overeenkomend token en wijzigt de ene locatievariabele. true retourneren meldt dat de drop is geslaagd; false retourneren wijst de drop af, volgens het gedocumenteerde contract van de bestemmingsactie.

Dit is een compleet voorbeeld voor een macOS-appdoel. Het is hier niet gecompileerd of uitgevoerd, omdat de onderzoeksomgeving geen Swift-toolchain of macOS-runtime heeft.

Houd visuele continuïteit en modeleigenaarschap gescheiden

In het voorbeeld blijft de oorspronkelijke kaart zichtbaar totdat een geaccepteerde drop isOnRight wijzigt. Loslaten buiten de panelen verandert het model niet. Terugplaatsen in het bezette paneel wijst dezelfde locatie opnieuw toe. Een niet-overeenkomend token of meerdere items worden afgewezen voordat de status verandert.

Zo voorkom je dat de kaart bij het starten van het slepen wordt verwijderd en na annulering moet worden teruggezet. De voorbeeldweergave zorgt voor visuele continuïteit; de bestemming beheert de locatieverandering. Pas voor een groter bord hetzelfde patroon toe op een stabiele itemidentifier en één gezaghebbende collectie.

De waardesemantiek van Swift helpt verklaren waarom het doorgeven van een payload het oorspronkelijke model niet automatisch verplaatst. Structuren en strings hebben waardesemantiek; ze worden niet impliciet verplaatst in de collectie van je app wanneer je ze toewijst of doorgeeft. Je modelupdate moet die verplaatsing expliciet vastleggen. Zie Structuren en klassen.

Maak ook onderscheid tussen een verplaatsing op applicatieniveau en de native sleepbewerking. Het voorbeeld wijzigt één gedeelde bordstatus na een geaccepteerde drop; het configureert geen native verplaatsingsonderhandeling en garandeert geen verplaatsingscursor.

Voor macOS 26 en hoger documenteert Apple DragConfiguration om ondersteunde bewerkingen aan te geven en onDragSessionUpdated(_:) om de uitgevoerde bewerking te observeren. Een bestemming kiest welke bewerking ze uitvoert. Als je de bron na een native verplaatsing verwijdert, stem dat dan af op de afhandeling van de bestemming, zodat hetzelfde gedeelde item niet twee keer wordt verwijderd. Zie DragConfiguration en onDragSessionUpdated(_:).

Wanneer moet AppKit de sleepsessie beheren?

Gebruik AppKit wanneer de acceptatiecriteria een expliciet gepositioneerde sleepafbeelding vereisen, bijvoorbeeld als de aanwijzer bij het oppakken op hetzelfde punt binnen de kaart moet blijven. SwiftUI’s gedocumenteerde modifier voor aangepaste voorbeeldweergaven neemt een payload en een weergave voor de voorbeeldweergave aan, en beschrijft een gecentreerde voorbeeldweergave. AppKit biedt rechtstreeks toegang tot het frame van het sleepitem.

Volg deze stappen voor een AppKit-implementatie:

  1. Laat de NSView van de bron de daadwerkelijke muisklik ontvangen en bijhouden of de interactie een sleepactie wordt.
  2. Render de volledige samengestelde kaart naar één NSImage en maak één NSDraggingItem met de bijbehorende pasteboard-writer.
  3. Stel de afbeelding en de beginrechthoek in met setDraggingFrame(_:contents:).
  4. Geef die rechthoek op in het coördinatenstelsel van de weergave die de interactie start en roep vervolgens beginDraggingSession(with:event:source:) aan met de initiërende muisklik en je NSDraggingSource.
  5. Laat de bestemming de drop valideren en vastleggen. Gebruik de voltooiingscallback van de bron om de sessie op te schonen.

Apple geeft aan dat de muislocatie van de initiërende gebeurtenis de offset van het gesleepte pictogram bepaalt en dat het beginframe van elk item de coördinaten van de bronweergave moet gebruiken. Houd de rechthoek van de kaart en de gebeurtenislocatie in hetzelfde coördinatenstelsel. Zie beginDraggingSession(with:event:source:).

setDraggingFrame(_:contents:) is de gedocumenteerde gemaksfunctie voor een sleepitem dat door één afbeelding wordt weergegeven. Een samengestelde kaart kan meerdere getekende elementen bevatten en toch door die ene afbeelding worden weergegeven. Zie setDraggingFrame(_:contents:).

Behoud de grenzen van gebeurtenissen en sessies

Kies één initiator voor de sleepactie binnen het interactieve gebied van de kaart. Vermijd als implementatieregel dat je een AppKit-sessie start boven op een actieve SwiftUI-bron met draggable voor datzelfde gebied. Beide zouden de interactie proberen te beheren. Maak geen muisgebeurtenissen aan om onduidelijk eigenaarschap van gebaren te compenseren.

Je kunt de AppKit-bron inbedden met NSViewRepresentable. Houd de native gebeurtenisafhandeling in de bijbehorende NSView en geef geaccepteerde modelwijzigingen door via bindings of een coordinator. SwiftUI beheert de lay-out van de weergegeven view; Apple zegt uitdrukkelijk dat je frame of bounds van die beheerde view niet rechtstreeks mag instellen. De afbeeldingsrechthoek van een sleepitem is een aparte kwestie. Zie NSViewRepresentable.

Bewaar een NSDraggingItem niet om het later aan te passen. Apple documenteert de beperkte levensduur ervan: wijzig sleepitems na het aanmaken van de sessie via sessie-enumeratie en gebruik elk opgesomd item alleen binnen de callback. Wijzigingen aan de bestemming kunnen wijzigingen aan de afbeelding van de bron ook overschrijven wanneer de sleepactie boven die bestemming plaatsvindt. Zie NSDraggingItem en enumerateDraggingItems(options:for:classes:searchOptions:using:).

Hoe stel je vast waarom een voorbeeldweergave er nog steeds verkeerd uitziet?

Controleer de fout bij de stap waar die optreedt:

Symptoom Eerste controle
Alleen de cirkel of tekst wordt verplaatst Plaats één sleepbron op de volledige kaartcontainer.
De afmetingen van de voorbeeldweergave wijken af van die van de kaart Gebruik dezelfde presentatieweergave en expliciete afmetingen opnieuw.
De omtrek bij het oppakken wijkt af van de bewegende afbeelding Controleer .dragPreview los van de closure voor de aangepaste voorbeeldweergave.
Lege ruimte start onverwacht het slepen Controleer .interaction en de grenzen van de broncontainer.
De AppKit-afbeelding springt bij het oppakken Controleer de initiërende muisklik en de coördinaten van de bronweergave.
Latere wijzigingen aan de AppKit-afbeelding hebben geen effect Voer ze uit binnen sessie-enumeratie.
De kaart verdwijnt na annulering Verplaats modelmutaties naar de afhandeling van geaccepteerde drops en controleer de opruiming.

Controleer afzonderlijk annulering, een geaccepteerde drop, een drop terug naar het oorspronkelijke paneel en een afgewezen payload. De belangrijke invariant is dat één modellocatie gedurende de hele actie gezaghebbend blijft. Begin met de volledige SwiftUI-voorbeeldweergave en gebruik pas daarna een AppKit-bron als de vereiste plaatsing van de afbeelding expliciete instellingen voor het frame en de gebeurtenis vereist.

Bronnen