The contentShape view modifier changes the shape a view uses for interactions: hit-testing,
drag previews, context menu previews, and the iPadOS hover effect. It never changes how the view
renders; that stays with clipShape.
Scoping a Shape to an Interaction
Pass a ContentShapeKinds value plus any Shape. Here the hover effect becomes a circle while
the view still draws as a rectangle.
struct ContentView: View {
var body: some View {
VStack {
Image(systemName: "star")
Text("Hello World!")
}
.contentShape(.hoverEffect, Circle())
.hoverEffect()
.onTapGesture {
print("Super star!")
}
}
}
The available kinds:
interactionsets the hit-testing shape.dragPreviewsets the outline for drag and drop previews.contextMenuPreviewsets the shape of context menu previews.hoverEffectsets the iPadOS pointer hover shape.
ContentShapeKinds is an OptionSet, so one call can cover several interactions:
.contentShape([.hoverEffect, .dragPreview], Circle())
Custom Shapes
Any Shape works, including your own paths. A custom triangle clips the drag preview without
touching the rendered view.
struct Triangle: Shape {
func path(in rect: CGRect) -> Path {
var path = Path()
path.move(to: CGPoint(x: rect.minX, y: rect.maxY))
path.addLine(to: CGPoint(x: rect.midX, y: rect.minY))
path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY))
path.addLine(to: CGPoint(x: rect.minX, y: rect.maxY))
return path
}
}
VStack {
Image(systemName: "star")
Text("Hello World!")
}
.contentShape(.dragPreview, Triangle())
Keep the two modifiers straight: contentShape is for interaction geometry, clipShape is for
rendering. A stack with spacing also needs contentShape(Rectangle()) before a tap gesture if
the gaps between children should be tappable.