Driving SwiftUI animations with the hinge angle on iPhone Duo
In my previous posts about iPhone Duo, I've been looking at how SwiftUI interfaces adapt as the device folds: toolbars move to the edge of the display, sheets shift away from the fold, and our own views can be arranged around it. But folding can also be part of the experience itself. As people open and close the device, SwiftUI can tell our apps how far the hinge is open, so we can respond to that movement as it happens.
We can observe these changes with the onHingeChange(isEnabled:_:) modifier, new in iOS 27.1, which gives us the hinge's angle and status whenever they change. This makes it a great fit for interactions and visual effects that follow the device as it folds and unfolds.
Let's see how we can create an animation driven by the hinge, using a simple flower graphic as an example. Here's what the result looks like:
The flower is a regular SwiftUI view with a bloom property that takes a value between 0 and 1. Its petals fan out from a shared base, gathered into a closed bud at 0 and fully spread at 1. Each petal is rotated around its bottom edge by its share of the spread multiplied by bloom, so changing that one value unfolds all the petals together.
struct Flower: View {
var bloom: Double
let petalCount = 9
let spread = 160.0
var body: some View {
ZStack(alignment: .bottom) {
// ... glow and other layers of petals ...
ForEach(0..<petalCount, id: \.self) { index in
let lastIndex = Double(petalCount - 1)
let fraction = Double(index) / lastIndex - 0.5
Petal()
// ... fill and shadows ...
.frame(width: 86, height: 420)
.rotationEffect(
.degrees(fraction * spread * bloom),
anchor: .bottom
)
}
}
}
}
To connect the flower to the hinge, we store bloom in a state property and update it in the onHingeChange() action. The action receives the previous and current DeviceHingeContext values. We only need the current one, which gives us the hinge's angle and status through its hinge property. The hinge property is nil on devices without a hinge.
struct BloomView: View {
@State private var bloom: Double = 1
var body: some View {
Flower(bloom: bloom)
.onHingeChange { _, context in
withAnimation(.smooth) {
guard let hinge = context.hinge else {
bloom = 1
return
}
if hinge.status == .closed {
bloom = 0
} else if hinge.status == .partiallyOpen {
let progress = (hinge.angle.degrees - 80) / 90
bloom = min(max(progress, 0), 1)
} else {
bloom = 1
}
}
}
}
}
While the device is partially open, the hinge angle sets the progress of the animation. At 80 degrees or less, the flower is a closed bud. At 170 degrees or more, it's in full bloom, and in between the petals unfold gradually.
The other states give the flower a resting position. When the device is closed, the app moves to the outer display, where the flower shows as a bud. When the device is fully open or has no hinge, the flower stays in full bloom.
Hinge observations are most useful for effects and interactions that respond to the folding motion itself, where every update counts. For adapting layout to the fold, reserved regions and ArrangementView are a better fit. Layout only needs to change when the fold becomes active or inactive, and that's when these APIs update, rather than on every change in the hinge angle. Reserved regions also tell us where the fold is within our view, which the hinge doesn't.
AI coding agents often reach for onHingeChange() whenever they need to adapt an interface to iPhone Duo, including for layout, where it causes unnecessary updates and doesn't tell the view where the fold is. To help with this, our Mac app SwiftFairy includes rules in its SwiftUI Layout & Resizability Scroll that guide agents to the most appropriate API for each case. You can download SwiftFairy from our tools website and try it out.
Our Swift expertise, on call for your agent
On-device Swift and SwiftUI reviews with fix guidance
Proper SwiftUI
SwiftUI Layout & Resizability
Performant Swift Charts
Modern Swift

