When a canvas drag ends, calculate the item’s final geometry in the target’s coordinate space. Test full containment first, then positive-area overlap. This tutorial implements that rule for rectangular items in your own Mac app and explains the AppKit equivalent.
Define what counts as overlap and containment
Consider a simple desktop canvas with a movable blue rectangle and a fixed gray target. The reader’s task is to report whether the released item fits entirely inside the target, overlaps it partially, or shares no area with it. Everything in the example is synthetic and stored in memory.
Containment is directional: the target must contain the item. Apple documents that CGRectContainsRect(target, item) returns true when the union of the rectangles equals the target. This includes an item that exactly matches the target. Checking containment first prevents a fully contained item from being reported as partial overlap. CGRectContainsRect documentation
For this example, overlap requires a shared region with positive width and positive height. Merely touching an edge or corner counts as No area overlap. Calculate the intersection and inspect its dimensions rather than leaving contact behavior implicit. Apple documents that CGRectIntersection returns the shared rectangle, or a null rectangle when there is no intersection. CGRectIntersection documentation
| Result | Rule used in this tutorial | Example |
|---|---|---|
Contained |
The target contains the complete item, including coincident boundaries | An item fits inside or exactly matches the target |
Overlap |
Containment fails, but the shared region has positive width and height | The item crosses the target’s edge |
No area overlap |
There is no positive-area intersection | The item is separate or only touches an edge |
Invalid geometry |
Either rectangle has nonfinite coordinates or nonpositive dimensions | A zero-width item |
These are application rules derived from the geometry operations. If your interface requires a margin inside the target, define that margin separately and test against a smaller acceptance rectangle. Keep the visible target and acceptance rule understandable to the person dragging the item.
Use one coordinate space and the final drag translation
The rectangle you draw and the rectangle you classify must describe the same position. In this example, both item and target use canvas coordinates. The canvas receives a named coordinate space, and the item’s DragGesture uses that same name. Apple’s current coordinateSpace(_:) modifier supports this pattern on macOS 14 and later. Named coordinate-space documentation
Store the item’s committed rectangle separately from its temporary drag translation. During the gesture, draw the committed rectangle offset by the translation. On successful completion, compute that final rectangle again from the callback’s value, classify it, and commit it once.
Apple defines translation as the total displacement from the start of the drag to the current event. Therefore, repeatedly adding the entire translation to an already updated position would accumulate too much movement. Apply it to the unchanged committed position instead. Drag translation documentation
The release point alone cannot answer whether the complete item fits inside a target. A person can grab a rectangle near one corner, leaving much of it outside even while the pointer is inside. Applying translation also preserves the original grab offset without moving the item’s center to the pointer.
Build a complete SwiftUI rectangle example
Create a macOS SwiftUI app in Xcode with a deployment target of macOS 14 or later. Replace the generated app entry point with the two blocks below, placed consecutively in one Swift file. Remove the original app entry point so the target contains only one @main declaration. The two blocks together form the complete program.
The first block defines the geometry classifier. It rejects invalid dimensions explicitly rather than silently turning them into a different rectangle. A Swift enumeration keeps the four possible results distinct; its string raw values provide the displayed messages. Swift enumeration documentation
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
}
The second block supplies the app and canvas. Attach the gesture to the movable rectangle, as Apple’s DragGesture documentation describes. The fixed canvas size keeps this example’s geometry explicit. DragGesture documentation
@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()
}
}
Drag the blue item over the gray target and release it. The message records the last completed placement. Partial overlap and separate placements remain where you released them; containment does not automatically change parentage or snap the item into a different container. The Reset button restores the starting rectangle.
There is deliberately no persistent position update in the gesture’s update callback. @GestureState holds only the preview translation and resets when the gesture becomes inactive. Apple specifies that onEnded runs only after successful completion, and recommends gesture state for values that must reset regardless of how the gesture ends. Consequently, an interrupted gesture can discard the preview while preserving the committed rectangle and previous result. GestureState documentation, onEnded documentation
This implementation treats the filled rectangles as the geometry. Adding a decorative shadow or outline does not expand the acceptance rectangle. If you introduce resizing, zooming, or rotation, update the geometry model and coordinate conversion before relying on the same classifier.
Check boundary cases before adding more items
Use the following synthetic cases with the example’s target: origin (200, 100), width 200, height 160. Coordinates describe item rectangles in the same canvas space.
| Item origin | Item size | Expected result | Reason |
|---|---|---|---|
(220, 120) |
80 × 60 |
Contained |
Every edge is within the target |
(360, 120) |
80 × 60 |
Overlap |
The item extends past the target’s right edge |
(400, 120) |
80 × 60 |
No area overlap |
Only the vertical edges touch |
(401, 120) |
80 × 60 |
No area overlap |
A gap separates the rectangles |
(200, 100) |
200 × 160 |
Contained |
Equal rectangles satisfy containment |
(180, 80) |
240 × 200 |
Overlap |
The item encloses the target, reversing containment |
The table’s arithmetic was checked with an independent deterministic calculation. The Swift program was inspected but not compiled in the research environment, which did not provide a Swift compiler. Build it in the macOS app target and exercise release positions near each boundary.
For multiple in-memory items, preserve the identity of the item selected when dragging begins. At completion, compare its final geometry with eligible targets and exclude the item itself. If several targets qualify, define a stable selection rule, such as an explicit target order. Geometry classification alone does not decide which target should own the item.
Apply the same geometry rule in an AppKit canvas
For an existing custom AppKit view, keep the classifier independent of event delivery. mouseUp(with:) reports release of the left mouse button; use it to finalize only a drag that your canvas already owns. An unrelated mouse-up should not trigger placement. AppKit mouse-up documentation
A practical event sequence is:
- In
mouseDown(with:), select a synthetic item using the canvas’s existing hit-testing rule. Save its identity, starting rectangle, and starting pointer position. - Convert each relevant mouse-event location into canvas coordinates. For a view attached to its window, the conversion expression is
convert(event.locationInWindow, from: nil). - In
mouseDragged(with:), derive a preview from the saved rectangle and the pointer displacement. - In
mouseUp(with:), recompute displacement using the release event, classify the final rectangle, then commit it once and clear the active drag.
Apple documents locationInWindow as a mouse event’s location in window coordinates. The point-conversion overload of NSView.convert(_:from:) converts from window coordinates when its view argument is nil. This avoids comparing a window point with a rectangle expressed in the canvas’s local coordinates. Event location documentation, Point conversion documentation
Keep event handling with the component that already owns the interaction. Apple distinguishes a direct NSView subclass that handles an event from a subclass that must let its parent handle unconsumed events. Follow that documented responder behavior; adding placement logic does not justify intercepting another view’s sequence or processing the same release through two independent handlers. NSView event-handling guidance
Know when rectangles cannot answer a shape question
The example gives an exact answer for axis-aligned filled rectangles. For circles, concave outlines, holes, or rotated shapes, bounding rectangles provide only a preliminary geometry test. Their shared area can include space that neither visible shape fills.
A pointer-inside-path test also cannot establish full containment of another shape. AppKit’s NSBezierPath.contains(_:) accepts a single point in the path’s coordinate system, and Apple states that it does not account for stroke width. Testing an item’s center therefore answers only whether that center is inside the path. NSBezierPath point-containment documentation
For filled paths on macOS 14 and later, SwiftUI documents supported boolean operations: Path.intersection(_:eoFill:) produces the common filled region, and Path.subtracting(_:eoFill:) produces the portion of the first filled region outside the second. These operations provide a path-based route for investigating overlap and the portion of an item outside a target. Align both paths in the same coordinate space and use the fill rule that matches your rendering. Path intersection documentation, Path subtraction documentation
Those operations concern filled regions and assume unclosed subpaths are closed. A stroked line or an application rule about boundary contact needs its own geometry definition. Keep the rectangular classifier for rectangular items; extend the geometry layer deliberately when the canvas begins drawing other shapes.
A dependable release result follows from three concrete choices: preserve the drag’s starting geometry, calculate the final item in the target’s coordinate space, and apply an explicit containment-and-overlap policy. With those choices fixed, the interface can report the result consistently without coupling geometry to event routing or item ownership.
Sources
- CGRectContainsRect(::) — Apple
- CGRectIntersection(::) — Apple
- coordinateSpace(_:) — Apple
- translation — Apple
- Enumerations — 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