Reputation: 8830
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)
}
}
}
Upvotes: 14
Views: 2148
Reputation: 757
You could achieve that using MagnificationGesture + Custom logic Video result
struct ZoomInOutView: View {
// Scale value
@State private var scale: CGFloat = 1.0
// Scale value for detecting in/out direction
@State private var scaleValue: CGFloat = 0
// Scale step, if need faster speed update step
@State private var zoomStep: CGFloat = 0.2
// Scale bounds
let minZoomStep: CGFloat = 1.0
let maxZoomStep: CGFloat = 20.0
var body: some View {
ZStack {
// Could be any UI
Color.red.frame(width: 50, height: 50)
.clipShape(Circle())
// Subscribe scale update to this element
.scaleEffect(scale)
// "Invisible" view for detecting gestures, just put it at top of the stack
Color.white.opacity(0.0001)
.gesture(MagnificationGesture().onChanged { updateScale($0) })
}
.onChange(of: scale, perform: { value in
// Any logic based on updated value gradient something else etc.
print("Zoom scale", value)
})
}
private func updateScale(_ scale: MagnificationGesture.Value) {
let zoomIn = scale > scaleValue ? false : true
let scale = min(max(scale.magnitude, 0), 20.0)
scaleValue = scale
if zoomIn {
if self.scale > minZoomStep {
self.scale -= zoomStep
}
} else {
if self.scale < maxZoomStep {
self.scale += zoomStep
}
}
}
}
Upvotes: -1
Reputation: 1281
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)
}
}
Upvotes: 3