Houd de vastgelegde positie van de vorm ongewijzigd terwijl SwiftUI de versleepbeweging vooraf weergeeft en leg daarna de uiteindelijke verplaatsing vast in onEnded. Zo blijft de beginpositie behouden tot de bewerking is voltooid, zonder afhankelijk te zijn van de timing van AppKit voor het loslaten van de muisknop. Hier volgt een compleet voorbeeld voor het verplaatsen van een rechthoek in je eigen Mac-app.

Welke voltooiingsgrens moet eigenaar zijn van de beginpositie?

Bij een interactie waarbij een vorm wordt verplaatst, is de concrete taak om elke voorvertoning en de uiteindelijke positie te berekenen op basis van dezelfde beginpositie. De bruikbare voltooiingsgrens is het succesvol voltooien van het gebaar van die vorm. Apple documenteert dat onEnded(_:) de uiteindelijke waarde van het gebaar aanlevert en alleen wordt uitgevoerd wanneer het gebaar succesvol eindigt. Apple beveelt @GestureState aan voor tijdelijke status die moet worden gereset, ongeacht hoe het gebaar eindigt. De documentatie van Apple over onEnded(_:).

Geef de interactie twee soorten status: een vastgelegde modelpositie en een tijdelijke verschuiving voor de voorvertoning. Toon tijdens het slepen de som daarvan. Pas bij succesvolle voltooiing de uiteindelijke verschuiving één keer toe op de vastgelegde positie. Dit ontwerp is afgeleid van Apples onderscheid tussen tijdelijke gebaarupdates en permanente statusupdates. Interactiviteit toevoegen met gebaren.

Informatie Eigenaar in dit voorbeeld Levensduur
Vastgelegd middelpunt van de rechthoek Privémodel met @State Blijft behouden na voltooiing van het gebaar
Huidige verschuiving voor de voorvertoning @GestureState Wordt gereset wanneer het gebaar inactief wordt
Uiteindelijke verschuiving Callbackargument onEnded Wordt synchroon gebruikt om de verplaatsing vast te leggen
AppKit-muisloslaatgebeurtenis AppKit-gebeurtenisafhandeling Meldt dat een knop is losgelaten; bepaalt niet wanneer een SwiftUI-gebaar is voltooid

Apple documenteert @State als door SwiftUI beheerde opslag en als bron van waarheid voor waarden in een weergavehiërarchie. @GestureState wordt automatisch gereset wanneer het bijbehorende gebaar inactief wordt. Door de basispositie in de eerste en de voorvertoning in de tweede te bewaren, krijgt elke waarde de levensduur die deze interactie vereist. State, GestureState.

Bouw een compleet voorbeeld voor het slepen van een rechthoek op de Mac

Maak in Xcode een macOS SwiftUI-app met een implementatiedoel van macOS 14 of hoger. Vervang de gegenereerde appdeclaratie en contentweergave door het volgende bestand, en zorg dat het doel maar één @main-declaratie heeft. Het voorbeeld gebruikt één synthetische rechthoek die volledig in het geheugen wordt opgeslagen. De genoemde API's voor coördinatenruimten zijn volgens de documentatie beschikbaar vanaf macOS 14. Initialisator voor sleepgebaren, Benoemde coördinatenruimten.

import SwiftUI

@main
struct GestureStartDemoApp: App {
    var body: some Scene {
        WindowGroup {
            ShapeCanvas()
        }
    }
}

private struct SampleItem {
    var center = CGPoint(x: 160, y: 140)
}

private struct ShapeCanvas: View {
    @State private var item = SampleItem()
    @GestureState private var previewTranslation = CGSize.zero

    private let canvasWidth: CGFloat = 600
    private let canvasHeight: CGFloat = 360
    private let shapeWidth: CGFloat = 80
    private let shapeHeight: CGFloat = 60

    private func projectedCenter(_ translation: CGSize) -> CGPoint {
        let proposedX = item.center.x + translation.width
        let proposedY = item.center.y + translation.height
        let halfWidth = shapeWidth / 2
        let halfHeight = shapeHeight / 2

        return CGPoint(
            x: min(max(proposedX, halfWidth),
                   canvasWidth - halfWidth),
            y: min(max(proposedY, halfHeight),
                   canvasHeight - halfHeight)
        )
    }

    private var moveGesture: some Gesture {
        DragGesture(
            minimumDistance: 3,
            coordinateSpace: .named("shapeCanvas")
        )
        .updating($previewTranslation) { value, translation, _ in
            translation = value.translation
        }
        .onEnded { value in
            let finalCenter = projectedCenter(value.translation)
            item.center = finalCenter
        }
    }

    var body: some View {
        let displayedCenter = projectedCenter(previewTranslation)

        VStack(alignment: .leading, spacing: 12) {
            Text("Drag the blue rectangle")
                .font(.headline)

            ZStack {
                Color.gray.opacity(0.12)

                RoundedRectangle(cornerRadius: 10)
                    .fill(Color.blue)
                    .frame(width: shapeWidth, height: shapeHeight)
                    .contentShape(Rectangle())
                    .gesture(moveGesture)
                    .position(
                        x: displayedCenter.x,
                        y: displayedCenter.y
                    )
            }
            .frame(width: canvasWidth, height: canvasHeight)
            .coordinateSpace(.named("shapeCanvas"))
            .clipped()

            Text(
                "Committed center: " +
                "\(Int(item.center.x)), \(Int(item.center.y))"
            )
            .monospacedDigit()
        }
        .padding(20)
    }
}

De belangrijkste invariant is dat item.center nooit verandert in updating. Deze blijft de modelbasispositie van het gebaar totdat onEnded finalCenter berekent. De uiteindelijke berekening leest value.translation rechtstreeks uit, zodat de eigenschap voor de voorvertoning tijdens de voltooiing niet de laatste waarde hoeft te behouden.

Het gebaar meet de verplaatsing in de benoemde coördinatenruimte van het omringende canvas. De position(x:y:) van de rechthoek plaatst het middelpunt in diezelfde bovenliggende ruimte. Door dezelfde ruimten te gebruiken, worden de lokale coördinaten van een bewegende vorm niet vermengd met canvascoördinaten. Apple documenteert zowel het meten van gebaren in benoemde ruimten als positionering op basis van het middelpunt. coordinateSpace(_:), position(x:y:).

De gedeelde functie projectedCenter houdt de rechthoek ook binnen het canvas. De voorvertoning en de vastlegging gebruiken identieke grenzen: horizontale middelpunten van 40 tot 560 en verticale middelpunten van 30 tot 330. Aan de rand blijft de verplaatsing gebaseerd op de totale verschuiving vanaf het oorspronkelijke begin van de sleepbeweging. Zodra de voorgestelde positie weer binnen de grenzen valt, beweegt de rechthoek van de rand af.

Een verplaatsing die kleiner is dan de ingestelde herkenningsdrempel van 3 punten, wordt niet als sleepbeweging herkend. In dit voorbeeld geldt het begin bewust als het begin van een herkende interactie om een vorm te verplaatsen. Als de modelstatus al bij de fysieke muisdruk moet worden vastgelegd, is dat een andere grens. Initialisator voor sleepgebaren.

Waarom blijft de beginpositie behouden bij de laatste update?

Apple definieert DragGesture.Value.translation als de totale verschuiving vanaf het begin van het gebaar tot de huidige gebeurtenis. Deze is cumulatief en niet de verandering sinds de vorige callback. De gerapporteerde verschuiving bij elke update optellen bij een modelpositie die al is bijgewerkt, zou de verplaatsing dus te groot maken. DragGesture.Value.

Neem de volgende expliciet illustratieve invoerwaarden, die allemaal binnen de canvasgrenzen vallen:

Fase Gerapporteerde totale verschuiving Vastgelegd middelpunt Weergegeven middelpunt
Voor het slepen (0, 0) (160, 140) (160, 140)
Eerste update (12, 5) (160, 140) (172, 145)
Tweede update (30, -10) (160, 140) (190, 130)
Succesvol einde (45, 20) Wordt (205, 160) Komt uit op (205, 160)

Elke voorvertoning gebruikt dezelfde basispositie. Bij voltooiing wordt de uiteindelijke (45, 20) één keer toegepast. Een volgende geslaagde sleepbeweging met de verschuiving (-15, 10) begint vervolgens vanaf (205, 160) en legt (190, 170) vast. Deze berekeningen zijn gecontroleerd met een deterministische Python-berekening. De SwiftUI-applicatie is getoetst aan de opgehaalde API-documentatie, maar is in deze omgeving niet gecompileerd of uitgevoerd.

Vervang startLocation niet door het beginmiddelpunt van de rechthoek. Apple definieert startLocation als de locatie van de eerste gebeurtenis van de sleepbeweging. Een aanwijzer kan vlak bij een hoek van de rechthoek beginnen, dus die locatie kan verschillen van het modelmiddelpunt van de vorm. Ook is predictedEndTranslation een voorspelling op basis van snelheid; gebruik voor deze interactie met directe positionering de uiteindelijke translation. DragGesture.Value.

In dit ontwerp is bij annulering geen terugdraaiing van de vastgelegde positie nodig, omdat updates van de voorvertoning deze nooit overschrijven. Apple documenteert dat de gebaarstatus wordt gereset wanneer het gebaar inactief wordt, terwijl onEnded alleen wordt uitgevoerd na een succesvolle afronding. De tijdelijke verplaatsing verdwijnt dus zonder dat een geannuleerde verplaatsing wordt vastgelegd. Deze conclusie volgt uit die gedocumenteerde gedragingen en vereist geen aanname over de volgorde van de reset en de callback voor succesvolle voltooiing. GestureState, onEnded(_:).

Waarom een AppKit-muisloslaatmonitor de basispositie niet moet wissen

Een lokale AppKit-gebeurtenismonitor ontvangt gebeurtenissen voordat ze naar de app worden doorgestuurd. Als je gedeelde status van het begin van een gebaar wist op basis van een bewaakte muisloslaatgebeurtenis, kan die status dus verdwijnen voordat de verdere afhandeling die gebeurtenis verwerkt. Dit volgt uit de gedocumenteerde grens voor het doorsturen van gebeurtenissen; het is geen garantie van SwiftUI over de volgorde van callbacks. addLocalMonitorForEvents(matching:handler:).

Ook meldt NSResponder.mouseUp(with:) dat de gebruiker de linkermuisknop heeft losgelaten. Het contract ervan verklaart niet dat elke SwiftUI-gebaarcallback of weergave-update al is voltooid. Behandel het waarnemen van ruwe gebeurtenissen en het voltooien van een herkend gebaar als afzonderlijke verantwoordelijkheden. mouseUp(with:).

De API's die hier zijn bekeken, definiëren geen algemene callback die aangeeft dat SwiftUI alle verwerking van een ruwe gebeurtenis heeft voltooid. Een opruimtaak in een wachtrij of een korte timer zou een aanname over de planning toevoegen zonder die relatie vast te stellen. Voor deze taak is de ondersteunde aanpak om de uiteindelijke modelberekening uit te voeren binnen onEnded, waar de uiteindelijke gebaarwaarde beschikbaar is.

Als de interactie in een aangepaste AppKit-weergave moet worden geïmplementeerd, laat die weergave dan de invoerreeks en sessiestatus beheren en integreer haar met NSViewRepresentable. Apple biedt hooks voor het aanmaken, bijwerken, coördineren en verwijderen van die integratie. Bewaar de sessie van de weergegeven weergave in de AppKit-weergave of haar coördinator, in plaats van die bij elke waarde van de representable opnieuw op te bouwen. SwiftUI beheert de lay-out van de weergegeven weergave, dus wijzig niet zelfstandig het beheerde frame of de beheerde grenzen ervan. NSViewRepresentable.

Wanneer vereist de interactie een expliciete momentopname?

De vastgelegde positie in het voorbeeld volstaat, omdat geen andere actie deze wijzigt terwijl het slepen bezig is. Gebruik voor een groter canvas deze beslisondersteuning voordat je een afzonderlijk sessieobject toevoegt:

Vereiste Gevolg voor het ontwerp
Eén item verplaatsen waarvan de vastgelegde positie tijdens de voorvertoning niet verandert Gebruik de getoonde aanpak met basispositie en voorvertoning
Meerdere geselecteerde items tegelijk verplaatsen Bewaar hun stabiele identificatoren en beginposities samen als één momentopname van de interactie
Andere opdrachten toestaan om posities tijdens het slepen te wijzigen Bepaal of je die wijzigingen blokkeert, het slepen annuleert of wijzigingen afhandelt op basis van een modelversie
De status vastleggen bij fysieke muisdruk vóór herkenning Gebruik een expliciet beheerde AppKit-invoergrens; de eerste update van een gebaar is niet hetzelfde
Een afzonderlijke sessie behouden na voltooiing Geef deze een expliciete eigenaar en beëindigingsregel, in plaats van te vertrouwen op tijdelijke gebaarstatus

Een momentopname moet waarden bewaren. Swift-structuren hebben waardesemantiek, terwijl het toewijzen van een klasse-instantie de verwijzing kopieert. Een array met verwijzingen naar veranderlijke itemobjecten bevriest hun posities dus niet vanzelf. Leg voor het verplaatsen van meerdere items de identificatoren en coördinaatwaarden vast die nodig zijn voor de uiteindelijke berekening. Structures and Classes — De programmeertaal Swift.

Als je een permanente sessie toevoegt, definieer dan expliciet wat er gebeurt bij succesvolle voltooiing, annulering en verwijdering van de weergave. Als je deze alleen wist in onEnded, blijft annulering onbehandeld, want die callback geldt alleen bij succes. Gebruik de resettransactie van gebaarstatus niet als algemene melding van voltooiing: Apple documenteert de resetclosure als een manier om een transactie aan te leveren voor het resetten van gebaarstatus. onEnded(_:), init(wrappedValue:reset:).

Voor het gebruikelijke geval waarin een rechthoek wordt verplaatst, behoud je de basispositie door het vastgelegde model tijdens de voorvertoning ongemoeid te laten. Bereken de uiteindelijke positie op basis van de eigen waarde van het succesvol voltooide gebaar, leg deze één keer vast en laat de tijdelijke gebaarstatus resetten. Zo heeft de beginpositie een duidelijke eigenaar en levensduur totdat de berekening waarvoor deze nodig is, is uitgevoerd.

Bronnen