I have a piece of code that allows you to zoom in and out on a circle with a gradient using the magnification gesture. This works fine if I place my fingers in the middle of the screen and zoom, but if I place my fingers on the edge of the screen and do the magnification gesture, I want it to zoom in on the point in between my fingers. Right now, it still magnifies with the center of the screen as center for the magnification.
How can I modify my code to allow the users to center on the CGPoint right between the placement of their finger?
struct ContentView: View {
@GestureState var magnificationState = MagnificationState.inactive
@State var viewMagnificationState = CGFloat(1.0)
var magnificationScale: CGFloat {
return viewMagnificationState * magnificationState.scale
}
var body: some View {
let gradient = Gradient(colors: [.red, .yellow, .green, .blue, .purple, .red, .yellow, .green, .blue, .purple, .red, .yellow, .green, .blue, .purple])
let magnificationGesture = MagnificationGesture()
.updating($magnificationState) { value, state, transaction in
state = .zooming(scale: value)
}.onEnded { value in
self.viewMagnificationState *= value
}
Circle()
.fill(
RadialGradient(gradient: gradient, center: .center, startRadius: 50, endRadius: 2000)
)
.frame(width: 2000, height: 2000)
.scaleEffect(magnificationScale)
.gesture(magnificationGesture)
}
}
enum MagnificationState {
case inactive
case zooming(scale: CGFloat)
var scale: CGFloat {
switch self {
case .zooming(let scale):
return scale
default:
return CGFloat(1.0)
}
}
}
Zooming while centering on an anchor point is still (!) not supported in SwiftUI. As a workaround, we can use UIPinchGestureRecognizer on a transparent UIView with UIViewRepresentable. Zooming with an anchor point is essentially scaling and translating. We can apply this to a view with a transformEffect view modifier. This view modifier applies a CGAffineTransform to the view.
The following extension simplifies scaling around an anchor point:
extension CGAffineTransform {
func scaled(by scale: CGFloat, with anchor: CGPoint) -> CGAffineTransform {
self
.translatedBy(x: anchor.x, y: anchor.y)
.scaledBy(x: scale, y: scale)
.translatedBy(x: -anchor.x, y: -anchor.y)
}
}
GestureTransformView is a UIViewRepresentable with a binding to a transform. We will update the transform in the delegate for the UIPinchGestureRecognizer.
struct GestureTransformView: UIViewRepresentable {
@Binding var transform: CGAffineTransform
func makeUIView(context: Context) -> UIView {
let view = UIView()
let zoomRecognizer = UIPinchGestureRecognizer(
target: context.coordinator,
action: #selector(Coordinator.zoom(_:)))
zoomRecognizer.delegate = context.coordinator
view.addGestureRecognizer(zoomRecognizer)
context.coordinator.zoomRecognizer = zoomRecognizer
return view
}
func updateUIView(_ uiView: UIView, context: Context) {
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
}
extension GestureTransformView {
class Coordinator: NSObject, UIGestureRecognizerDelegate {
var parent: GestureTransformView
var zoomRecognizer: UIPinchGestureRecognizer?
var startTransform: CGAffineTransform = .identity
var pivot: CGPoint = .zero
init(_ parent: GestureTransformView){
self.parent = parent
}
func setGestureStart(_ gesture: UIGestureRecognizer) {
startTransform = parent.transform
pivot = gesture.location(in: gesture.view)
}
@objc func zoom(_ gesture: UIPinchGestureRecognizer) {
switch gesture.state {
case .began:
setGestureStart(gesture)
break
case .changed:
applyZoom()
break
case .cancelled:
fallthrough
case .ended:
applyZoom()
startTransform = parent.transform
zoomRecognizer?.scale = 1
default:
break
}
}
func applyZoom() {
let gestureScale = zoomRecognizer?.scale ?? 1
parent.transform = startTransform
.scaled(by: gestureScale, with: pivot)
}
}
}
And this is how you can use the GestureTransformView. Note that the transformEffect is applied to the Stack, not the Circle. This makes sure that the (previous) transformation is applied correctly to the overlay as well.
struct ContentView: View {
@State var transform: CGAffineTransform = .identity
var body: some View {
let gradient = Gradient(colors: [.red, .yellow, .green, .blue, .purple,
.red, .yellow, .green, .blue, .purple,
.red, .yellow, .green, .blue, .purple])
ZStack {
Circle()
.fill(
RadialGradient(gradient : gradient,
center : .center,
startRadius: 50,
endRadius : 2000)
)
.frame(width: 2000, height: 2000)
.overlay {
GestureTransformView(transform: $transform)
}
} .transformEffect(transform)
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With