Isn't there an easy way to pinch to zoom in an image in Swiftui? Isn't there an easy way to pinch to zoom in an image in Swiftui? swift swift

Isn't there an easy way to pinch to zoom in an image in Swiftui?


The other answers here are overly complicated with custom zooming logic. If you want the standard, battle-tested UIScrollView zooming behavior you can just use a UIScrollView!

SwiftUI allows you to put any UIView inside an otherwise SwiftUI view hierarchy using UIViewRepresentable or UIViewControllerRepresentable. Then to put more SwiftUI content inside that view, you can use UIHostingController. Read more about SwiftUI–UIKit interop in Interfacing with UIKit and the API docs.

You can find a more complete example where I'm using this in a real app at: https://github.com/jtbandes/SpacePOD/blob/main/SpacePOD/ZoomableScrollView.swift (That example also includes more tricks for centering the image.)

var body: some View {  ZoomableScrollView {    Image("Your image here")  }}
struct ZoomableScrollView<Content: View>: UIViewRepresentable {  private var content: Content  init(@ViewBuilder content: () -> Content) {    self.content = content()  }  func makeUIView(context: Context) -> UIScrollView {    // set up the UIScrollView    let scrollView = UIScrollView()    scrollView.delegate = context.coordinator  // for viewForZooming(in:)    scrollView.maximumZoomScale = 20    scrollView.minimumZoomScale = 1    scrollView.bouncesZoom = true    // create a UIHostingController to hold our SwiftUI content    let hostedView = context.coordinator.hostingController.view!    hostedView.translatesAutoresizingMaskIntoConstraints = true    hostedView.autoresizingMask = [.flexibleWidth, .flexibleHeight]    hostedView.frame = scrollView.bounds    scrollView.addSubview(hostedView)    return scrollView  }  func makeCoordinator() -> Coordinator {    return Coordinator(hostingController: UIHostingController(rootView: self.content))  }  func updateUIView(_ uiView: UIScrollView, context: Context) {    // update the hosting controller's SwiftUI content    context.coordinator.hostingController.rootView = self.content    assert(context.coordinator.hostingController.view.superview == uiView)  }  // MARK: - Coordinator  class Coordinator: NSObject, UIScrollViewDelegate {    var hostingController: UIHostingController<Content>    init(hostingController: UIHostingController<Content>) {      self.hostingController = hostingController    }    func viewForZooming(in scrollView: UIScrollView) -> UIView? {      return hostingController.view    }  }}


The SwiftUI API is pretty unhelpful here: the onChanged gives number relative to start of current zoom gesture and no obvious way within a callback to get the initial value. And there is an onEnded callback but easy to miss/forget.

A work around, add:

@State var lastScaleValue: CGFloat = 1.0

Then in the callback:

.gesture(MagnificationGesture().onChanged { val in            let delta = val / self.lastScaleValue            self.lastScaleValue = val            let newScale = self.scale * delta//... anything else e.g. clamping the newScale}.onEnded { val in  // without this the next gesture will be broken  self.lastScaleValue = 1.0}

where newScale is your own tracking of scale (perhaps state or a binding). If you set your scale directly it will get messed up as on each tick the amount will be relative to previous amount.


Here's one way of adding pinch zooming to a SwiftUI view. It overlays a UIView with a UIPinchGestureRecognizer in a UIViewRepresentable, and forwards the relevant values back to SwiftUI with bindings.

You can add the behaviour like this:

Image("Zoom")    .pinchToZoom()

This adds behaviour similar to zooming photos in the Instagram feed. Here's the full code:

import UIKitimport SwiftUIclass PinchZoomView: UIView {    weak var delegate: PinchZoomViewDelgate?    private(set) var scale: CGFloat = 0 {        didSet {            delegate?.pinchZoomView(self, didChangeScale: scale)        }    }    private(set) var anchor: UnitPoint = .center {        didSet {            delegate?.pinchZoomView(self, didChangeAnchor: anchor)        }    }    private(set) var offset: CGSize = .zero {        didSet {            delegate?.pinchZoomView(self, didChangeOffset: offset)        }    }    private(set) var isPinching: Bool = false {        didSet {            delegate?.pinchZoomView(self, didChangePinching: isPinching)        }    }    private var startLocation: CGPoint = .zero    private var location: CGPoint = .zero    private var numberOfTouches: Int = 0    init() {        super.init(frame: .zero)        let pinchGesture = UIPinchGestureRecognizer(target: self, action: #selector(pinch(gesture:)))        pinchGesture.cancelsTouchesInView = false        addGestureRecognizer(pinchGesture)    }    required init?(coder: NSCoder) {        fatalError()    }    @objc private func pinch(gesture: UIPinchGestureRecognizer) {        switch gesture.state {        case .began:            isPinching = true            startLocation = gesture.location(in: self)            anchor = UnitPoint(x: startLocation.x / bounds.width, y: startLocation.y / bounds.height)            numberOfTouches = gesture.numberOfTouches        case .changed:            if gesture.numberOfTouches != numberOfTouches {                // If the number of fingers being used changes, the start location needs to be adjusted to avoid jumping.                let newLocation = gesture.location(in: self)                let jumpDifference = CGSize(width: newLocation.x - location.x, height: newLocation.y - location.y)                startLocation = CGPoint(x: startLocation.x + jumpDifference.width, y: startLocation.y + jumpDifference.height)                numberOfTouches = gesture.numberOfTouches            }            scale = gesture.scale            location = gesture.location(in: self)            offset = CGSize(width: location.x - startLocation.x, height: location.y - startLocation.y)        case .ended, .cancelled, .failed:            isPinching = false            scale = 1.0            anchor = .center            offset = .zero        default:            break        }    }}protocol PinchZoomViewDelgate: AnyObject {    func pinchZoomView(_ pinchZoomView: PinchZoomView, didChangePinching isPinching: Bool)    func pinchZoomView(_ pinchZoomView: PinchZoomView, didChangeScale scale: CGFloat)    func pinchZoomView(_ pinchZoomView: PinchZoomView, didChangeAnchor anchor: UnitPoint)    func pinchZoomView(_ pinchZoomView: PinchZoomView, didChangeOffset offset: CGSize)}struct PinchZoom: UIViewRepresentable {    @Binding var scale: CGFloat    @Binding var anchor: UnitPoint    @Binding var offset: CGSize    @Binding var isPinching: Bool    func makeCoordinator() -> Coordinator {        Coordinator(self)    }    func makeUIView(context: Context) -> PinchZoomView {        let pinchZoomView = PinchZoomView()        pinchZoomView.delegate = context.coordinator        return pinchZoomView    }    func updateUIView(_ pageControl: PinchZoomView, context: Context) { }    class Coordinator: NSObject, PinchZoomViewDelgate {        var pinchZoom: PinchZoom        init(_ pinchZoom: PinchZoom) {            self.pinchZoom = pinchZoom        }        func pinchZoomView(_ pinchZoomView: PinchZoomView, didChangePinching isPinching: Bool) {            pinchZoom.isPinching = isPinching        }        func pinchZoomView(_ pinchZoomView: PinchZoomView, didChangeScale scale: CGFloat) {            pinchZoom.scale = scale        }        func pinchZoomView(_ pinchZoomView: PinchZoomView, didChangeAnchor anchor: UnitPoint) {            pinchZoom.anchor = anchor        }        func pinchZoomView(_ pinchZoomView: PinchZoomView, didChangeOffset offset: CGSize) {            pinchZoom.offset = offset        }    }}struct PinchToZoom: ViewModifier {    @State var scale: CGFloat = 1.0    @State var anchor: UnitPoint = .center    @State var offset: CGSize = .zero    @State var isPinching: Bool = false    func body(content: Content) -> some View {        content            .scaleEffect(scale, anchor: anchor)            .offset(offset)            .animation(isPinching ? .none : .spring())            .overlay(PinchZoom(scale: $scale, anchor: $anchor, offset: $offset, isPinching: $isPinching))    }}extension View {    func pinchToZoom() -> some View {        self.modifier(PinchToZoom())    }}