Bereken wanneer het slepen op een canvas eindigt de uiteindelijke geometrie van het item in het coördinatenstelsel van het doel. Controleer eerst volledige insluiting en daarna overlap met positieve oppervlakte. Deze tutorial implementeert die regel voor rechthoekige items in je eigen Mac-app en legt het equivalent in AppKit uit.
Bepaal wat als overlap en insluiting geldt
Neem een eenvoudig desktopcanvas met een verplaatsbare blauwe rechthoek en een vast grijs doel. De taak is te bepalen of het losgelaten item volledig in het doel past, het gedeeltelijk overlapt of er geen oppervlakte mee deelt. Alles in het voorbeeld is synthetisch en wordt in het geheugen opgeslagen.
Insluiting is directioneel: het doel moet het item bevatten. Apple documenteert dat CGRectContainsRect(target, item) true retourneert wanneer de vereniging van de rechthoeken gelijk is aan het doel. Dit omvat een item dat precies overeenkomt met het doel. Controleer eerst op insluiting om te voorkomen dat een volledig ingesloten item als gedeeltelijke overlap wordt gerapporteerd. Documentatie over CGRectContainsRect
In dit voorbeeld vereist overlap een gedeeld gebied met zowel positieve breedte als positieve hoogte. Alleen een rand of hoek raken geldt als Geen oppervlakte-overlap. Bereken de doorsnede en controleer de afmetingen ervan, zodat het gedrag bij contact expliciet is. Apple documenteert dat CGRectIntersection de gedeelde rechthoek retourneert, of een null-rechthoek als er geen doorsnede is. Documentatie over CGRectIntersection
| Resultaat | Regel in deze tutorial | Voorbeeld |
|---|---|---|
Ingesloten |
Het doel bevat het volledige item, ook als de grenzen samenvallen | Een item past in het doel of is er precies even groot als |
Overlap |
Insluiting geldt niet, maar het gedeelde gebied heeft positieve breedte en hoogte | Het item steekt over de rand van het doel |
Geen oppervlakte-overlap |
Er is geen doorsnede met positieve oppervlakte | Het item staat los of raakt alleen een rand |
Ongeldige geometrie |
Een van beide rechthoeken heeft niet-eindige coördinaten of niet-positieve afmetingen | Een item zonder breedte |
Dit zijn toepassingsregels die zijn afgeleid van de geometrische bewerkingen. Als je interface een marge binnen het doel vereist, definieer die marge dan afzonderlijk en controleer een kleinere acceptatierechthoek. Zorg dat het zichtbare doel en de acceptatieregel begrijpelijk zijn voor degene die het item versleept.
Gebruik één coördinatenstelsel en de uiteindelijke sleepverplaatsing
De rechthoek die je tekent en de rechthoek die je classificeert, moeten dezelfde positie beschrijven. In dit voorbeeld gebruiken zowel het item als het doel canvascoördinaten. Het canvas krijgt een benoemde coördinatenruimte en de DragGesture van het item gebruikt dezelfde naam. Apple's huidige modifier coordinateSpace(_:) ondersteunt dit patroon op macOS 14 en hoger. Documentatie over benoemde coördinatenruimtes
Sla de vastgelegde rechthoek van het item afzonderlijk op van de tijdelijke sleepverplaatsing. Teken tijdens het slepen de vastgelegde rechthoek verschoven met die verplaatsing. Bereken bij succesvolle voltooiing die uiteindelijke rechthoek opnieuw aan de hand van de waarde uit de callback, classificeer hem en leg hem één keer vast.
Apple definieert translation als de totale verplaatsing vanaf het begin van het slepen tot aan de huidige gebeurtenis. Als je de volledige verplaatsing telkens optelt bij een positie die al is bijgewerkt, wordt de beweging dus te groot. Pas de verplaatsing toe op de ongewijzigde vastgelegde positie. Documentatie over sleepverplaatsing
Het loslaatpunt alleen kan niet bepalen of het volledige item in een doel past. Iemand kan een rechthoek vlak bij een hoek vastpakken, waardoor een groot deel erbuiten blijft, zelfs als de aanwijzer binnen het doel staat. Door de verplaatsing toe te passen, blijft ook de oorspronkelijke afstand tussen het aangrijppunt en het item behouden, zonder het midden van het item naar de aanwijzer te verplaatsen.
Bouw een compleet SwiftUI-voorbeeld met rechthoeken
Maak in Xcode een macOS SwiftUI-app met als minimale implementatieversie macOS 14 of hoger. Vervang het gegenereerde app-startpunt door de twee onderstaande blokken, direct achter elkaar in één Swift-bestand. Verwijder het oorspronkelijke app-startpunt zodat het doel maar één @main-declaratie bevat. Samen vormen de twee blokken het volledige programma.
Het eerste blok definieert de geometrieclassificatie. Het verwerpt ongeldige afmetingen expliciet in plaats van ze stilzwijgend in een andere rechthoek om te zetten. Een Swift-enumeratie houdt de vier mogelijke resultaten gescheiden; de ruwe tekenreekswaarden worden gebruikt voor de getoonde berichten. Documentatie over Swift-enumeraties
import SwiftUI
import CoreGraphics
enum Placement: String {
case contained = "Contained"
case overlap = "Overlap"
case separate = "No area overlap"
case invalid = "Invalid geometry"
}
func isUsable(_ rect: CGRect) -> Bool {
let values = [rect.origin.x, rect.origin.y,
rect.width, rect.height,
rect.maxX, rect.maxY]
return values.allSatisfy { $0.isFinite }
&& rect.width > 0 && rect.height > 0
}
func classify(item: CGRect, target: CGRect) -> Placement {
guard isUsable(item), isUsable(target) else {
return .invalid
}
if CGRectContainsRect(target, item) {
return .contained
}
let shared = CGRectIntersection(item, target)
if !shared.isNull && shared.width > 0 && shared.height > 0 {
return .overlap
}
return .separate
}
Het tweede blok bevat de app en het canvas. Koppel het gebaar aan de verplaatsbare rechthoek, zoals beschreven in Apple's documentatie over DragGesture. Door het canvas een vaste grootte te geven, blijft de geometrie van dit voorbeeld expliciet. Documentatie over DragGesture
@main
struct RectanglePlacementApp: App {
var body: some Scene {
WindowGroup {
PlacementCanvas()
}
}
}
struct PlacementCanvas: View {
private let target = CGRect(x: 200, y: 100,
width: 200, height: 160)
@State private var item = CGRect(x: 40, y: 120,
width: 80, height: 60)
@State private var message = "Drag the blue item, then release."
@GestureState private var translation: CGSize = .zero
private var preview: CGRect {
item.offsetBy(dx: translation.width,
dy: translation.height)
}
private var move: some Gesture {
DragGesture(minimumDistance: 1,
coordinateSpace: .named("placementCanvas"))
.updating($translation) { value, state, _ in
state = value.translation
}
.onEnded { value in
let finalItem = item.offsetBy(
dx: value.translation.width,
dy: value.translation.height
)
let result = classify(item: finalItem, target: target)
message = result.rawValue
if result != .invalid {
item = finalItem
}
}
}
var body: some View {
VStack(alignment: .leading, spacing: 12) {
Text(message)
ZStack(alignment: .topLeading) {
Color.white
Rectangle()
.fill(Color.gray.opacity(0.25))
.frame(width: target.width, height: target.height)
.position(x: target.midX, y: target.midY)
Rectangle()
.fill(Color.blue)
.frame(width: item.width, height: item.height)
.position(x: preview.midX, y: preview.midY)
.gesture(move)
}
.frame(width: 640, height: 380)
.coordinateSpace(.named("placementCanvas"))
Button("Reset") {
item = CGRect(x: 40, y: 120, width: 80, height: 60)
message = "Drag the blue item, then release."
}
}
.padding()
}
}
Sleep het blauwe item over het grijze doel en laat het los. Het bericht legt de laatst voltooide plaatsing vast. Gedeeltelijke overlap en losse plaatsingen blijven op de plek staan waar je ze hebt losgelaten; insluiting verandert niet automatisch de bovenliggende structuur en laat het item niet naar een andere container springen. De knop Reset herstelt de oorspronkelijke rechthoek.
De callback voor updates van het gebaar werkt de positie bewust niet blijvend bij. @GestureState bevat alleen de voorbeeldverplaatsing en wordt gereset wanneer het gebaar inactief wordt. Apple specificeert dat onEnded alleen wordt uitgevoerd na succesvolle voltooiing en beveelt gesture state aan voor waarden die moeten worden gereset, ongeacht hoe het gebaar eindigt. Daardoor kan een onderbroken gebaar de voorbeeldweergave weggooien en tegelijk de vastgelegde rechthoek en het vorige resultaat behouden. Documentatie over GestureState, Documentatie over onEnded
Deze implementatie beschouwt de gevulde rechthoeken als de geometrie. Een decoratieve schaduw of omlijning vergroot de acceptatierechthoek niet. Als je schalen, zoomen of roteren toevoegt, werk dan eerst het geometrische model en de coördinatenomzetting bij voordat je op dezelfde classifier vertrouwt.
Controleer grensgevallen voordat je meer items toevoegt
Gebruik de volgende synthetische gevallen met het doel uit het voorbeeld: oorsprong (200, 100), breedte 200, hoogte 160. De coördinaten beschrijven itemrechthoeken in dezelfde canvasruimte.
| Oorsprong van item | Afmetingen van item | Verwacht resultaat | Reden |
|---|---|---|---|
(220, 120) |
80 × 60 |
Ingesloten |
Alle randen vallen binnen het doel |
(360, 120) |
80 × 60 |
Overlap |
Het item steekt voorbij de rechterrand van het doel |
(400, 120) |
80 × 60 |
Geen oppervlakte-overlap |
Alleen de verticale randen raken elkaar |
(401, 120) |
80 × 60 |
Geen oppervlakte-overlap |
Er zit een opening tussen de rechthoeken |
(200, 100) |
200 × 160 |
Ingesloten |
Gelijke rechthoeken voldoen aan de voorwaarde voor insluiting |
(180, 80) |
240 × 200 |
Overlap |
Het item omsluit het doel; de richting van de insluiting is omgekeerd |
De berekeningen in de tabel zijn gecontroleerd met een onafhankelijke deterministische berekening. De Swift-code is nagekeken, maar niet gecompileerd in de onderzoeksomgeving, waar geen Swift-compiler beschikbaar was. Bouw de code in het macOS-appdoel en probeer loslaatposities dicht bij elke grens uit.
Behoud bij meerdere items in het geheugen de identiteit van het item dat is geselecteerd toen het slepen begon. Vergelijk bij voltooiing de uiteindelijke geometrie ervan met geschikte doelen en sluit het item zelf uit. Als meerdere doelen in aanmerking komen, definieer dan een stabiele selectieregel, zoals een expliciete volgorde van doelen. De geometrieclassificatie bepaalt op zichzelf niet welk doel eigenaar van het item moet worden.
Pas dezelfde geometrische regel toe in een AppKit-canvas
Houd de classifier voor een bestaande aangepaste AppKit-view onafhankelijk van de gebeurtenisafhandeling. mouseUp(with:) meldt het loslaten van de linkermuisknop; gebruik dit om een sleepbewerking af te ronden die je canvas al beheert. Een niet-gerelateerde muisknop-los-gebeurtenis mag geen plaatsing activeren. Documentatie over muisknop loslaten in AppKit
Een praktische volgorde van gebeurtenissen is:
- Selecteer in
mouseDown(with:)een synthetisch item met de bestaande hit-testingregel van het canvas. Sla de identiteit, de beginrechthoek en de beginpositie van de aanwijzer op. - Zet de locatie van elke relevante muisgebeurtenis om naar canvascoördinaten. Voor een view die aan het venster is gekoppeld, is de omzettingsexpressie
convert(event.locationInWindow, from: nil). - Leid in
mouseDragged(with:)een voorbeeldweergave af van de opgeslagen rechthoek en de verplaatsing van de aanwijzer. - Bereken in
mouseUp(with:)de verplaatsing opnieuw met de loslaatgebeurtenis, classificeer de uiteindelijke rechthoek, leg die vervolgens één keer vast en wis de actieve sleepbewerking.
Apple documenteert locationInWindow als de locatie van een muisgebeurtenis in venstercoördinaten. De puntconversieoverload van NSView.convert(_:from:) converteert vanuit venstercoördinaten wanneer het viewargument nil is. Zo voorkom je dat je een punt in venstercoördinaten vergelijkt met een rechthoek in lokale canvascoördinaten. Documentatie over gebeurtenislocaties, Documentatie over puntconversie
Laat de gebeurtenisafhandeling bij het onderdeel dat de interactie al beheert. Apple maakt onderscheid tussen een directe NSView-subklasse die een gebeurtenis afhandelt en een subklasse die niet-verwerkte gebeurtenissen aan de bovenliggende view moet doorgeven. Volg dat gedocumenteerde respondergedrag; plaatsingslogica toevoegen rechtvaardigt niet dat je de reeks gebeurtenissen van een andere view onderschept of dezelfde loslaatgebeurtenis via twee onafhankelijke handlers verwerkt. Richtlijnen voor gebeurtenisafhandeling in NSView
Weet wanneer rechthoeken geen antwoord geven op een vormvraag
Het voorbeeld geeft een exact antwoord voor gevulde, assenparallelle rechthoeken. Voor cirkels, concave contouren, uitsparingen of gedraaide vormen bieden omhullende rechthoeken alleen een eerste geometrische controle. Hun gedeelde gebied kan ruimte bevatten die geen van beide zichtbare vormen opvult.
Een test of een aanwijzer binnen een pad valt, kan evenmin volledige insluiting van een andere vorm vaststellen. AppKit's NSBezierPath.contains(_:) accepteert één punt in het coördinatenstelsel van het pad, en Apple geeft aan dat deze methode geen rekening houdt met de lijndikte. Het testen van het midden van een item bepaalt dus alleen of dat midden binnen het pad ligt. Documentatie over puntinsluiting in NSBezierPath
Voor gevulde paden op macOS 14 en hoger documenteert SwiftUI ondersteunde booleaanse bewerkingen: Path.intersection(_:eoFill:) levert het gemeenschappelijke gevulde gebied op en Path.subtracting(_:eoFill:) levert het deel van het eerste gevulde gebied buiten het tweede op. Deze bewerkingen bieden een padgebaseerde manier om overlap en het deel van een item buiten een doel te onderzoeken. Lijn beide paden uit in hetzelfde coördinatenstelsel en gebruik de vulregel die overeenkomt met je weergave. Documentatie over paddoorsnede, Documentatie over padaftrekking
Deze bewerkingen gelden voor gevulde gebieden en gaan ervan uit dat niet-gesloten subpaden gesloten zijn. Een getekende lijn of een toepassingsregel over contact tussen randen vereist een eigen geometrische definitie. Gebruik de rechthoekige classifier voor rechthoekige items; breid de geometrielaag bewust uit wanneer het canvas andere vormen begint te tekenen.
Een betrouwbaar resultaat bij het loslaten volgt uit drie concrete keuzes: behoud de startgeometrie van het slepen, bereken het uiteindelijke item in het coördinatenstelsel van het doel en pas een expliciet beleid voor insluiting en overlap toe. Met die keuzes vast kan de interface het resultaat consistent melden, zonder geometrie te koppelen aan gebeurtenisroutering of eigenaarschap van items.
Bronnen
- CGRectContainsRect(::) — Apple
- CGRectIntersection(::) — Apple
- coordinateSpace(_:) — Apple
- translation — Apple
- Enumeraties — Swift.org
- DragGesture — Apple
- GestureState — Apple
- onEnded(_:) — Apple
- mouseUp(with:) — Apple
- locationInWindow — Apple
- convert(_:from:) — Apple
- NSView — Apple
- contains(_:) — Apple
- intersection(_:eoFill:) — Apple
- subtracting(_:eoFill:) — Apple