A few issues with the iPhone Photos app

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 🤦‍♂️

Answered by DTS Engineer in 888710022

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:

  • LazyVGrid for the grid container (iOS 14 or later).
  • MagnifyGesture (iOS 17 or later) for pinch input. MagnifyGesture replaced the now-deprecated MagnificationGesture; 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/@Observable model 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:

  1. SwiftUI-native, "good-enough" zoomLazyVGrid with a @State column count; during MagnifyGesture, apply .scaleEffect() to the grid; on gesture end, snap to a new column count and let LazyVGrid relayout. Smooth during the pinch, with a small snap at the end. Acceptable for many apps.
  2. Bridge to UIKit for true interpolated zoom — wrap a UICollectionView + UICollectionViewTransitionLayout via UIViewRepresentable. 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 UICollectionViewLayout instances — 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 the transitionProgress property, 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; its scale property maps to your transitionProgress value.
  • 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.)

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:

  • LazyVGrid for the grid container (iOS 14 or later).
  • MagnifyGesture (iOS 17 or later) for pinch input. MagnifyGesture replaced the now-deprecated MagnificationGesture; 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/@Observable model 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:

  1. SwiftUI-native, "good-enough" zoomLazyVGrid with a @State column count; during MagnifyGesture, apply .scaleEffect() to the grid; on gesture end, snap to a new column count and let LazyVGrid relayout. Smooth during the pinch, with a small snap at the end. Acceptable for many apps.
  2. Bridge to UIKit for true interpolated zoom — wrap a UICollectionView + UICollectionViewTransitionLayout via UIViewRepresentable. 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 UICollectionViewLayout instances — 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 the transitionProgress property, 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; its scale property maps to your transitionProgress value.
  • 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.)

(part 2 of 2) — continuing from my response above.

The following performance considerations and progressive-loading techniques apply to both the SwiftUI path and the UIKit path described in part 1.

PhotoKit performance tips (apply to both paths)

A few details from the PHCachingImageManager and PHImageManager header comments that are easy to miss and matter for smoothness:

  • Use PHImageRequestOptionsDeliveryModeOpportunistic (the default for async requests) — your result handler may be called multiple times: first with a fast, lower-quality result, then with the final high-quality image as it becomes available. This is what lets the grid feel "instant" even while the real thumbnails are still being prepared. From the header: "client may get several image results when the call is asynchronous."
  • Match your caching options exactly to your request options. From the startCachingImagesForAssets header comment: "The options values shall exactly match the options values used in loading methods." If your targetSize, contentMode, or options differ between the cache call and the request call, the caching effectively does nothing — a frequently missed pitfall when setting up the two call sites separately.
  • Check PHImageResultIsDegradedKey in the info dictionary of your result handler to know whether you're receiving a placeholder or the final image. Use this to fade in the final image, or to skip work on the intermediate result.
  • allowSecondaryDegradedImage (iOS 17 or later) — opt-in flag on PHImageRequestOptions for an additional intermediate-quality step between the initial degraded result and the final image. Worth enabling for very large libraries or slower-storage scenarios.

Progressive loading

Smooth scrolling and zooming under a heavy photo library depends on three loosely related "progressive loading" mechanisms working together:

  • Multi-pass image qualityOpportunistic delivery + allowSecondaryDegradedImage give you "fast placeholder → final image" so cells display content immediately rather than waiting for the high-quality data.
  • Cell prefetchingUICollectionView prepares cells in advance via the prefetchDataSource property and the UICollectionViewDataSourcePrefetching protocol. From the UICollectionView docs: "Cell prefetching prepares cells in advance of the time they're required... Cell prefetching is enabled by default." Combined with PhotoKit caching, this is what makes scrolling feel instant.
  • PHCachingImageManager-driven preloading — in your UICollectionViewDataSourcePrefetching implementation, call startCachingImagesForAssets:targetSize:contentMode:options: for assets that are about to scroll into view, and stopCachingImagesForAssets:targetSize:contentMode:options: for assets that have scrolled out. Make sure the options match what requestImage will use later (per the gotcha above). This is the single highest-impact piece for grid smoothness.

SwiftUI caveat: LazyVGrid lazily creates cells (only what's visible) but doesn't expose a prefetch protocol equivalent to UICollectionViewDataSourcePrefetching. To get the same prefetch behavior in SwiftUI, either:

  • Track scroll position manually (via GeometryReader or ScrollViewReader) and call startCachingImagesForAssets:... based on what's about to appear, or
  • Bridge to UICollectionView via UIViewRepresentable to inherit the prefetch protocol.

Further reading

If you're already on one specific path (UIKit or SwiftUI) and run into a specific snag, post the code and I can take a closer look.

A few issues with the iPhone Photos app
 
 
Q