在SwiftUI中,难道没有一种简单的方法可以通过捏合手势来放大图像吗?

79
我想在SwiftUI中实现调整大小和移动图像的功能(就像地图一样),可以通过捏合缩放和拖动来完成。
在UIKit中,我将图像嵌入到UIScrollView中,它会自动处理这些操作,但是我不知道如何在SwiftUI中实现。 我尝试使用MagnificationGesture,但无法使其平滑运行。
我已经搜索了一段时间,有人知道是否有更简单的方法吗?

1
做同样的事情,将图像添加到滚动视图中。在SwiftUI中仍然可以使用滚动视图。 - Scriptable
我不知道SwiftUI中的ScrollView是否原生支持缩放,或者我只是找不到它。我有这个代码,但它的行为很奇怪。ScrollView(Axis.Set(arrayLiteral: [.horizontal, .vertical]), showsIndicators: false) { Image("building").resizable().scaledToFit().scaleEffect(self.scale) } .gesture(MagnificationGesture().onChanged {scale in self.scale = scale }) - Zheoni
14
仍在寻找答案的人,唯一有效的答案来自jtbandes-尽管建议您查看他在GitHub上的工作示例。我已经查看了网络上关于如何在SwiftUI中本地执行此操作的所有文章,但这是不可能的。正确计算各种变量(中心,锚点,边界等)所需的状态以及合并拖放和缩放的能力在SwiftUI 2.0版本中不存在。我已经为图像实现了jtbandes的解决方案,它非常好用,就像苹果相册一样。Pastebin链接:https://pastebin.com/embed_js/rpSRTddm - RPSM
同意RPSM所说,SwiftUI在2023年仍然不够灵活,无法本地实现此操作。特别是在Web图像方面(您确实必须进行黑客攻击才能在图像加载后获得基本框架尺寸),浪费了将近一周的时间。 UIScrollView方法是唯一可行的方法,适用于生产就绪产品。 - jusko
18个回答

89

这里的其他答案都过于复杂,使用自定义缩放逻辑。如果你想要标准、经过实战检验的UIScrollView缩放行为,你可以直接使用UIScrollView

SwiftUI允许你使用UIViewRepresentableUIViewControllerRepresentable将任何UIView放置在另一个SwiftUI视图层次结构中。然后,为了在该视图中放置更多的SwiftUI内容,你可以使用UIHostingController。在与UIKit交互API文档中了解有关SwiftUI-UIKit交互的更多信息。

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

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
    }
  }
}

5
我知道这个方法可行,但我想你误解了问题。我在询问如何使用SwiftUI轻松地进行“本地化”操作,而不使用UIKit。无论如何,感谢您的回复!这可能对许多人有用。 - Zheoni
13
希望 SwiftUI 的 ScrollView 尽快添加这个功能,但同时我认为在没有这个功能之前,将一些 UIKit 混合进去的解决方案并不比“本地”的(纯SwiftUI的)解决方案更优。逃生口是有原因的!我将在我的项目中使用这个方法 :) - jtbandes
3
很棒的样例!我一直在尝试使用原生的SwiftUI,甚至是带手势的UIKit视图,但是scaleEffect功能不正常。然而,当我尝试使用你的代码时,放置图片时出现了非常奇怪的“跳跃”或“突然”的现象。我不认为这与图片有任何关系,但你的代码很简单。你有过类似的经历吗? - RPSM
2
是的,我根据那个和其他定位问题实际上做了一些重大更改。发现这篇文章很有帮助。您可以在https://github.com/jtbandes/space-pics/blob/main/APOD/ZoomableScrollView.swift看到我的最新版本,其中包括UIScrollView的子类 - 它不完美,但现在我知道了关于捕捉问题的问题,我实际上也在一些其他应用程序中看到了它们。我惊讶地发现这很难做到,并且UIScrollView默认情况下不容易做到。 - jtbandes
3
我无法感谢你的足够,这完全拯救了我!我使用了你上面发布的代码,并对其进行了一些修改以适应我的需求,即我添加了一个UIImageView而不是嵌入式SwiftUI视图,这就是我所需要的全部。我必须考虑安全区域等问题,但结果非常棒。我看了看你的NASA照片项目,当我有更多时间时,我会尝试将其集成到我的其他非图像项目中。再次感谢你。 - RPSM
显示剩余10条评论

44

SwiftUI的API在这里相当无用:onChanged提供了相对于当前缩放手势开始的数字,而在回调中没有明显的方法可以获取初始值。还有一个onEnded回调,但很容易被忽略/遗忘。

一个解决方法是添加:

@State var lastScaleValue: CGFloat = 1.0

然后在回调函数中:

.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
})

如果您直接设置标度,它将变得混乱,因为每次tick的数量都将与先前的数量相关。新比例尺是您自己跟踪的比例(可能是状态或绑定)。


实际上,这更加自然。现在我很好地理解了onChange值和onEnded值。谢谢!:D - Zheoni
1
最后一行缺少一个 ")"。 - Dikey
self.scale 定义在哪里? - Hussein
无法与选项卡栏一起使用 - undefined

38

这里是一种在SwiftUI视图中添加缩放功能的方法。它通过在UIViewRepresentable中叠加一个带有UIPinchGestureRecognizerUIView,并使用绑定将相关值转发回SwiftUI。

您可以按照以下方式添加此行为:

Image("Zoom")
    .pinchToZoom()

这将添加类似于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())
    }
}

1
嗨,我想做更像Facebook的捏合功能,可以保持缩放并且可以滚动。我尝试通过在“pinch”函数中将“.ended”与“.canceled”和“.failed”分开,并从“.ended”情况中删除“scale”来调整您的代码。通过这个改变,我能够保持比例,但我无法缩小(使用捏合)也无法四处查看,请问我错过了什么?我已经多次扫描了您的代码,但是无法找出还需要什么。至少在我的想法中,捏出应该可以工作,然后再添加拖动手势。 - Pedro Cavaleiro
嗨 @PedroCavaleiro。我不认为这段代码适用于此用例。我建议你改用 UIScrollView 及其缩放功能。 - Avario
2
我真的很喜欢这个解决方案,但它会阻止内容视图中所有SwiftUI手势识别器的工作。如果内容视图是可表示的,则似乎也会阻止其所有UIGestureRecognizers的工作。 - hidden-username
1
你刚刚救了我的命!!谢谢 :) - bhakti123
你觉得能否在你的类中添加双指翻译功能?这将允许以用户定义的中心进行缩放。对于绘图应用程序非常有用! - Paul Ollivier
3
如果我们需要在容器中缩放单个图像/视图,那么这是一个不错的解决方案。但是,如果我们在VStack中有多个图像,则交互的图像始终显示在基于VStack序列的下一个图像下面。有人注意到了吗? - Sunil Targe

28

我认为值得一提的极其简单的方法是 - 使用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
    }
}

优点:

  • 不需要逻辑
  • 感觉专业
  • 由苹果编写(不太可能在将来中断)

如果您只是为了查看图像而呈现图像,则此方法可能非常适合您。但是,如果您想添加图像注释等内容,我建议您遵循其他答案中的一个。

编辑以添加view.autoScales = true按照maka的建议。


1
你说得对,这对我来说非常完美。谢谢! - toddler
1
想要补充的是,这对我解决了问题,但这一行帮助了我,因为我的图像从一个巨大的缩放开始(它是一张高分辨率图像):view.autoScales = true - maka
1
谢谢提醒,这解决了我的问题。干杯! - Mahmud Ahsan
1
这解决了我的问题。谢谢。 - indra
4
这真的是目前为止最好的方法。其他方法都感觉笨重且代码太多了...谢谢。 - Nico S.
显示剩余3条评论

19

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

image

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
    }
}

此外,我在GitHub上有一个Swift Package解决方案,链接在这里


1
还可以在 macOS 上使用,大多数其他答案都无法做到。 - DudeOnRock
有没有办法在双击时添加缩放功能?我认为它与拖动手势产生了冲突。 - Javier Heisecke
在这个页面上尝试了几种解决方案。这绝对是最优雅和流畅的解决方案。非常Swifty。谢谢! - DeveloperSammy
这是一个不错的Swifty解决方案,但在分页的TabView中由于拖动手势而无法工作。 - Gerry Shaw
不错的解决方案,但是发现一个奇怪的效果:当视图关闭时,图片仍然显示一段时间,并且过渡效果不理想。 - David
显示剩余4条评论

13

其他答案也不错,这里再给一个提示:如果你正在使用SwiftUI手势,可以使用@GestureState来代替@State来存储手势状态。它会在手势结束后自动将状态重置为初始值,因此您可以简化此类代码:

其他答案没问题,我还有一个小技巧:如果你使用 SwiftUI 手势,可以使用 @GestureState 替代 @State 来存储手势状态。它会在手势结束后自动将状态重置为初始值,从而可以简化此类代码:

@State private var scale: CGFloat = 1.0

.gesture(MagnificationGesture().onChanged { value in
  // Anything with value
  scale = value
}.onEnded { value in
  scale = 1.0
})

使用:

@GestureState private var scale: CGFloat = 1.0

.gesture(MagnificationGesture().updating($scale) { (newValue, scale, _) in
  // Anything with value
  scale = newValue
})

看起来很优雅,谢谢! - Olexander Korenyuk

8
我的个人意见。我进行了搜索,并从iOSCretor存储库(https://github.com/ioscreator/ioscreator,感谢Arthur Knopper!)找到了解决方案。
我稍微修改并复制到这里,以方便使用,并添加了重置方法。
技术上,我们:
  1. add image with scale and state.

  2. add 2 gestures that work simultaneously

  3. add also a "reset" via double tap

    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)
    
         }
    
     }
    
为了您的方便,这是一个 GIST: https://gist.github.com/ingconti/124d549e2671fd91d86144bc222d171a

如果没有缩放,防止拖动会更好:简单来说,在条件下,gestureState = ...和offset.height += ...,如果scale> 1.0 {。 - ingconti
这真的很好。如果没有缩放,您会如何建议传递绑定值以通过拖动来解除图像? - Robert
这很棒,但似乎缩放总是从中心发生?例如,与您捏合的位置无关。不确定是否有简单的解决方法... 您可能需要在scaleState更改时调整offsetState,但数学超出了我的能力范围... - Ruben Martinez Jr.
更新:为了使图像从当前偏移量缩放,您可以像这样更新偏移量: offset = CGSize(width: offset.width * (newScale / oldScale), height: offset.height * (newScale / oldScale))。您将不得不在另一个 zoomGestureupdating 块中更新手势状态,然后在 onEnded 中更新 offset - Ruben Martinez Jr.
之前的建议都很好! - undefined

6
看起来SwiftUI的ScrollView没有本地支持,但是仍然有一种相当简单的方法可以实现。创建一个MagnificationGesture,像你想要的那样,但一定要确保将当前比例乘以手势的.onChanged闭包中的值。该闭包提供给您的是缩放的变化量而不是当前比例值。
当您处于缩小状态并开始缩放时,它不会从当前比例(例如0.5到0.6)增加,而是从1增加到1.1。这就是为什么您看到奇怪行为的原因。
如果MagnificationGesture在具有.scaleEffect的同一视图上,则此答案将起作用。否则,James的答案将更好。
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)
    }
}

附言:你可能会发现,使用 ScrollView 来实现此目的很笨重,并且无法同时拖动和缩放。如果是这种情况,而您对此不满意,我建议您考虑添加多个手势并手动调整内容的偏移量,而不是使用 ScrollView


3
我觉得这个方法行不通。回调函数中的比例尺是相对于手势开始的。所以每次回调函数都乘以差值会搞砸一些东西,例如如果你将比例扩大到两倍,则在每个周期内它都会使你的比例扩大到两倍。这可能不是你想要的。 - James
在某些情况下是正确的。这取决于你如何设置你的层次结构。如果手势不在正在缩放的视图上,那么你需要使用你的答案;如果手势在正在缩放的相同视图上,我的答案就可以解决问题 :) - ethoooo

6

我也在为这个问题苦苦挣扎。但是有一些工作样本是通过这个视频制作的-(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
    }

}

2
希望苹果公司未来能够提供一种标准且简单的方法来执行这些拖动操作。请注意,在SwiftUI的最新版本中,simultaneously已更名为simultaneousGesture - Zhou Haibo

5

这是基于jtbandes答案的另一种解决方案。它仍然将UIScrollView包装在UIViewRepresentable中,但有几个变化:

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

使用:

struct EncompassingView: View {
    let uiImage: UIImage

    var body: some View {
        GeometryReader { geometry in
            ZoomableView(uiImage: uiImage, viewSize: geometry.size)
        }
    }
}

定义:

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)
        }
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接