NEW Tool! SwiftFairy: Local Swift and SwiftUI code reviews for your coding agent. Learn more ...NEW Tool! SwiftFairy:Local code reviews for your agents. Learn more...

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:

iPhone Duo in a partially folded position with a purple flower in the middle of the inner display. As the device unfolds, the flower's petals fan out from a closed bud into full bloom, and as it folds, they close again.

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.

SwiftFairy

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