英文:
SWIFTUI ViewModifier using UIViewRepresentable; getting a value from the UIView to my SwiftUI View
问题
以下是您提供的内容的翻译:
我在 SwiftUI 视图上有一个自定义修饰符,用于平移和缩放图像。我需要获取用户长按的位置。该修饰符在底层使用一个 UIView(UIViewRepresentable),其中将捏合和缩放手势添加到视图中。将长按手势直接添加到修饰符(SwiftUI 视图)与这个捏合和缩放手势冲突。因此,我直接将长按手势添加到了 UIView。这个方法运行得很好。
然而,我需要在我的 ViewModifier 中获取位置(CGPoint),以便绑定到调用视图。我似乎无法弄清楚如何做到这一点...
简而言之:我需要在我的 ViewModifier 中获取 tapLocation 的值,该值是 UIView 中长按的位置!
调用视图:
let arrowPointUp = Image(systemName: "arrowtriangle.up.fill")
struct ContentView: View {
@State private var mapImage = UIImage(named: "worldMap")!
@State private var tapLocation = CGPoint.zero
@State private var height = 0.0
@State private var width = 0.0
var body: some View {
GeometryReader { proxy in
ZStack {
Image(uiImage: mapImage)
.resizable()
.fixedSize()
arrowPointUp
.foregroundColor(.green)
.position(tapLocation)
arrowPointUp
.foregroundColor(.red)
.position(x: 776, y: 1150)
arrowPointUp
.foregroundColor(.blue)
.position(x: 1178, y: 1317)
}
.frame(width: mapImage.size.width, height: mapImage.size.height)
.PinchToZoomAndPan(contentSize: mapImage.size, tapLocation: $tapLocation)
}
}
}
ViewModifier:
import SwiftUI
import UIKit
extension View {
func PinchToZoomAndPan(contentSize: CGSize, tapLocation: Binding<CGPoint>) -> some View {
modifier(PinchAndZoomModifier(contentSize: contentSize, tapLocation: tapLocation))
}
}
struct PinchAndZoomModifier: ViewModifier {
private var contentSize: CGSize
private var min: CGFloat = 0.75 // 1.0
private var max: CGFloat = 3.0
@State var currentScale: CGFloat = 1.0
@Binding var tapLocation: CGPoint
init(contentSize: CGSize, tapLocation: Binding<CGPoint>) {
self.contentSize = contentSize
self._tapLocation = tapLocation
print("ContentSize: \(self.contentSize)")
}
var doubleTapGesture: some Gesture {
TapGesture(count: 2).onEnded {
currentScale = 1.0
}
}
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))
.simultaneousGesture(doubleTapGesture)
}
.animation(.easeInOut, value: currentScale)
}
}
// 以下部分略
您还提供了 GitHub 链接,但我只翻译了您提供的内容,如有需要,请随时提问。
英文:
I have a custom modifier on a SwiftUI View to pan and zoom an Image. I need to get the location where the user long presses. The modifier uses an UIView (UIViewRepresentable) under the hood where the pinch-and-zoom gesture is added to the view. Adding the longPressGesture directly to the modifier (swiftUI View) conflicts with this pinch-and-zoom gesture. Thus I added a longPress gesture directly to the UIView. This works well.
However, I need the location (CGPoint) up the chain in my ViewModifier to for a Binding to the calling View. I can't seem to work out how to do that...
SHORT: I need tapLocation on my ViewModifier to have the value of the. long press in the UIView!
Calling View:
let arrowPointUp = Image(systemName: "arrowtriangle.up.fill")
struct ContentView: View {
@State private var mapImage = UIImage(named: "worldMap")!
@State private var tapLocation = CGPoint.zero
@State private var height = 0.0
@State private var width = 0.0
var body: some View {
GeometryReader { proxy in
ZStack {
Image(uiImage: mapImage)
.resizable()
.fixedSize()
arrowPointUp
.foregroundColor(.green)
.position(tapLocation)
arrowPointUp
.foregroundColor(.red)
.position(x: 776, y: 1150)
arrowPointUp
.foregroundColor(.blue)
.position(x: 1178, y: 1317)
}
.frame(width: mapImage.size.width, height: mapImage.size.height)
.PinchToZoomAndPan(contentSize: mapImage.size, tapLocation: $tapLocation)
}
}
}
The ViewModifier
import SwiftUI
import UIKit
extension View {
func PinchToZoomAndPan(contentSize: CGSize, tapLocation: Binding<CGPoint>) -> some View {
modifier(PinchAndZoomModifier(contentSize: contentSize, tapLocation: tapLocation))
}
}
struct PinchAndZoomModifier: ViewModifier {
private var contentSize: CGSize
private var min: CGFloat = 0.75 // 1.0
private var max: CGFloat = 3.0
@State var currentScale: CGFloat = 1.0
@Binding var tapLocation: CGPoint
init(contentSize: CGSize, tapLocation: Binding<CGPoint>) {
self.contentSize = contentSize
self._tapLocation = tapLocation
print("ContentSize: \(self.contentSize)")
}
var doubleTapGesture: some Gesture {
TapGesture(count: 2).onEnded {
currentScale = 1.0
}
}
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))
.simultaneousGesture(doubleTapGesture)
}
.animation(.easeInOut, value: currentScale)
}
}
// THREE
class PinchZoomView: UIView {
let minScale: CGFloat
let maxScale: CGFloat
var isPinching: Bool = false
var scale: CGFloat = 1.0
let scaleChange: (CGFloat) -> Void
var longPressLocation = CGPoint.zero
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
let longPressGesture = UILongPressGestureRecognizer(target: self, action: #selector(longPress(gesture:)))
addGestureRecognizer(pinchGesture)
addGestureRecognizer(longPressGesture)
}
required init?(coder: NSCoder) {
fatalError()
}
@objc private func longPress(gesture: UILongPressGestureRecognizer) {
switch gesture.state {
case .ended:
longPressLocation = gesture.location(in: self)
print("Long Pressed in UIView on \(longPressLocation) with scale \(scale)")
default:
break
}
}
@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
}
}
}
// TWO
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) { }
}
// ONE
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 {
ZStack {
content
.scaleEffect(scale, anchor: anchor)
.animation(.spring(), value: isPinching)
.overlay(PinchZoom(minScale: minScale, maxScale: maxScale, scale: $scale, isPinching: $isPinching))
}
}
}
TIA
答案1
得分: 0
以下是您提供的代码的翻译部分:
import SwiftUI
import UIKit
extension View {
func PinchToZoomAndPan(contentSize: CGSize, tapLocation: Binding<CGPoint>) -> some View {
modifier(PinchAndZoomModifier(contentSize: contentSize, tapLocation: tapLocation))
}
}
struct PinchAndZoomModifier: ViewModifier {
private var contentSize: CGSize
private var min: CGFloat = 0.75 // 1.0
private var max: CGFloat = 3.0
@State var currentScale: CGFloat = 1.0
// 用户长按时在图像框架中的位置,以发送回调用视图
@Binding var tapLocation: CGPoint
init(contentSize: CGSize, tapLocation: Binding<CGPoint>) {
self.contentSize = contentSize
self._tapLocation = tapLocation
}
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, longPressLocation: $tapLocation))
}
.animation(.easeInOut, value: currentScale)
}
}
// THREE; 缩放和缩放视图以嵌入到SwiftUI视图中
class PinchZoomView: UIView {
let minScale: CGFloat
let maxScale: CGFloat
var isPinching: Bool = false
var scale: CGFloat = 1.0
let scaleChange: (CGFloat) -> Void
let location: (CGPoint) -> Void
private var longPressLocation = CGPoint.zero
init(minScale: CGFloat, maxScale: CGFloat, currentScale: CGFloat, scaleChange: @escaping (CGFloat) -> Void, location: @escaping (CGPoint) -> Void) {
self.minScale = minScale
self.maxScale = maxScale
self.scale = currentScale
self.scaleChange = scaleChange
self.location = location
super.init(frame: .zero)
// 手势
let pinchGesture = UIPinchGestureRecognizer(target: self, action: #selector(pinch(gesture:)))
pinchGesture.cancelsTouchesInView = false
let longPressGesture = UILongPressGestureRecognizer(target: self, action: #selector(longPress(gesture:)))
addGestureRecognizer(pinchGesture)
addGestureRecognizer(longPressGesture)
}
required init?(coder: NSCoder) {
fatalError()
}
// 用户长按时的位置,以在调用视图中设置一个点
// 需要根据当前的缩放比例进行修正!
@objc private func longPress(gesture: UILongPressGestureRecognizer) {
switch gesture.state {
case .ended:
longPressLocation = gesture.location(in: self)
let correctedLocation = CGPoint(x: longPressLocation.x / scale, y: longPressLocation.y / scale)
location(correctedLocation)
print("Long Pressed in UIView on \(longPressLocation) with scale \(scale)")
default:
break
}
}
@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
}
}
}
// TWO: 将UIView桥接到SwiftUI
struct PinchZoom: UIViewRepresentable {
let minScale: CGFloat
let maxScale: CGFloat
@Binding var scale: CGFloat
@Binding var isPinching: Bool
@Binding var longPressLocation: CGPoint
func makeUIView(context: Context) -> PinchZoomView {
let pinchZoomView = PinchZoomView(minScale: minScale, maxScale: maxScale, currentScale: scale, scaleChange: { scale = $0 }, location: { longPressLocation = $0 })
return pinchZoomView
}
func updateUIView(_ pageControl: PinchZoomView, context: Context) { }
}
// ONE; 使用UIKit视图的修饰符
struct PinchToZoom: ViewModifier {
let minScale: CGFloat
let maxScale: CGFloat
@Binding var scale: CGFloat
@State var anchor: UnitPoint = .center
@State var isPinching: Bool = false
@Binding var longPressLocation: CGPoint
func body(content: Content) -> some View {
ZStack {
content
.scaleEffect(scale, anchor: anchor)
.animation(.spring(), value: isPinching)
.overlay(PinchZoom(minScale: minScale, maxScale: maxScale, scale: $scale, isPinching: $isPinching, longPressLocation: $longPressLocation))
}
}
}
希望这可以帮助您理解代码的翻译部分!
英文:
import SwiftUI
import UIKit
extension View {
func PinchToZoomAndPan(contentSize: CGSize, tapLocation: Binding<CGPoint>) -> some View {
modifier(PinchAndZoomModifier(contentSize: contentSize, tapLocation: tapLocation))
}
}
struct PinchAndZoomModifier: ViewModifier {
private var contentSize: CGSize
private var min: CGFloat = 0.75 // 1.0
private var max: CGFloat = 3.0
@State var currentScale: CGFloat = 1.0
// The location in the Image frame the user long pressed
// to send back to the calling View
@Binding var tapLocation: CGPoint
init(contentSize: CGSize, tapLocation: Binding<CGPoint>) {
self.contentSize = contentSize
self._tapLocation = tapLocation
}
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, longPressLocation: $tapLocation))
}
.animation(.easeInOut, value: currentScale)
}
}
// THREE; Pinch and zoom View to embed in SwiftUI View
class PinchZoomView: UIView {
let minScale: CGFloat
let maxScale: CGFloat
var isPinching: Bool = false
var scale: CGFloat = 1.0
let scaleChange: (CGFloat) -> Void
let location: (CGPoint) -> Void
private var longPressLocation = CGPoint.zero
init(minScale: CGFloat, maxScale: CGFloat, currentScale: CGFloat, scaleChange: @escaping (CGFloat) -> Void, location: @escaping (CGPoint) -> Void) {
self.minScale = minScale
self.maxScale = maxScale
self.scale = currentScale
self.scaleChange = scaleChange
self.location = location
super.init(frame: .zero)
// Gestures
let pinchGesture = UIPinchGestureRecognizer(target: self, action: #selector(pinch(gesture:)))
pinchGesture.cancelsTouchesInView = false
let longPressGesture = UILongPressGestureRecognizer(target: self, action: #selector(longPress(gesture:)))
addGestureRecognizer(pinchGesture)
addGestureRecognizer(longPressGesture)
}
required init?(coder: NSCoder) {
fatalError()
}
// location where the user long pressed, to set a pin in the calling View
// Needs to be corrected for the current zoom scale!
@objc private func longPress(gesture: UILongPressGestureRecognizer) {
switch gesture.state {
case .ended:
longPressLocation = gesture.location(in: self)
let correctedLocation = CGPoint(x: longPressLocation.x / scale, y: longPressLocation.y / scale)
location(correctedLocation)
print("Long Pressed in UIView on \(longPressLocation) with scale \(scale)")
default:
break
}
}
@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
}
}
}
// TWO: Bridge UIView to SwiftUI
struct PinchZoom: UIViewRepresentable {
let minScale: CGFloat
let maxScale: CGFloat
@Binding var scale: CGFloat
@Binding var isPinching: Bool
@Binding var longPressLocation: CGPoint
func makeUIView(context: Context) -> PinchZoomView {
let pinchZoomView = PinchZoomView(minScale: minScale, maxScale: maxScale, currentScale: scale, scaleChange: { scale = $0 }, location: { longPressLocation = $0 })
return pinchZoomView
}
func updateUIView(_ pageControl: PinchZoomView, context: Context) { }
}
// ONE; Modifier to use the UIKit View
struct PinchToZoom: ViewModifier {
let minScale: CGFloat
let maxScale: CGFloat
@Binding var scale: CGFloat
@State var anchor: UnitPoint = .center
@State var isPinching: Bool = false
@Binding var longPressLocation: CGPoint
func body(content: Content) -> some View {
ZStack {
content
.scaleEffect(scale, anchor: anchor)
.animation(.spring(), value: isPinching)
.overlay(PinchZoom(minScale: minScale, maxScale: maxScale, scale: $scale, isPinching: $isPinching, longPressLocation: $longPressLocation))
}
}
}
Closures did the job!
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论