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

前端 未结 7 1492
自闭症患者
自闭症患者 2020-12-28 08:07

I want to be able to resize and move an image in SwiftUI (like if it were a map) with pinch to zoom and drag it around.

With UIKit I embedded the image into a

相关标签:
7条回答
  • 2020-12-28 08:41
    struct DetailView: View {
        var item: MenuItem
        @State private var zoomed:Bool = false
        @State var scale: CGFloat = 1.0
        @State var isTapped: Bool = false
        @State var pointTaped: CGPoint = CGPoint.zero
        @State var draggedSize: CGSize = CGSize.zero
        @State var previousDraged: CGSize = CGSize.zero
    
        var width = UIScreen.main.bounds.size.width
        var height = UIScreen.main.bounds.size.height
    
        var body: some View {
            GeometryReader {  reader in
                VStack(alignment: .center) {
                    ScrollView(){
                        HStack {
                            ScrollView(.vertical){
                                Image(self.item.mainImage)
                                    .resizable()
                                    .scaledToFill()
                                    .animation(.default).offset(x: self.draggedSize.width, y: 0)
                                    .scaleEffect(self.scale).scaleEffect(self.isTapped ? 2 : 1, anchor: UnitPoint(x : (self.pointTaped.x) / (reader.frame(in : .global).maxX),y: (self.pointTaped.y) / (reader.frame(in : .global).maxY )))
                                    .gesture(TapGesture(count: 2)
                                        .onEnded({ value in
                                            self.isTapped = !self.isTapped
                                        })
                                        .simultaneously(with: DragGesture(minimumDistance: 0, coordinateSpace: .global)  .onChanged { (value) in
                                            self.pointTaped = value.startLocation
                                            self.draggedSize = CGSize(width: value.translation.width + self.previousDraged.width, height: value.translation.height + self.previousDraged.height)
                                        }
                                        .onEnded({ (value) in
                                            let offSetWidth = (reader.frame(in :.global).maxX * self.scale) - (reader.frame(in :.global).maxX) / 2
                                            let newDraggedWidth = self.previousDraged.width * self.scale
                                            if (newDraggedWidth > offSetWidth){
                                                self.draggedSize = CGSize(width: offSetWidth / self.scale, height: value.translation.height + self.previousDraged.height)
                                            }
                                            else if (newDraggedWidth < -offSetWidth){
                                                self.draggedSize = CGSize(width:  -offSetWidth / self.scale, height: value.translation.height + self.previousDraged.height)
                                            }
                                            else{
                                                self.draggedSize = CGSize(width: value.translation.width + self.previousDraged.width, height: value.translation.height + self.previousDraged.height)
                                            }
                                            self.previousDraged =  self.draggedSize
                                        })))
    
                                    .gesture(MagnificationGesture()
                                        .onChanged { (value) in
                                            self.scale = value.magnitude
    
                                    }.onEnded { (val) in
                                        //self.scale = 1.0
                                        self.scale = val.magnitude
                                        }
                                )
                            }
                        }
    
                            HStack {
                                Text(self.item.description)
                                    .foregroundColor(Color.black)
                                    .multilineTextAlignment(.leading)
                                    .padding(4)
                            }
                    }
                }.navigationBarTitle("Menu Detail")
            }
        }
    }
    
    0 讨论(0)
  • 2020-12-28 08:43

    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 UIKit
    import SwiftUI
    
    class 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())
        }
    }
    
    0 讨论(0)
  • 2020-12-28 08:49

    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
        }
      }
    }
    
    0 讨论(0)
  • 2020-12-28 08:49

    Looks like there isn't native support in SwiftUI's ScrollView, however, there's still a pretty simple way to do it.

    Create a MagnificationGesture like you were going for, but be sure to multiply your current scale by the value you get in the gesture's .onChanged closure. This closure is giving you the change in zoom rather than the current scale value.

    When you're zoomed out and begin to zoom in it won't increase from the current scale (0.5 to 0.6 as an arbitrary example), it will increase from 1 to 1.1. That's why you were seeing weird behavior.

    This answer will work if the MagnificationGesture is on the same view that has the .scaleEffect. Otherwise, James' answer will work better.

    struct ContentView: View {
        @State var scale: CGFloat
        var body: some View {
            let gesture = MagnificationGesture(minimumScaleDelta: 0.1)
                .onChanged { scaleDelta in
                    self.scale *= scaleDelta
            }
            return ScrollView {
                // Your ScrollView content here :)
            }
                .gesture(gesture)
                .scaleEffect(scale)
        }
    }
    

    P.S. You may find that using a ScrollView for this purpose is clunky and you aren't able to drag and zoom simultaneously. If this is the case & you aren't happy with it I would look into adding multiple gestures and adjusting your content's offset manually rather than using a ScrollView.

    0 讨论(0)
  • 2020-12-28 08:56

    I am also struggle with this issue. But some working sample is made with the this video-(https://www.youtube.com/watch?v=p0SwXJYJp2U)

    This is not completed. It's difficult to scale with anchor point. Hope this is hint to someone else.

    struct ContentView: View {
    
        let maxScale: CGFloat = 3.0
        let minScale: CGFloat = 1.0
    
        @State var lastValue: CGFloat = 1.0
        @State var scale: CGFloat = 1.0
        @State var draged: CGSize = .zero
        @State var prevDraged: CGSize = .zero
        @State var tapPoint: CGPoint = .zero
        @State var isTapped: Bool = false
    
        var body: some View {
            let magnify = MagnificationGesture(minimumScaleDelta: 0.2)
                .onChanged { value in
                    let resolvedDelta = value / self.lastValue
                    self.lastValue = value
                    let newScale = self.scale * resolvedDelta
                    self.scale = min(self.maxScale, max(self.minScale, newScale))
    
                    print("delta=\(value) resolvedDelta=\(resolvedDelta)  newScale=\(newScale)")
            }
    
            let gestureDrag = DragGesture(minimumDistance: 0, coordinateSpace: .local)
                .onChanged { (value) in
                    self.tapPoint = value.startLocation
                    self.draged = CGSize(width: value.translation.width + self.prevDraged.width,
                                         height: value.translation.height + self.prevDraged.height)
            }
    
            return GeometryReader { geo in
                    Image("dooli")
                        .resizable().scaledToFit().animation(.default)
                        .offset(self.draged)
                        .scaleEffect(self.scale)
    //                    .scaleEffect(self.isTapped ? 2 : 1,
    //                                 anchor: UnitPoint(x: self.tapPoint.x / geo.frame(in: .local).maxX,
    //                                                   y: self.tapPoint.y / geo.frame(in: .local).maxY))
                        .gesture(
                            TapGesture(count: 2).onEnded({
                                self.isTapped.toggle()
                                if self.scale > 1 {
                                    self.scale = 1
                                } else {
                                    self.scale = 2
                                }
                                let parent = geo.frame(in: .local)
                                self.postArranging(translation: CGSize.zero, in: parent)
                            })
                            .simultaneously(with: gestureDrag.onEnded({ (value) in
                                let parent = geo.frame(in: .local)
                                self.postArranging(translation: value.translation, in: parent)
                            })
                        ))
                        .gesture(magnify.onEnded { value in
                            // without this the next gesture will be broken
                            self.lastValue = 1.0
                            let parent = geo.frame(in: .local)
                            self.postArranging(translation: CGSize.zero, in: parent)
                        })
                }
                .frame(height: 300)
                .clipped()
                .background(Color.gray)
    
        }
    
        private func postArranging(translation: CGSize, in parent: CGRect) {
            let scaled = self.scale
            let parentWidth = parent.maxX
            let parentHeight = parent.maxY
            let offset = CGSize(width: (parentWidth * scaled - parentWidth) / 2,
                                height: (parentHeight * scaled - parentHeight) / 2)
    
            print(offset)
            var resolved = CGSize()
            let newDraged = CGSize(width: self.draged.width * scaled,
                                   height: self.draged.height * scaled)
            if newDraged.width > offset.width {
                resolved.width = offset.width / scaled
            } else if newDraged.width < -offset.width {
                resolved.width = -offset.width / scaled
            } else {
                resolved.width = translation.width + self.prevDraged.width
            }
            if newDraged.height > offset.height {
                resolved.height = offset.height / scaled
            } else if newDraged.height < -offset.height {
                resolved.height = -offset.height / scaled
            } else {
                resolved.height = translation.height + self.prevDraged.height
            }
            self.draged = resolved
            self.prevDraged = resolved
        }
    
    }
    
    0 讨论(0)
  • 2020-12-28 08:58

    Here's an alternative approach to @James and @ethoooo 's. The final zoom state and the transient gesture state are kept separate (the transient will always return 1), so it's a state you can set from a button or stepper for example in addition to the gesture itself.

      @State var scrollContentZoom: CGFloat = 1
      @GestureState var scrollContentGestureZoom: CGFloat = 1
      var contentZoom: CGFloat { scrollContentZoom*scrollContentGestureZoom }
      
      var magnification: some Gesture {
        MagnificationGesture()
          .updating($scrollContentGestureZoom) { state, gestureState, transaction in
            print("Magnifed: \(state)")
            gestureState = state
          }
          .onEnded { (state) in
            scrollContentZoom = contentZoom*state
          }
      }
    
    0 讨论(0)
提交回复
热议问题