#Where the SwiftUI view lives
A Mac Dynamic Island is two problems stacked: an AppKit window that has no business existing, and a SwiftUI view that has to feel alive inside it. I covered the window in the architecture guide: a borderless, non-activating NSPanel pinned under the notch. This post is the other half, the dynamic island swiftui code that makes the thing stretch, morph, and settle like hardware.
The seam between the two is one line. The panel hosts a single SwiftUI root through an NSHostingView, and from there everything is declarative:
// The island's SwiftUI root, hosted inside the NSPanel
let host = NSHostingView(rootView: IslandView())
panel.contentView = hostEverything below assumes that IslandView is the root. I keep the AppKit side deliberately dumb, no manual frame animation, no CALayer work. Width, height, corner radius, and content all animate inside SwiftUI, because one animation system driving every property is the only way the stretch stays coherent.
#An animatable squircle shape
The silhouette is where most clones fall down. A plain RoundedRectangle reads as a floating badge because the real cutout uses continuous, squircle curvature, not a circular arc. SwiftUI ships the reference for that curve as RoundedRectangle(cornerRadius:style:.continuous), where the curvature ramps in gradually and the corner reaches out about 1.53 times its radius along each edge before it meets the straight side.
You could stop at that built-in shape, but the notch is not symmetric: its top corners are tighter than its bottom (I measured roughly 4 pt versus 8 pt in the notch dimensions post). RoundedRectangle can't give the top and bottom different radii, so you draw your own Shape. Each corner becomes three cubic Bézier segments, which is how a continuous corner keeps its curvature smooth instead of snapping from straight to arc:
struct NotchShape: Shape {
var top: CGFloat // top corner radius, ~4 pt at rest
var bottom: CGFloat // bottom corner radius, ~8 pt at rest
// spring both radii by making them animatable
var animatableData: AnimatablePair<CGFloat, CGFloat> {
get { AnimatablePair(top, bottom) }
set { top = newValue.first; bottom = newValue.second }
}
func path(in rect: CGRect) -> Path {
var p = Path()
// Each corner is a continuous (squircle) curve, not one arc:
// curvature ramps in over ~1.53x the radius along each edge,
// drawn as three cubic Bezier segments so the join stays smooth.
// ...build the four corners from `top` and `bottom`...
return p
}
}The load-bearing part is animatableData. Expose the two radii through it and SwiftUI will interpolate them frame by frame, so when a spring drives the shape open the corners round out on the same curve as the size. Forget it and the radius jumps in one step while everything else glides, which is exactly the tell that reads as fake. We just retuned this on NotchBay, and the difference between a corner that springs and a corner that snaps is the whole illusion.
#Springs that stretch, not slide
The island's signature is the stretch: a compact pill snaps wide when a call lands, then settles with a hint of overshoot. That overshoot is not decoration, it's the cue the eye reads as physics, so drive the change with a spring, never a linear or ease curve:
withAnimation(.spring(duration: 0.42, bounce: 0.3)) {
expanded.toggle()
}Modern SwiftUI springs take duration and bounce, which is far easier to reason about than the old response and damping pair. Duration is roughly how long the motion takes; bounce is how much it overshoots, from 0 (no overshoot) up. I settled around 0.3 bounce for NotchBay: enough spring that the card feels supple, not so much that it wobbles like a toy. Above 0.4 it starts to look nervous.
The second rule matters just as much: scale content in and out, don't translate it. A common mistake is to slide the expanded content down from behind the notch, which reads as a curtain dropping rather than the island growing:
// Right: the detail grows out of the pill detail .scaleEffect(expanded ? 1 : 0.4, anchor: .top) .opacity(expanded ? 1 : 0) // Wrong: it slides down from behind the notch (a curtain) // detail.offset(y: expanded ? 0 : -80)
Anchor the scale to the top so the content appears to emerge from the pill, fade opacity alongside it, and let the same spring carry the size. Grown-from, not slid-in, is the difference between a live activity and a dropdown menu.
#Morph the chip into the card
When the island expands, the icon and title shouldn't disappear and reappear, they should travel from their spot on the compact chip to their new spot on the expanded card. matchedGeometryEffect is built for exactly this. Give both layouts a shared namespace and tag the shared elements with the same id:
@Namespace private var island
if expanded {
ExpandedCard {
Image(systemName: icon).matchedGeometryEffect(id: "icon", in: island)
Text(title).matchedGeometryEffect(id: "title", in: island)
}
} else {
CompactChip {
Image(systemName: icon).matchedGeometryEffect(id: "icon", in: island)
Text(title).matchedGeometryEffect(id: "title", in: island)
}
}SwiftUI treats the two tagged views as one element in two positions and interpolates frame, size, and position between them for you. Wrap the swap in the same spring and the icon glides and scales into its card position while the shape stretches around it. The trick is that the compact and expanded trees are genuinely different views; matchedGeometryEffect is what stitches them into one continuous motion instead of a hard cut.
One caution from shipping this: keep the tagged elements simple. A single Image or Text morphs cleanly. Try to match a whole stack with nested layout and the interpolation can look rubbery, so morph the anchors and let the surrounding chrome cross-fade.
#Wiring it together, and what to tune first
Put together, IslandView is a ZStack: the NotchShape as the background, the compact or expanded content on top, all switched by one @State flag inside a single withAnimation. The shape springs its radii, the size springs its frame, the content scales and morphs, and the panel underneath never moves.
If you're building your own, tune in this order. First the resting shape, until the pill's top edge hugs the hardware and you can't tell where metal ends and pixels begin. Then the spring, one number at a time, duration then bounce, on a real expansion you trigger over and over. Then the morph, last, because it only looks right once the shape and spring are already honest. I spent more time on the corner curve and the bounce value than on any feature logic, and that ratio is correct: the motion is the product.
If you'd rather see the finished behavior before writing any of it, the Dynamic Island for Mac overview walks the category, and NotchBay itself is the reference I tuned all of this against.
#Frequently asked questions
How do I draw the Dynamic Island shape in SwiftUI?
Build a custom Shape rather than a plain RoundedRectangle. The real notch uses continuous, squircle curvature (the corner reaches about 1.53 times its radius along each edge) and has tighter top corners than bottom, so you draw each corner as three cubic Bezier segments and expose the radii through animatableData.
Why not just use RoundedRectangle with a continuous style?
It is the right curve, but it forces one radius on all four corners. The Mac cutout's top corners are tighter than its bottom, so a symmetric rounded rectangle looks subtly off. A custom Shape lets you set top and bottom independently and still animate them.
How do I make the corner radius spring?
Put the radius, or a pair of radii, behind the Shape's animatableData. SwiftUI then interpolates it every frame, so a spring that changes the size rounds the corners on the same curve. Without animatableData the radius jumps in one step while the size glides, which reads as fake.
What spring settings feel right for the stretch?
Use .spring(duration:bounce:). Bounce controls overshoot; around 0.3 gives a supple stretch, while above roughly 0.4 it starts to wobble. Tune duration and bounce one at a time on a real expansion, not in isolation.
Should the expanded content slide in or scale in?
Scale it in, anchored to the top, with opacity fading alongside. Translating the content down from behind the notch reads as a curtain dropping rather than the island growing out of the pill.
How does matchedGeometryEffect morph the chip into the card?
Give the compact and expanded layouts a shared Namespace and tag the icon and title with the same id in both. SwiftUI treats them as one element in two positions and interpolates between them, so the shared pieces travel smoothly while the rest cross-fades.