在 Swiftui 中没有一种简单的方法可以通过捏来放大图像吗?

Zhe*_*oni 27 image scrollview pinchzoom swift swiftui

我希望能够在 SwiftUI 中调整大小和移动图像(就像它是地图一样),并通过捏来缩放和拖动它。

使用 UIKit 我将图像嵌入到 a 中UIScrollView并处理它,但我不知道如何在 SwiftUI 中执行此操作。我尝试使用,MagnificationGesture但我无法让它顺利工作。

我已经搜索了一段时间了,有没有人知道是否有更简单的方法?

Jar*_*ren 30

我认为值得一提的极其简单的方法 - 使用 Apple 的PDFKit.

import SwiftUI
import PDFKit

struct PhotoDetailView: UIViewRepresentable {
    let image: UIImage

    func makeUIView(context: Context) -> PDFView {
        let view = PDFView()
        view.document = PDFDocument()
        guard let page = PDFPage(image: image) else { return view }
        view.document?.insert(page, at: 0)
        view.autoScales = true
        return view
    }

    func updateUIView(_ uiView: PDFView, context: Context) {
        // empty
    }
}
Run Code Online (Sandbox Code Playgroud)

优点:

  • 需要 0 个逻辑
  • 感觉很专业
  • 由 Apple 编写(将来不太可能中断)

如果您只是展示图像以供查看,那么此方法可能非常适合您。但如果您想添加图像注释等,我会遵循其他答案之一。

根据 maka 的建议进行编辑添加view.autoScales = true

  • 这确实是最好的方法。其他一切都感觉很笨拙并且代码太多......谢谢 (4认同)

Jam*_*mes 29

SwiftUI API 在这里非常无用: onChanged 给出了相对于当前缩放手势开始的数字,并且在回调中没有明显的方法来获取初始值。并且有一个 onEnded 回调但很容易错过/忘记。

解决方法,添加:

@State var lastScaleValue: CGFloat = 1.0
Run Code Online (Sandbox Code Playgroud)

然后在回调中:

.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
}
Run Code Online (Sandbox Code Playgroud)

其中 newScale 是您自己的比例跟踪(可能是状态或绑定)。如果你直接设置你的比例,它会变得一团糟,因为在每个刻度上,数量将相对于之前的数量。


jtb*_*des 28

这里的其他答案对于自定义缩放逻辑过于复杂。如果你想要标准的、久经考验的 UIScrollView 缩放行为,你可以只使用 UIScrollView

SwiftUI 允许您使用UIViewRepresentable或将任何 UIView 放在 SwiftUI 视图层次结构中UIViewControllerRepresentable。然后要将更多 SwiftUI 内容放入该视图中,您可以使用UIHostingController. 在接口与 UIKitAPI 文档中阅读有关 SwiftUI–UIKit 互操作的更多信息。

你可以找到一个更完整的例子,我在一个真实的应用程序中使用它:https : //github.com/jtbandes/SpacePOD/blob/main/SpacePOD/ZoomableScrollView.swift(该例子还包括更多的技巧来使图片。)

var body: some View {
  ZoomableScrollView {
    Image("Your image here")
  }
}

Run Code Online (Sandbox Code Playgroud)
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
    }
  }
}
Run Code Online (Sandbox Code Playgroud)

  • 我确实希望 SwiftUI 的 ScrollView 尽快获得此功能,但与此同时,我认为没有太多理由更喜欢“本机”(纯 SwiftUI)解决方案而不是混合在某些 UIKit 中的解决方案。逃生舱口的存在是有原因的!我将在我的项目中使用它:) (7认同)
  • 这有效,我知道,但我认为你误解了这个问题。我要求一种简单的“原生”方法来使用 SwiftUI 来完成此操作,而不使用 UIKit。不管怎样,谢谢你的回复!可能对很多人有用。 (6认同)
  • 很棒的样品!我一直在尝试让一些东西与本机 SwiftUI 甚至带有手势的 UIKit 视图一起使用,但scaleEffect刚刚被破坏了。但是,尝试使用您的代码时,当我放置图像时,我会得到一个非常奇怪的“跳跃”或“捕捉”。我不认为图像与它有任何关系,但你的代码非常简单。你经历过类似的事情吗? (3认同)
  • 我对你感激不尽,这彻底拯救了我!我采用了您上面发布的代码并做了一些修改以供我使用,即我添加了 UIImageView 而不是嵌入式 SwiftUI View,这就是我所需要的。我必须考虑安全区域等,但结果非常棒。我看了你的美国宇航局照片项目,当我有更多时间时,我会考虑将其整合到我的其他非图像项目中。再次谢谢你。 (3认同)
  • 是的,我实际上根据这个问题和其他定位问题做出了一些重大改变。发现[本文](https://medium.com/@ssamadgh/designing-apps-with-scroll-views-part-i-8a7a44a5adf7)很有帮助。您可以在 https://github.com/jtbandes/space-pics/blob/main/APOD/ZoomableScrollView.swift 中查看我的最新版本,其中包含 UIScrollView 的子类 - 它并不完美,但现在我知道了捕捉问题实际上我也在其他一些应用程序中看到了它们。我很震惊地发现要做到这一点是多么困难,而且 UIScrollView 默认情况下不容易做到这一点。 (2认同)

小智 25

这是我的解决方案,它可以像苹果的照片应用程序一样进行图像缩放。

图像

import SwiftUI

public struct SwiftUIImageViewer: View {

    let image: Image

    @State private var scale: CGFloat = 1
    @State private var lastScale: CGFloat = 1

    @State private var offset: CGPoint = .zero
    @State private var lastTranslation: CGSize = .zero

    public init(image: Image) {
        self.image = image
    }

    public var body: some View {
        GeometryReader { proxy in
            ZStack {
                image
                    .resizable()
                    .aspectRatio(contentMode: .fit)
                    .scaleEffect(scale)
                    .offset(x: offset.x, y: offset.y)
                    .gesture(makeDragGesture(size: proxy.size))
                    .gesture(makeMagnificationGesture(size: proxy.size))
            }
            .frame(maxWidth: .infinity, maxHeight: .infinity)
            .edgesIgnoringSafeArea(.all)
        }
    }

    private func makeMagnificationGesture(size: CGSize) -> some Gesture {
        MagnificationGesture()
            .onChanged { value in
                let delta = value / lastScale
                lastScale = value

                // To minimize jittering
                if abs(1 - delta) > 0.01 {
                    scale *= delta
                }
            }
            .onEnded { _ in
                lastScale = 1
                if scale < 1 {
                    withAnimation {
                        scale = 1
                    }
                }
                adjustMaxOffset(size: size)
            }
    }

    private func makeDragGesture(size: CGSize) -> some Gesture {
        DragGesture()
            .onChanged { value in
                let diff = CGPoint(
                    x: value.translation.width - lastTranslation.width,
                    y: value.translation.height - lastTranslation.height
                )
                offset = .init(x: offset.x + diff.x, y: offset.y + diff.y)
                lastTranslation = value.translation
            }
            .onEnded { _ in
                adjustMaxOffset(size: size)
            }
    }

    private func adjustMaxOffset(size: CGSize) {
        let maxOffsetX = (size.width * (scale - 1)) / 2
        let maxOffsetY = (size.height * (scale - 1)) / 2

        var newOffsetX = offset.x
        var newOffsetY = offset.y

        if abs(newOffsetX) > maxOffsetX {
            newOffsetX = maxOffsetX * (abs(newOffsetX) / newOffsetX)
        }
        if abs(newOffsetY) > maxOffsetY {
            newOffsetY = maxOffsetY * (abs(newOffsetY) / newOffsetY)
        }

        let newOffset = CGPoint(x: newOffsetX, y: newOffsetY)
        if newOffset != offset {
            withAnimation {
                offset = newOffset
            }
        }
        self.lastTranslation = .zero
    }
}
Run Code Online (Sandbox Code Playgroud)

另外,我在 GitHub 中将此解决方案作为 Swift Package提供


Ava*_*rio 22

这是向 SwiftUI 视图添加捏合缩放的一种方法。它覆盖一个UIViewUIPinchGestureRecognizer一个UIViewRepresentable,并转发相关的值回SwiftUI绑定的。

您可以添加这样的行为:

Image("Zoom")
    .pinchToZoom()
Run Code Online (Sandbox Code Playgroud)

这增加了类似于在 Instagram 提要中缩放照片的行为。这是完整的代码:

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())
    }
}
Run Code Online (Sandbox Code Playgroud)

  • 如果我们需要缩放容器中的单个图像/视图,这是一个很好的解决方案。但是,如果我们在 VStack 中有多个图像,则交互的图像始终显示在基于 VStack 序列的下一个图像下方。有人注意到吗? (3认同)
  • 我真的很喜欢这个解决方案,但它阻止了内容视图中的所有 SwiftUI 手势识别器。如果内容视图是可表示的,它似乎也会阻止其所有 UIGestureRecognizer。 (2认同)

Lou*_*Lac 14

其他答案都可以,这里有一个额外的提示:如果您使用 SwiftUI 手势,您可以使用 a@GestureState而不是 a@State来存储手势状态。手势结束后,它会自动将状态重置为初始值,因此您可以简化此类代码:

@State private var scale: CGFloat = 1.0

.gesture(MagnificationGesture().onChanged { value in
  // Anything with value
  scale = value
}.onEnded { value in
  scale = 1.0
})
Run Code Online (Sandbox Code Playgroud)

和:

@GestureState private var scale: CGFloat = 1.0

.gesture(MagnificationGesture().updating($scale) { (newValue, scale, _) in
  // Anything with value
  scale = newValue
})
Run Code Online (Sandbox Code Playgroud)


ing*_*nti 8

我的两分钱。我确实搜索并找到了解决方案:iOSCretor repo(https://github.com/ioscreator/ioscreator,感谢 Arthur Knopper!)

我在这里稍微修改了一下,复制过来,为了方便,添加了reset方法。

从技术上讲我们:

  1. 添加具有比例和状态的图像。

  2. 添加 2 个同时工作的手势

  3. 还可以通过双击添加“重置”

    import SwiftUI
    
     struct ContentView: View {
    
    
         @GestureState private var scaleState: CGFloat = 1
         @GestureState private var offsetState = CGSize.zero
    
         @State private var offset = CGSize.zero
         @State private var scale: CGFloat = 1
    
         func resetStatus(){
             self.offset = CGSize.zero
             self.scale = 1
         }
    
         init(){
             resetStatus()
         }
    
         var zoomGesture: some Gesture {
             MagnificationGesture()
                 .updating($scaleState) { currentState, gestureState, _ in
                     gestureState = currentState
                 }
                 .onEnded { value in
                     scale *= value
                 }
         }
    
         var dragGesture: some Gesture {
             DragGesture()
                 .updating($offsetState) { currentState, gestureState, _ in
                     gestureState = currentState.translation
                 }.onEnded { value in
                     offset.height += value.translation.height
                     offset.width += value.translation.width
                 }
         }
    
         var doubleTapGesture : some Gesture {
             TapGesture(count: 2).onEnded { value in
                 resetStatus()
             }
         }
    
    
         var body: some View {
             Image(systemName: "paperplane")
                 .renderingMode(.template)
                 .resizable()
                 .foregroundColor(.red)
                 .scaledToFit()
                 .scaleEffect(self.scale * scaleState)
                 .offset(x: offset.width + offsetState.width, y: offset.height + offsetState.height)
                 .gesture(SimultaneousGesture(zoomGesture, dragGesture))
                 .gesture(doubleTapGesture)
    
         }
    
     }
    
    Run Code Online (Sandbox Code Playgroud)

为了您的方便,这里有一个 GIST: https: //gist.github.com/ingconti/124d549e2671fd91d86144bc222d171a


eth*_*ooo 6

SwiftUI 的 ScrollView 似乎没有本机支持,但是,仍然有一种非常简单的方法可以做到。

创建一个MagnificationGesture你想要的样子,但一定要把你当前的比例乘以你在手势的.onChanged闭包中得到的值。这个闭包让你改变缩放而不是当前的比例值。

当您缩小并开始放大时,它不会从当前比例增加(任意示例为 0.5 到 0.6),它会从 1 增加到 1.1。这就是为什么你看到奇怪的行为。

如果 位于MagnificationGesture具有.scaleEffect. 否则,詹姆斯的回答会更好。

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)
    }
}
Run Code Online (Sandbox Code Playgroud)

PS 您可能会发现ScrollView为此目的使用 a很笨拙,并且您无法同时拖动和缩放。如果是这种情况并且您对此不满意,我会考虑添加多个手势并手动调整内容的偏移量,而不是使用ScrollView.

  • 我认为这行不通。回调中的比例是相对的,因此手势开始。因此,在每个回调上乘以增量会弄乱事情,例如,如果您扩展到双倍大小,那么在每个刻度上,它都会使您的规模加倍。可能不是你想要的。 (2认同)

Ido*_*Ido 6

最有效、最动态的方式:

我的体验MagnificationGesture非常糟糕,它非常滞后并且消耗了大量的 CPU 和 RAM(就像许多其他解决方案一样)。最好的解决方案是使用基本的UIScrollView.

基于另一个解决方案,我实现了更动态的结构,允许您:

  • View随心所欲地使用它
  • 通过双击放大/缩小
  • 缩放至您双击的特定点

最重要的是- 它已经过测试,可以保证它不会耗尽您的 CPU 和 RAM!

如何使用它:

//  ContentView.swift

import SwiftUI

struct ContentView: View {
    var body: some View {
        ZoomableContainer{
            // Put here any `View` you'd like (e.g. `Image`, `Text`)
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

实施:

//  ZoomableContainer.swift

import SwiftUI

fileprivate let maxAllowedScale = 4.0

struct ZoomableContainer<Content: View>: View {
    let content: Content

    @State private var currentScale: CGFloat = 1.0
    @State private var tapLocation: CGPoint = .zero

    init(@ViewBuilder content: () -> Content) {
        self.content = content()
    }

    func doubleTapAction(location: CGPoint) {
        tapLocation = location
        currentScale = currentScale == 1.0 ? maxAllowedScale : 1.0
    }

    var body: some View {
        ZoomableScrollView(scale: $currentScale, tapLocation: $tapLocation) {
            content
        }
        .onTapGesture(count: 2, perform: doubleTapAction)
    }

    fileprivate struct ZoomableScrollView<Content: View>: UIViewRepresentable {
        private var content: Content
        @Binding private var currentScale: CGFloat
        @Binding private var tapLocation: CGPoint

        init(scale: Binding<CGFloat>, tapLocation: Binding<CGPoint>, @ViewBuilder content: () -> Content) {
            _currentScale = scale
            _tapLocation = tapLocation
            self.content = content()
        }

        func makeUIView(context: Context) -> UIScrollView {
            // Setup the UIScrollView
            let scrollView = UIScrollView()
            scrollView.delegate = context.coordinator // for viewForZooming(in:)
            scrollView.maximumZoomScale = maxAllowedScale
            scrollView.minimumZoomScale = 1
            scrollView.bouncesZoom = true
            scrollView.showsHorizontalScrollIndicator = false
            scrollView.showsVerticalScrollIndicator = false
            scrollView.clipsToBounds = false

            // 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: content), scale: $currentScale)
        }

        func updateUIView(_ uiView: UIScrollView, context: Context) {
            // Update the hosting controller's SwiftUI content
            context.coordinator.hostingController.rootView = content

            if uiView.zoomScale > uiView.minimumZoomScale { // Scale out
                uiView.setZoomScale(currentScale, animated: true)
            } else if tapLocation != .zero { // Scale in to a specific point
                uiView.zoom(to: zoomRect(for: uiView, scale: uiView.maximumZoomScale, center: tapLocation), animated: true)
                // Reset the location to prevent scaling to it in case of a negative scale (manual pinch)
                // Use the main thread to prevent unexpected behavior
                DispatchQueue.main.async { tapLocation = .zero }
            }

            assert(context.coordinator.hostingController.view.superview == uiView)
        }

        // MARK: - Utils

        func zoomRect(for scrollView: UIScrollView, scale: CGFloat, center: CGPoint) -> CGRect {
            let scrollViewSize = scrollView.bounds.size

            let width = scrollViewSize.width / scale
            let height = scrollViewSize.height / scale
            let x = center.x - (width / 2.0)
            let y = center.y - (height / 2.0)

            return CGRect(x: x, y: y, width: width, height: height)
        }

        // MARK: - Coordinator

        class Coordinator: NSObject, UIScrollViewDelegate {
            var hostingController: UIHostingController<Content>
            @Binding var currentScale: CGFloat

            init(hostingController: UIHostingController<Content>, scale: Binding<CGFloat>) {
                self.hostingController = hostingController
                _currentScale = scale
            }

            func viewForZooming(in scrollView: UIScrollView) -> UIView? {
                return hostingController.view
            }

            func scrollViewDidEndZooming(_ scrollView: UIScrollView, with view: UIView?, atScale scale: CGFloat) {
                currentScale = scale
            }
        }
    }
}

Run Code Online (Sandbox Code Playgroud)

  • 惊人的。这对我来说效果最好。 (3认同)

Bro*_*Han 5

我也在为这个问题而挣扎。但是使用此视频制作了一些工作示例-(https://www.youtube.com/watch?v=p0SwXJYJp2U

这还没有完成。很难用锚点进行缩放。希望这是对其他人的暗示。

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
    }

}
Run Code Online (Sandbox Code Playgroud)

  • 希望苹果将来能提供一个标准且简单的方法来完成这些拖动操作。请注意,在 SwiftUI 最新版本中,```simultaneous`` 已重命名为 ```simultaneousGesture```。 (2认同)

sma*_*kus 5

这是 @James 接受的响应的完整示例,它还具有通过调整隐藏矩形来滚动新缩放图像的基本支持,该隐藏矩形根据图像比例调整滚动视图内容的大小:

import SwiftUI

struct EnlargedImage: View {
    var image = UIImage(named: "YourImageName")
    @State var scale: CGFloat = 1.0
    @State var lastScaleValue: CGFloat = 1.0

    var body: some View {
   
        ScrollView([.vertical, .horizontal], showsIndicators: false){
            ZStack{
                
                Rectangle().foregroundColor(.clear).frame(width: image!.size.width * scale, height: image!.size.height * scale, alignment: .center)
                
                Image(uiImage: image!).scaleEffect(scale)
                .gesture(MagnificationGesture().onChanged { val in
                    let delta = val / self.lastScaleValue
                    self.lastScaleValue = val
                    var newScale = self.scale * delta
                    if newScale < 1.0
                    {
                        newScale = 1.0
                    }
                    scale = newScale
                }.onEnded{val in
                    lastScaleValue = 1
                })
                
               
            }
        }.background(Color(.systemBackground).edgesIgnoringSafeArea(.all))
    
    }
}
Run Code Online (Sandbox Code Playgroud)

我的 GitHub中有一个更好的版本。


ati*_*oSE 5

这是另一个解决方案,基于 jtbandes 的答案。它仍然将 a 包裹UIScrollView在 a 中UIViewRepresentable,但有一些更改:

  • 它专门针对UIImageSwiftUI 内容,而不是通用的 SwiftUI 内容:它适用于这种情况,并且不需要包装底层UIImage到 SwiftUI 中Image
  • 它根据自动布局约束来布局图像视图,而不是自动调整大小蒙版
  • 它通过根据当前缩放级别计算顶部和前导约束的适当值,将图像居中于视图中间

使用:

struct EncompassingView: View {
    let uiImage: UIImage

    var body: some View {
        GeometryReader { geometry in
            ZoomableView(uiImage: uiImage, viewSize: geometry.size)
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

定义:

struct ZoomableView: UIViewRepresentable {
    let uiImage: UIImage
    let viewSize: CGSize

    private enum Constraint: String {
        case top
        case leading
    }
    
    private var minimumZoomScale: CGFloat {
        let widthScale = viewSize.width / uiImage.size.width
        let heightScale = viewSize.height / uiImage.size.height
        return min(widthScale, heightScale)
    }
    
    func makeUIView(context: Context) -> UIScrollView {
        let scrollView = UIScrollView()
        
        scrollView.delegate = context.coordinator
        scrollView.maximumZoomScale = minimumZoomScale * 50
        scrollView.minimumZoomScale = minimumZoomScale
        scrollView.bouncesZoom = true
        
        let imageView = UIImageView(image: uiImage)
        scrollView.addSubview(imageView)
        imageView.translatesAutoresizingMaskIntoConstraints = false
        
        let topConstraint = imageView.topAnchor.constraint(equalTo: scrollView.topAnchor)
        topConstraint.identifier = Constraint.top.rawValue
        topConstraint.isActive = true
        
        let leadingConstraint = imageView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor)
        leadingConstraint.identifier = Constraint.leading.rawValue
        leadingConstraint.isActive = true
        
        imageView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor).isActive = true
        imageView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor).isActive = true

        return scrollView
    }
    
    func makeCoordinator() -> Coordinator {
        return Coordinator()
    }
    
    func updateUIView(_ scrollView: UIScrollView, context: Context) {
        guard let imageView = scrollView.subviews.first as? UIImageView else {
            return
        }
        
        // Inject dependencies into coordinator
        context.coordinator.zoomableView = imageView
        context.coordinator.imageSize = uiImage.size
        context.coordinator.viewSize = viewSize
        let topConstraint = scrollView.constraints.first { $0.identifier == Constraint.top.rawValue }
        let leadingConstraint = scrollView.constraints.first { $0.identifier == Constraint.leading.rawValue }
        context.coordinator.topConstraint = topConstraint
        context.coordinator.leadingConstraint = leadingConstraint

        // Set initial zoom scale
        scrollView.zoomScale = minimumZoomScale
    }
}

// MARK: - Coordinator

extension ZoomableView {
    class Coordinator: NSObject, UIScrollViewDelegate {
        var zoomableView: UIView?
        var imageSize: CGSize?
        var viewSize: CGSize?
        var topConstraint: NSLayoutConstraint?
        var leadingConstraint: NSLayoutConstraint?

        func viewForZooming(in scrollView: UIScrollView) -> UIView? {
            zoomableView
        }
        
        func scrollViewDidZoom(_ scrollView: UIScrollView) {
            let zoomScale = scrollView.zoomScale
            print("zoomScale = \(zoomScale)")
            guard
                let topConstraint = topConstraint,
                let leadingConstraint = leadingConstraint,
                let imageSize = imageSize,
                let viewSize = viewSize
            else {
                return
            }
            topConstraint.constant = max((viewSize.height - (imageSize.height * zoomScale)) / 2.0, 0.0)
            leadingConstraint.constant = max((viewSize.width - (imageSize.width * zoomScale)) / 2.0, 0.0)
        }
    }
}
Run Code Online (Sandbox Code Playgroud)


Lea*_*ner 5

SwiftUI 中图像缩放和拖动的实现

struct PhotoViewer: View {
    @State private var uiimage = UIImage(named: "leaf.png")

    @GestureState private var scaleState: CGFloat = 1
    @GestureState private var offsetState = CGSize.zero

    @State private var offset = CGSize.zero
    @State private var scale: CGFloat = 1

    var magnification: some Gesture {
        MagnificationGesture()
            .updating($scaleState) { currentState, gestureState, _ in
                gestureState = currentState
            }
            .onEnded { value in
                scale *= value
            }
    }

    var dragGesture: some Gesture {
        DragGesture()
            .updating($offsetState) { currentState, gestureState, _ in
                gestureState = currentState.translation
            }.onEnded { value in
                offset.height += value.translation.height
                offset.width += value.translation.width
            }
    }

    var body: some View {
        Image(uiImage: uiimage!)
            .resizable()
            .scaledToFit()
            .scaleEffect(self.scale * scaleState)
            .offset(x: offset.width + offsetState.width, y: offset.height + offsetState.height)
            .gesture(SimultaneousGesture(magnification, dragGesture))
    }
}
Run Code Online (Sandbox Code Playgroud)