Zooming and dragging simultaneously on an Image using SwiftUI. (iOS 15)

Andrés Mendieta
4 min readAug 28, 2023

--

I recently had to use both these gestures on an image using SwiftUI. I found a lot of documentation for using these gestures separately, but not together. So, I’m going to show what I was finally able to implement. Maybe it can help someone else 🤷🏻‍♂️.

The first thing I understood is that the zoom gesture should be implemented first, as the scale will be crucial for the proper functioning of the drag.

In my case, I added a Text and an Image. I applied several modifiers to the image that are relevant for this case. As we can see, I want the image to occupy the width of the screen and be square. Therefore, the height is equal to the screen width. In the .onAppear method, I’m setting the value of the screen width for quicker access. Finally, I’m using .clipped to crop our image to the desired size.

struct ContentView: View {
@State var screenW = 0.0

var body: some View {
GeometryReader { geometry in
VStack {
Text("Mily")
.font(.largeTitle)
Image("pugVertical")
.resizable()
.scaledToFill()
.frame(width: screenW, height: screenW)
.clipped()
}
.onAppear {
screenW = geometry.size.width
}
}
}
}

As you can see, our model Mily the pug 🐶 appears in a square with the width of the screen, filling the square completely.

Gestures

Now let’s add the .scaleEffect modifier. It requires a scale parameter, which by default has a value of 1. Then we add the .gesture modifier, allowing us to add a MagnificationGesture. In the .onChange block, it will provide us with the value obtained from the pinch-to-zoom gesture. Using this value, we will calculate the scale. The other method is .onEnded, where we will store the last scale value for later use.

struct ContentView: View {
@State var screenW = 0.0
@State var scale = 1.0
@State var lastScale = 0.0

var body: some View {
GeometryReader { geometry in
VStack {
Text("Mily")
.font(.largeTitle)
Image("pugVertical")
.resizable()
.scaleEffect(scale)
.scaledToFill()
.frame(width: screenW, height: screenW)
.clipped()
.gesture(
MagnificationGesture(minimumScaleDelta: 0)
.onChanged({ value in
withAnimation(.interactiveSpring()) {
scale = handleScaleChange(value)
}
})
.onEnded({ _ in
lastScale = scale
})
)
}
.onAppear {
screenW = geometry.size.width
}
}
}

private func handleScaleChange(_ zoom: CGFloat) -> CGFloat {
lastScale + zoom - (lastScale == 0 ? 0 : 1)
}
}

In the handleScaleChange method, when we haven’t performed any zoom gesture, we should add lastScale to zoom. However, if we have already zoomed in on the image, it’s necessary to subtract 1 from that operation.

Now, we just need to add the DragGesture. To achieve this, we use the .simultaneously method. Similarly, in this case, we need to save the last offset in the .onEnded method.

struct ContentView: View {
@State var screenW = 0.0
@State var scale = 1.0
@State var lastScale = 0.0
@State var offset: CGSize = .zero
@State var lastOffset: CGSize = .zero

var body: some View {
GeometryReader { geometry in
VStack {
Text("Mily")
.font(.largeTitle)
Image("pugVertical")
.resizable()
.scaleEffect(scale)
.offset(offset)
.scaledToFill()
.frame(width: screenW, height: screenW)
.clipped()
.gesture(
MagnificationGesture(minimumScaleDelta: 0)
.onChanged({ value in
withAnimation(.interactiveSpring()) {
scale = handleScaleChange(value)
}
})
.onEnded({ _ in
lastScale = scale
})
.simultaneously(
with: DragGesture(minimumDistance: 0)
.onChanged({ value in
withAnimation(.interactiveSpring()) {
offset = handleOffsetChange(value.translation)
}
})
.onEnded({ _ in
lastOffset = offset
})

)
)
}
.onAppear {
screenW = geometry.size.width
}
}
}

private func handleScaleChange(_ zoom: CGFloat) -> CGFloat {
lastScale + zoom - (lastScale == 0 ? 0 : 1)
}

private func handleOffsetChange(_ offset: CGSize) -> CGSize {
var newOffset: CGSize = .zero

newOffset.width = offset.width + lastOffset.width
newOffset.height = offset.height + lastOffset.height

return newOffset
}
}

This is how the final version looks like.

Here, we were able to see both interactions happening simultaneously. Now, it’s just a matter of adding some desired behaviors, but I’ll leave that for another article.

Here, we were able to see both interactions happening simultaneously. Now, it would only remain to add some desired behaviors, but I’ll leave that for another article.

Thank you very much for reading this article; I hope it has been helpful to you.

If you find anything that can be improved, please don’t hesitate to comment. See you later!

--

--