I’m curious what framework and rendering technique you’re using for the photo album’s grid zoom in and out. Why is it so incredibly smooth? I can’t seem to pull that off 🤦♂️
Thanks for the positive observation about the Photos app's grid zoom — it is genuinely smooth, and wanting to replicate that in your own app is a reasonable aspiration. I spent some cycles putting together the following summary of APIs you may wish to explore pursuing this goal.
A framing note before getting to the substance: I can't share the specifics of how Apple's Photos app is implemented internally, since that's not something DTS can detail. What I can do is point you to the public APIs and techniques that let you build similar grid-zoom behavior in your own app. The good news is that the public surface includes a class for the interpolated-zoom effect you're after.
The SwiftUI path
For a photo-library-backed grid with smooth pinch-to-zoom in SwiftUI, the building blocks are:
LazyVGridfor the grid container (iOS 14 or later).MagnifyGesture(iOS 17 or later) for pinch input.MagnifyGesturereplaced the now-deprecatedMagnificationGesture; the gesture's value gives you the magnification factor to drive a state variable.PHCachingImageManager— for loading and caching thumbnails. Its documentation explicitly calls out the photo-grid use case: "use a caching image manager when you want to populate a collection view or similar UI with thumbnails of photo or video assets." Wrap calls in a@StateObject/@Observablemodel or load on.task/.onAppear.
Honest caveat: pure SwiftUI gets you a basic grid with pinch-driven scaling effects (via .scaleEffect() during the gesture) but doesn't natively interpolate the column count the way UICollectionViewTransitionLayout (covered in the UIKit path below) does. Two practical patterns:
- SwiftUI-native, "good-enough" zoom —
LazyVGridwith a@Statecolumn count; duringMagnifyGesture, apply.scaleEffect()to the grid; on gesture end, snap to a new column count and letLazyVGridrelayout. Smooth during the pinch, with a small snap at the end. Acceptable for many apps. - Bridge to UIKit for true interpolated zoom — wrap a
UICollectionView+UICollectionViewTransitionLayoutviaUIViewRepresentable. More work, but matches the continuous-interpolation behavior. Bridging is genuinely the right call here, not a workaround.
The UIKit path
For UIKit-based apps — or as the implementation behind a UIViewRepresentable bridge from SwiftUI — the building blocks are:
UICollectionView— the scrolling/recycling container for the grid itself.- Two
UICollectionViewLayoutinstances — one for each zoom level you want to interpolate between (for example, a 3-column layout and a 5-column layout). UICollectionViewTransitionLayout— this is the key piece. From its documentation: "This layout object determines the layout of each item by interpolating between the layout values in the current and new layout objects. The interpolation is driven by the value in thetransitionProgressproperty, which you update periodically from your code to drive the transition. For example, if you use this class in conjunction with a gesture recognizer, the handler for your gesture recognizer would update that property and invalidate the layout." That's a built-in API designed for exactly the interactive layout-to-layout interpolation behavior pinch-to-zoom needs.UIPinchGestureRecognizer— drives the gesture; itsscaleproperty maps to yourtransitionProgressvalue.PHCachingImageManager— same caching pattern as the SwiftUI path (see above).
The interactive transition flow is: pinch starts → call startInteractiveTransition(to:completion:) on the collection view (it returns a UICollectionViewTransitionLayout) → as the pinch updates, set the layout's transitionProgress from the gesture's scale → on gesture end, call finishInteractiveTransition() to commit or cancelInteractiveTransition() to revert.
(Continued in part 2 — performance considerations and progressive loading that apply to both paths.)