英文:
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 <= 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))
}
}
after that you just need to call this code like this
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: proxy.size.width, height: proxy.size.height)))
}
}
}
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论