如何制作一个可平移、可缩放的视图,就像Apple地图上的那个?

huangapple go评论67阅读模式
英文:

How can I make a pannable, zoomable View like the one on Apple Maps?

问题

我正在尝试创建一个可以在所有方向上缩放和平移的视图,类似于Apple地图上的视图。

我尝试使用ScrollView([.vertical, .horizontal])来进行平移,并在下面的代码中尝试使缩放部分基本工作。在测试过程中,当放大时平移无法正常工作,甚至缩放效果也不太好。

如果有人能告诉我如何在这里实现任何功能,或者只是指导我走正确的方向,我将不胜感激! - 我在SwiftUI方面有点新手! 😅

英文:

I'm trying to make a View that I can zoom in and out of and pan in all directions, much like the one on Maps by Apple.

I've tried using ScrollView([.vertical, .horizontal]) for the panning, and some code to make the zooming sort-of work (in the code below). During testing, the panning won't work when zoomed in and even the zooming isn't very well made.

import SwiftUI

struct PanAndZoom: View {
    @State var currentZoom: Double = 0
    @State var finalZoom: Double = 0
    var zoom: Double {
        max(min(currentZoom + finalZoom + 1, 5), 0.1)
    }
    var body: some View {
        ScrollView([.horizontal, .vertical]) {
            Group {
                Image("sampleImage")
            }
            .scaleEffect(zoom)
        }
        .gesture(
            MagnificationGesture()
                .onChanged { value in
                    currentZoom = value * 0.5 - 1
                }
                .onEnded { _ in
                    finalZoom += currentZoom
                    currentZoom = 0
                }
        )
    }
}

struct PanAndZoom_Previews: PreviewProvider {
    static var previews: some View {
        PanAndZoom()
    }
}

If anyone could tell me how to achieve anything here or even just guide me in the right direction, that would be greatly appreciated! - I'm a bit of a newbie to SwiftUI! 😅

答案1

得分: 1

你可以使用 uiViewRepresentable 来使用 UIkit 小部件。我附上了一些可能对你有帮助的代码。

import SwiftUI
import UIKit

struct ImageModifier: ViewModifier {
    private var contentSize: CGSize
    private var min: CGFloat = 1.0
    private var max: CGFloat = 3.0
    @State var currentScale: CGFloat = 1.0

    init(contentSize: CGSize) {
        self.contentSize = contentSize
    }

    var doubleTapGesture: some Gesture {
        TapGesture(count: 2).onEnded {
            if currentScale <= min { currentScale = max } else
            if currentScale >= max { currentScale = min } else {
                currentScale = ((max - min) * 0.5 + min) < currentScale ? max : min
            }
        }
    }

    func body(content: Content) -> some View {
        ScrollView([.horizontal, .vertical]) {
            content
                .frame(width: contentSize.width * currentScale, height: contentSize.height * currentScale, alignment: .center)
                .modifier(PinchToZoom(minScale: min, maxScale: max, scale: $currentScale))
        }
        .gesture(doubleTapGesture)
        .animation(.easeInOut, value: currentScale)
    }
}

class PinchZoomView: UIView {
    let minScale: CGFloat
    let maxScale: CGFloat
    var isPinching: Bool = false
    var scale: CGFloat = 1.0
    let scaleChange: (CGFloat) -> Void

    init(minScale: CGFloat,
           maxScale: CGFloat,
         currentScale: CGFloat,
         scaleChange: @escaping (CGFloat) -> Void) {
        self.minScale = minScale
        self.maxScale = maxScale
        self.scale = currentScale
        self.scaleChange = scaleChange
        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

        case .changed, .ended:
            if gesture.scale <= minScale {
                scale = minScale
            } else if gesture.scale >= maxScale {
                scale = maxScale
            } else {
                scale = gesture.scale
            }
            scaleChange(scale)
        case .cancelled, .failed:
            isPinching = false
            scale = 1.0
        default:
            break
        }
    }
}

struct PinchZoom: UIViewRepresentable {
    let minScale: CGFloat
    let maxScale: CGFloat
    @Binding var scale: CGFloat
    @Binding var isPinching: Bool

    func makeUIView(context: Context) -> PinchZoomView {
        let pinchZoomView = PinchZoomView(minScale: minScale, maxScale: maxScale, currentScale: scale, scaleChange: { scale = $0 })
        return pinchZoomView
    }

    func updateUIView(_ pageControl: PinchZoomView, context: Context) { }
}

struct PinchToZoom: ViewModifier {
    let minScale: CGFloat
    let maxScale: CGFloat
    @Binding var scale: CGFloat
    @State var anchor: UnitPoint = .center
    @State var isPinching: Bool = false

    func body(content: Content) -> some View {
        content
            .scaleEffect(scale, anchor: anchor)
            .animation(.spring(), value: isPinching)
            .overlay(PinchZoom(minScale: minScale, maxScale: maxScale, scale: $scale, isPinching: $isPinching))
    }
}

之后,你只需要像这样调用这段代码:

import SwiftUI

struct yourView: View {
    var body: some View {
        GeometryReader { geoMetryProxy in
            Image("yourImage")
                .resizable()
                .frame(width: geoMetryProxy.size.width, height: geoMetryProxy.size.height)
                .scaledToFit()
                .clipShape(Rectangle())
                .modifier(ImageModifier(contentSize: CGSize(width: geoMetryProxy.size.width, height: geoMetryProxy.size.height)))
        }
    }
}
英文:

You can use UIkit widgets using uiViewRepresentable. i am attaching some code that might help you out.

import SwiftUI
import UIKit
struct ImageModifier: ViewModifier {
private var contentSize: CGSize
private var min: CGFloat = 1.0
private var max: CGFloat = 3.0
@State var currentScale: CGFloat = 1.0
init(contentSize: CGSize) {
self.contentSize = contentSize
}
var doubleTapGesture: some Gesture {
TapGesture(count: 2).onEnded {
if currentScale &lt;= min { currentScale = max } else
if currentScale &gt;= max { currentScale = min } else {
currentScale = ((max - min) * 0.5 + min) &lt; currentScale ? max : min
}
}
}
func body(content: Content) -&gt; some View {
ScrollView([.horizontal, .vertical]) {
content
.frame(width: contentSize.width * currentScale, height: contentSize.height * currentScale, alignment: .center)
.modifier(PinchToZoom(minScale: min, maxScale: max, scale: $currentScale))
}
.gesture(doubleTapGesture)
.animation(.easeInOut, value: currentScale)
}
}
class PinchZoomView: UIView {
let minScale: CGFloat
let maxScale: CGFloat
var isPinching: Bool = false
var scale: CGFloat = 1.0
let scaleChange: (CGFloat) -&gt; Void
init(minScale: CGFloat,
maxScale: CGFloat,
currentScale: CGFloat,
scaleChange: @escaping (CGFloat) -&gt; Void) {
self.minScale = minScale
self.maxScale = maxScale
self.scale = currentScale
self.scaleChange = scaleChange
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
case .changed, .ended:
if gesture.scale &lt;= minScale {
scale = minScale
} else if gesture.scale &gt;= maxScale {
scale = maxScale
} else {
scale = gesture.scale
}
scaleChange(scale)
case .cancelled, .failed:
isPinching = false
scale = 1.0
default:
break
}
}
}
struct PinchZoom: UIViewRepresentable {
let minScale: CGFloat
let maxScale: CGFloat
@Binding var scale: CGFloat
@Binding var isPinching: Bool
func makeUIView(context: Context) -&gt; PinchZoomView {
let pinchZoomView = PinchZoomView(minScale: minScale, maxScale: maxScale, currentScale: scale, scaleChange: { scale = $0 })
return pinchZoomView
}
func updateUIView(_ pageControl: PinchZoomView, context: Context) { }
}
struct PinchToZoom: ViewModifier {
let minScale: CGFloat
let maxScale: CGFloat
@Binding var scale: CGFloat
@State var anchor: UnitPoint = .center
@State var isPinching: Bool = false
func body(content: Content) -&gt; some View {
content
.scaleEffect(scale, anchor: anchor)
.animation(.spring(), value: isPinching)
.overlay(PinchZoom(minScale: minScale, maxScale: maxScale, scale: $scale, isPinching: $isPinching))
}
}

after that you just need to call this code like this

import SwiftUI
struct yourView: View {
var body: some View {
GeometryReader { geoMetryProxy in
Image(&quot;yourImage&quot;)
.resizable()
.frame(width: geoMetryProxy.size.width, height: geoMetryProxy.size.height)
.scaledToFit()
.clipShape(Rectangle())
.modifier(ImageModifier(contentSize: CGSize(width: proxy.size.width, height: proxy.size.height)))
}
}
}

huangapple
  • 本文由 发表于 2023年7月10日 12:20:23
  • 转载请务必保留本文链接:https://go.coder-hub.com/76650665.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定