# Build a SwiftUI Photo Gallery Grid with Pinch to Zoom

> By Lawrence Arya, Founder & CEO of VP0. Published 2026-06-08. 6 min read.
> Source: https://vp0.com/blogs/swiftui-photo-gallery-grid-with-pinch-to-zoom-free-ios-template-vibe-coding-guid

**TL;DR.** A SwiftUI photo gallery grid with pinch-to-zoom is really two interactions. Pinching the grid changes its density, fewer or more columns, the way the Photos app does, built on LazyVGrid with an animated column count. Pinching an opened photo magnifies and pans it, built with a magnify gesture and bounds. The quiet work is loading thumbnails at thumbnail size, not full resolution, so the grid stays smooth. Start from a gallery template so the grid, the viewer, and the gestures are already wired.

## The two meanings of pinch-to-zoom in a gallery

"Pinch to zoom" in a photo gallery means two different things, and a good gallery does both. The first is pinching the grid itself to change its density, the way the iOS Photos app grows and shrinks the thumbnails as you pinch, showing fewer larger images or more smaller ones. The second is opening one photo and pinching to magnify and pan it. They feel related but are built separately: one animates a grid layout, the other transforms a single image.

Knowing which you mean shapes the build. Most galleries want both, the grid pinch for browsing density and the photo pinch for detail, so it is worth designing them as two clear interactions rather than one vague "zoom."

## The grid: LazyVGrid and a pinch to change density

The grid is a [LazyVGrid](https://developer.apple.com/documentation/swiftui/lazyvgrid) inside a scroll view, with a column count you control. To pinch the grid, you read a magnify gesture and map it to the number of columns: pinch out and the column count drops so each photo grows, pinch in and it rises so more fit. Animate the column change so the grid reflows smoothly instead of snapping, and clamp it to a sensible range, say two to five columns, so a hard pinch does not produce a single giant tile or a wall of dots.

`LazyVGrid` matters because it builds only the rows on screen. A gallery can hold thousands of photos, and an eager grid would try to lay them all out at once, so laziness is what keeps the scroll alive at any density.

## The viewer: pinch-to-zoom and pan on one photo

Tap a thumbnail and you open the viewer, where pinch-to-zoom means magnifying a single image. A [MagnifyGesture](https://developer.apple.com/documentation/swiftui/magnifygesture) drives a scale you apply to the photo, usually combined with a drag to pan once zoomed in. Three details make it feel right: clamp the scale between one and a reasonable maximum so the image cannot shrink to nothing or pixelate to mush, snap back to fit when the user zooms below one, and constrain the pan so the photo cannot be dragged entirely off screen. A double-tap to toggle between fit and a zoomed level is the expected shortcut and worth adding.

This is the interaction people judge a gallery by, because a janky or unbounded zoom feels broken immediately.

## Thumbnails are where galleries get slow

Most gallery jank is image jank. A grid that loads full-resolution photos into small thumbnails burns memory and drops frames, especially when a 4,000 by 3,000 pixel image renders into a 120-point tile. Load thumbnails at thumbnail size, cache them, and reserve the full-resolution decode for the viewer when a photo is actually opened. [AsyncImage](https://developer.apple.com/documentation/swiftui/asyncimage) loads remote images without blocking the scroll, but you still downsample and cache yourself, because the cost is in the pixels, not the views.

Get this right and the grid stays smooth even while pinching through density changes; get it wrong and no layout cleverness saves it.

## Building it from a template

The grid, the density pinch, the viewer, and the bounded zoom are the same in every gallery, so they are worth starting from. A free [VP0](https://vp0.com) design ships the photo grid, the full-screen viewer, the pinch and pan gestures, and the loading states as a SwiftUI file with a machine-readable source page, so pasting the link into Claude Code or Cursor gives the agent the gallery structure to wire to your photos. The same grid pattern sits behind a [Pinterest-style masonry grid](/blogs/pinterest-waterfall-grid-masonry-react-native/) and a [Midjourney-style image grid selector](/blogs/midjourney-style-image-grid-selector-ui-swiftui/), and the lazy-rendering discipline matches a [SwiftUI social feed](/blogs/social-media-feed-ui-swiftui/).

## Common mistakes building a photo gallery

The recurring ones are about bounds and image size. An unclamped photo zoom lets the image shrink to nothing or blow up past usefulness, and an unconstrained pan drags it off screen. A grid pinch with no column limits produces a single tile or an unreadable wall. Loading full-resolution images into thumbnails janks the grid no matter how lazy it is. Skipping the snap-back when a user zooms below fit leaves the photo floating awkwardly. And an eager grid instead of `LazyVGrid` stalls on a large library.

## Key takeaways: a SwiftUI photo gallery

- **Pinch-to-zoom means two things.** Grid density and single-photo magnification, built separately.
- **The grid is a LazyVGrid with a pinch-driven column count.** Animate and clamp the density.
- **The viewer needs bounded zoom and pan.** Clamp the scale, snap back below fit, constrain the pan, add double-tap.
- **Thumbnails cause most jank.** Load at thumbnail size and cache, decode full resolution only in the viewer.
- **Start from a gallery template.** A free VP0 SwiftUI design gives an agent the grid, the viewer, and the gestures to wire to photos.

## Frequently asked questions

**How do I build a SwiftUI photo gallery grid with pinch to zoom?** Build two interactions. For the grid, use a LazyVGrid with a column count driven by a magnify gesture, animating and clamping it to a range like two to five columns so pinching changes density smoothly. For the viewer, open a photo and apply a MagnifyGesture-driven scale with a drag to pan, clamping the scale, snapping back below fit, and constraining the pan. Load thumbnails at thumbnail size and cache them so the grid stays smooth. A free gallery template gives you the grid, the viewer, and the gestures to start from.

**What is the safest way to build this with Claude Code or Cursor?** Give the agent a gallery template with both the grid pinch and the viewer zoom already shaped. A free VP0 SwiftUI design has a machine-readable source page with the photo grid, the full-screen viewer, the pinch and pan gestures, and the loading states, so Claude Code or Cursor wires your photos into a working gallery. That avoids the common result where an AI tool ships an unclamped zoom that flies off screen and a grid that loads full-resolution images into tiny tiles.

**Can VP0 provide a free SwiftUI template for a photo gallery?** Yes. VP0 has free gallery designs in SwiftUI with the grid, the full-screen viewer, the pinch-to-zoom and pan gestures, and the loading states already built, each exposing an AI-readable source page. Because the gallery exists, your agent connects it to your photo source instead of reinventing the bounded zoom and the thumbnail performance that usually trip up hand-built galleries.

**How do I pinch the grid to change column count like the Photos app?** Read a magnify gesture on the grid and map its value to the LazyVGrid column count: pinching out lowers the count so photos grow, pinching in raises it so more fit. Animate the change so the grid reflows smoothly rather than snapping, and clamp the count to a sensible range so a hard pinch cannot produce one giant tile or an unreadable wall. Keep the grid lazy so it only lays out visible rows at any density.

**What common errors happen when vibe coding a photo gallery?** An unclamped photo zoom that shrinks to nothing or pixelates, an unconstrained pan that drags the image off screen, and a grid pinch with no column limits are the frequent interaction bugs. On performance, loading full-resolution images into thumbnails janks the grid, and an eager grid instead of LazyVGrid stalls on a large library. Bound the zoom and pan, clamp the column count, and load thumbnails at thumbnail size with caching.

## Frequently asked questions

### How do I build a SwiftUI photo gallery grid with pinch to zoom?

Build two interactions. For the grid, use a LazyVGrid with a column count driven by a magnify gesture, animating and clamping it to a range like two to five columns so pinching changes density smoothly. For the viewer, open a photo and apply a MagnifyGesture-driven scale with a drag to pan, clamping the scale, snapping back below fit, and constraining the pan. Load thumbnails at thumbnail size and cache them so the grid stays smooth. A free gallery template gives you the grid, the viewer, and the gestures to start from.

### What is the safest way to build this with Claude Code or Cursor?

Give the agent a gallery template with both the grid pinch and the viewer zoom already shaped. A free VP0 SwiftUI design has a machine-readable source page with the photo grid, the full-screen viewer, the pinch and pan gestures, and the loading states, so Claude Code or Cursor wires your photos into a working gallery. That avoids the common result where an AI tool ships an unclamped zoom that flies off screen and a grid that loads full-resolution images into tiny tiles.

### Can VP0 provide a free SwiftUI template for a photo gallery?

Yes. VP0 has free gallery designs in SwiftUI with the grid, the full-screen viewer, the pinch-to-zoom and pan gestures, and the loading states already built, each exposing an AI-readable source page. Because the gallery exists, your agent connects it to your photo source instead of reinventing the bounded zoom and the thumbnail performance that usually trip up hand-built galleries.

### How do I pinch the grid to change column count like the Photos app?

Read a magnify gesture on the grid and map its value to the LazyVGrid column count: pinching out lowers the count so photos grow, pinching in raises it so more fit. Animate the change so the grid reflows smoothly rather than snapping, and clamp the count to a sensible range so a hard pinch cannot produce one giant tile or an unreadable wall. Keep the grid lazy so it only lays out visible rows at any density.

### What common errors happen when vibe coding a photo gallery?

An unclamped photo zoom that shrinks to nothing or pixelates, an unconstrained pan that drags the image off screen, and a grid pinch with no column limits are the frequent interaction bugs. On performance, loading full-resolution images into thumbnails janks the grid, and an eager grid instead of LazyVGrid stalls on a large library. Bound the zoom and pan, clamp the column count, and load thumbnails at thumbnail size with caching.

---
*Published on the [VP0 Journal](https://vp0.com/blogs). Free to read, index and cite with attribution.*
