Using the pan gesture here caused some wonky snapping behavior (similar to what is pointed out here) and it doesn't have a nice springy/bounce effect that panning around a ScrollView does.
I wanted to use a SwiftUI ScrollView and just support the zoom gesture...
struct ExampleView: View {
@State private var lastScale: CGFloat = 1.0
@State private var scale: CGFloat = 1.0
var body: some View {
let contentSize: CGFloat = 1500 //testing on iPad
ScrollView(
[.horizontal, .vertical]
) {
Text("My Content")
.font(.system(size: 300))
.frame(
width: contentSize,
height: contentSize
)
.scaleEffect(scale)
.frame(
width: contentSize * scale,
height: contentSize * scale
)
.background(.red)
.gesture(
MagnificationGesture()
.onChanged { val in
let delta = val / lastScale
lastScale = val
let newScale = scale * delta
if newScale <= 3 && newScale >= 1 {
scale = newScale
}
}.onEnded { val in
lastScale = 1
}
)
}
}
}
It works "fine", but the main problem is that zooming shifts content towards the center, instead of zooming in where you make your gesture. This isn't a ScrollView specific issue, even without the ScrollView I had the same experience.
Example showing zooming shifting away from zoom area
However, to solve this... SwiftUI ScrollViews are not very flexible. If I want to track content offset and programmatically adjust offset while I scale it is a pretty huge effort, since there's no direct support for this in SwiftUI.
The workaround I found for this is to actually zoom the whole scrollview instead, not the content.
Example showing zooming remains centered on the zoom area
struct ExampleView: View {
@State private var lastScale: CGFloat = 1.0
@State private var scale: CGFloat = 1.0
var body: some View {
let contentSize: CGFloat = 1500 //testing on iPad
ScrollView(
[.horizontal, .vertical]
) {
Text("My Content")
.font(.system(size: 300))
.frame(
width: contentSize,
height: contentSize
)
.background(.red)
.padding(contentSize / 2)
}
.scaleEffect(scale)
.gesture(
MagnificationGesture()
.onChanged { val in
let delta = val / lastScale
lastScale = val
let newScale = scale * delta
if newScale <= 3 && newScale >= 1 {
scale = newScale
}
}.onEnded { val in
lastScale = 1
}
)
}
}
Obviously, this is a bit of hack but works well when the ScrollView content covers a whole screen in a ZStack. You just have to be sure you have enough content padding to handle the zoom threshold and prevent shrinking below 1.0 scale.
This wont work for all scenarios but it worked great for mine (moving around a game board), just wanted to post just in case someone else is in the same boat.