【问题标题】:Isn't there an easy way to pinch to zoom in an image in Swiftui?在 Swiftui 中是否有一种简单的方法可以通过捏合来放大图像?
【发布时间】:2019-10-11 13:00:32
【问题描述】:

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

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

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

【问题讨论】:

  • 做同样的事情,将图像添加到滚动视图中。 SwiftUI 仍然可以使用滚动视图
  • 不知道 SwiftUI 中的 ScrollView 是否原生支持缩放。或者我只是找不到它。我得到了这个,但它的行为很奇怪。 ScrollView(Axis.Set(arrayLiteral: [.horizo​​ntal, .vertical]), showsIndicators: false) { Image("building").resizable().scaledToFit().scaleEffect(self.scale) } .gesture(MagnificationGesture() .onChanged {scale in self.scale = scale })
  • 对于仍在寻找的任何人,唯一有效的答案来自 jtbandes - 尽管我建议您查看他在 GitHub 中的工作示例。我已经查看了网络上的每篇文章,了解如何在本地执行此操作,但这根本不可能。正确计算各种变量(中心、锚点、边界等)所需的状态以及将拖动和缩放合并在一起的能力,在 SwiftUI 2.0 版本中根本不存在。我已经为图像实现了 jtbandes 解决方案,它工作得非常好,就像 Apple Photos 一样。 Pastebin 链接:pastebin.com/embed_js/rpSRTddm

标签: swift image scrollview swiftui pinchzoom


【解决方案1】:

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

SwiftUI 允许您使用 UIViewRepresentableUIViewControllerRepresentable 将任何 UIView 放入其他 SwiftUI 视图层次结构中。然后要将更多 SwiftUI 内容放入该视图中,您可以使用 UIHostingController。在Interfacing with UIKitAPI docs 中阅读有关 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
    }
  }
}

【讨论】:

  • 这行得通,我知道,但我认为你误解了这个问题。我在寻求一种简单的“原生”方式来使用 SwiftUI,而不使用 UIKit。无论如何感谢您的回复!可能对许多人有用。
  • 我确实希望 SwiftUI 的 ScrollView 尽快获得此功能,但与此同时,我认为没有太多理由更喜欢“本机”(纯 SwiftUI)解决方案而不是混合一些 UIKit 的解决方案。逃生舱的存在是有原因的!我将在我的项目中使用它:)
  • 很棒的样品!我一直在尝试使用原生 SwiftUI 甚至是带有手势的 UIKit 视图,而 scaleEffect 刚刚损坏。但是,尝试使用您的代码时,我会在放置图像时得到一个非常奇怪的“跳跃”或“捕捉”。我认为图像与它没有任何关系,但是您的代码非常简单。你有过这样的经历吗?
  • 感激不尽,这完全救了我!我采用了您在上面发布的代码并进行了一些修改以供我使用,即我添加了一个 UIImageView 而不是嵌入式 SwiftUI 视图,这就是我所需要的。我不得不迎合安全区域等,但结果非常棒。我看了你的美国宇航局照片项目,当我有更多时间时,我会考虑将它整合到我的其他非图像项目中。再次,谢谢。
  • 完美!谢谢
【解决方案2】:

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
}

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

【讨论】:

  • 它实际上更...自然。现在我很好地理解了 onChange 值和 onEnded 值。谢谢! :D
【解决方案3】:

这是向 SwiftUI 视图添加捏合缩放的一种方法。它将UIViewUIPinchGestureRecognizer 覆盖在UIViewRepresentable 中,并通过绑定将相关值转发回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())
    }
}

【讨论】:

  • 嗨,我打算做一些更像 Facebook 捏的东西,女巫保持放大,你可以滚动。我首先尝试在pinch 函数中调整您的代码,将.ended.canceled.failed 分开,并从.ended 案例中删除scale。有了这个改变,我可以保持比例,但我不能缩小(使用捏)也不能环顾四周,你能指出我错过了什么吗?我扫描了您的代码几次,无法弄清楚还需要什么。在我看来,至少捏出应该可以工作,然后只需添加拖动手势。
  • 嗨@PedroCavaleiro。我认为这段代码不适用于这个用例。我建议您改用 UIScrollView 及其缩放功能。
  • 我真的很喜欢这个解决方案,但它会阻止内容视图中的所有 SwiftUI 手势识别器。如果内容视图是可表示的,它似乎也阻止了它的所有 UIGestureRecognizers。
  • 你刚刚救了我的命!!谢谢你:)
  • 您认为您可以在您的班级中添加 2 个手指翻译吗?这将允许在用户定义的中心进行缩放。例如,对于绘图应用程序非常有用!
【解决方案4】:

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

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

【讨论】:

  • 我认为这行不通。回调的比例是相对的,所以手势的开始。因此,将每个回调乘以 delta 会使事情变得混乱,例如如果你扩大到两倍大小,那么在每个刻度上它都会让你的比例加倍。可能不是你想要的。
  • 在某些情况下确实如此。这取决于您如何设置层次结构。如果手势不在缩放的视图上,您​​将需要使用您的答案,如果手势在缩放的同一视图上,我的回答可以解决问题:)
【解决方案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
    }

}

【讨论】:

  • 希望苹果以后能提供一种标准简单的方式来做这些拖拽操作。注意,在 SwiftUI 最新版本中,simultaneously 被重命名为 simultaneousGesture
【解决方案6】:

其他答案很好,这里有一个额外的提示:如果您使用的是 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
})

【讨论】:

    【解决方案7】:

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

    优点:

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

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

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

    【讨论】:

    • 你说得对,这对我来说是完美的。谢谢!
    • 想补充一点,它为我解决了这个问题,但这条线有帮助,因为我的图像以一个巨大的缩放开始(这是一个高分辨率图像):view.autoScales = true
    • 缩放看起来不错,但不能拖动,它总是会回到中心。
    • 感谢您的提及。它解决了我的问题。干杯!
    • 这确实是最好的方法。其他一切都感觉很笨拙,代码太多......谢谢
    【解决方案8】:

    这是@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))
        
        }
    }
    

    我在my GitHub 有一个更好的版本。

    【讨论】:

      【解决方案9】:

      这是另一种解决方案,基于 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)
              }
          }
      }
      

      【讨论】:

        【解决方案10】:

        这是@James 和@ethoooo 的另一种方法。最终缩放状态和瞬态手势状态是分开的(瞬态将始终返回 1),因此除了手势本身之外,您还可以通过按钮或步进器设置该状态。

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

        【讨论】:

          【解决方案11】:
          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")
                  }
              }
          }
          

          【讨论】:

          • 虽然这段代码 sn-p 可以解决问题,但including an explanation 确实有助于提高帖子的质量。请记住,您正在为将来的读者回答问题,而这些人可能不知道您的代码建议的原因。也请尽量不要用解释性的 cmets 挤满你的代码,这会降低代码和解释的可读性!
          • 您的示例代码包含很多与问题无关的代码。这只会让我们更难提取重要部分。
          猜你喜欢
          • 1970-01-01
          • 2018-09-10
          • 2010-09-19
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2023-04-08
          • 2013-07-29
          相关资源
          最近更新 更多