Zooming With The Magnify Gesture in SwiftUI

November 3rd, 2025

⏱ Reading Time: 4 mins

Zooming content in and out is a fundamental gesture that users of smartphones and tablets are well acquainted with. Also known as pinch to zoom, this gesture is available to attach to our SwiftUI views programmatically, letting users change the scale level of what they are interacting with, using a familiar move of the fingers. The magnify gesture, available as of iOS 17 in SwiftUI, is the way to go when it comes to enable this gesture in our apps. And we’re just about to go through it in the parts that follow next.

Temporary magnification

As it happens with all other gestures, using the MagnifyGesture() in SwiftUI is not difficult, and programmatically it’s applied pretty much like any other gesture. However, there’s something important to know, and decide about it in advance, before starting to write any code; do we need magnification to be permanent or temporary?

Phrasing it differently, what is going to happen in the zoomed view when the gesture is finished? Should it go back to its original scale, or the new zoom level should remain after the fingers stop touching the screen?

Answering this defines the APIs to use when tracking changes to magnification. Here, we’ll get started with the case of temporary zooming, which is the simpler one.

The first move is to declare a gesture state property that keeps track of the magnification level:

If GestureState property wrapper is new to you, the following extract from the official Apple docs explains it nicely:

A property wrapper type that updates a property while the user performs a gesture and resets the property back to its initial state when the gesture ends.

Once the above property is in place, we go ahead and provide it as argument to the scaleEffect modifier. This modifier is attached to the view we want to make zoomable. In this demonstration, the target view is an Image view, so zooming makes sense:

With the ground set, we can now apply the magnify gesture with a new instance of the MagnifyGesture in the gesture view modifier:

However, that’s not enough in order to enable magnification tracking. To get real-time magnification updates, we need to apply the updating(_:body:) modifier to MagnifyGesture. As you see next, this modifier accepts two arguments:

  1. The binding to the magnification gesture state property so the modifier can update its value.
  2. A closure with three parameters that gets called for as long as the gesture is in progress. The first parameter value of the closure contains the current magnification value, while the second contains the previous value, which we update explicitly.

By assigning the current magnification value to the state parameter value in the above closure, the modifier updates magnification behind the scenes. As a result, this changes the scale of the image view.

Here’s the outcome:

The full code that implements the above is presented next:

Persisting magnification

Resetting the zoom when the gesture is ended is not always a desired effect, so let’s find out how to make it stay permanent and capable of continuing on a subsequent magnification gesture right from the point it was stopped.

First off, what we don’t need any more is the magnification gesture state property we met previously, and the updating(_:body:) modifier. We start over from this state:

This time, we need two state properties (@State, not @GestureState):

  1. One that keeps the total magnification value (scale) when the gesture is ended.
  2. One that keeps the current magnification value while the gesture is in progress.

The value we’ll pass in the scaleEffect modifier is the product of these two, and soon you’ll understand why:

In the MagnifyGesture we’ll apply two different modifiers now:

The onChanged modifier is called while the gesture is taking place; while the user pinches to zoom in or out. This is the place to update the currentMagnification value:

The onEnded modifier, as its name implies, is called just once right when the gesture is finished. Two things to do here; calculate the totalMagnification, and reset the currentMagnification:

You might ask yourself why we need two state properties instead of one in order to persist magnification, or why we just don’t update totalMagnification in onChanged modifier too. You can easily get an answer to that if you add a print command to onChanged and see the value of value.magnification while pinching.

You will find out that the zoom value always starts from 1.0, no matter if last scale had a different value (for example, 2.5). As a result, the target view will always start zooming in or out from its original scale again, with the seamless subsequent magnify gestures being impossible. On the contrary, having one property to keep the current scale and another the total scale, while combining them properly, leads to a fluent magnification effect.

Given the above configuration, it makes sense that the result of totalMagnification * currentMagnification will always produce the correct scale value. But, omitting a step, such as not resetting currentMagnification, leads to an incorrect scale when the gesture starts or ends.

Putting everything together now, here’s the final code that allows to persist magnification:

Wrapping up

Using the magnify gesture doesn’t hide any tricks, but it requires different treatment if zooming will be temporary or it should persist once the gesture is finished. The appropriate set of properties and modifiers should be used, depending on the case, with both of them having been demonstrated in this post. Keep in mind that MagnifyGesture is available since iOS 17. For older versions, there’s the MagnificationGesture to use, but it’s a deprecated API nowadays. I hope you found this post useful. Thanks for reading!

Stay Up To Date

Subscribe to my newsletter and get notifiied instantly when I post something new on SerialCoder.dev.

    We respect your privacy. Unsubscribe at any time.