'Long Press Gesture Recognizer location(in: view) not correctly calculating location
I'm using a UIScrollView
to display an image with various markers on top. The image view has a UILongPressGestureRecognizer
that detects long presses. When the long press event is detected, I want to create a new marker at that location.
The problem I'm having is that when I zoom in or out, the location of the gesture recognizer's location(in: view)
seems to be off. Here's a snippet of my implementation:
let longPressGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(self.onLongPress(gesture:)))
self.hostingController.view.addGestureRecognizer(longPressGestureRecognizer)
@objc func onLongPress(gesture: UILongPressGestureRecognizer) {
switch gesture.state {
case .began:
guard let view = gesture.view else { break }
let location = gesture.location(in: view)
let pinPointWidth = 32.0
let pinPointHeight = 42.0
let x = location.x - (pinPointWidth / 2)
let y = location.y - pinPointHeight
let finalLocation = CGPoint(x: x, y: y)
self.onLongPress(finalLocation)
default:
break
}
}
Please note that I'm using a UIViewControllerRepresentable
that contains a UIViewController
with a UIScrollView
that is surfaced to my SwiftUI View. Maybe this might be causing it.
Here's the SwiftUI code:
var body: some View {
UIScrollViewWrapper(scaleFactor: $scaleFactor, onLongPress: onInspectionCreated) {
ZStack(alignment: .topLeading) {
Image(uiImage: image)
ForEach(filteredInspections, id: \.syncToken) { inspection in
InspectionMarkerView(
scaleFactor: scaleFactor,
xLocation: CGFloat(inspection.xLocation),
yLocation: CGFloat(inspection.yLocation),
iconName: iconNameForInspection(inspectionMO: inspection),
label: inspection.readableIdPaddedOrNewInspection)
.onTapGesture {
selectedInspection = inspection
}
}
}
}
.clipped()
}
Here's a link to a reproducible example project: https://github.com/Kukiwon/sample-project-zoom-long-press-location
Here's a recording of the problem: Link to video
Any help is greatly appreciated!
Solution 1:[1]
I don't use SwiftUI, but I've seen some quirky stuff looking at UIHostingController
implementations, and it appears a specific quirk is hitting you.
I inset your scroll view by 40-pts and gave the main view a red background to make it a little easier to see what's going on.
First, add a 2-pixel blue line around the border of your sheet_hd
image, and scroll all the way to the bottom-left corner. It should look like this:
As you zoom in, keeping the scroll at bottom-left, it will look like this:
So far, so good -- and using a long-press to add a marker works as expected.
However, as soon as we zoom out to less than 1.0 zoom scale:
we can no longer see the bottom edge of the image.
Zooming back in makes it more obvious:
And the long-press location is incorrect.
For further clarification, if we set .clipsToBounds = false
on the scroll view, and set .alpha = 0.5
on the image view, we see this:
We can drag the view up to see the bottom edge, but as soon as we release the touch is bounces back below the frame of the scroll view.
What should fix this is to use this extension:
// extension to remove safe area from UIHostingController
// source: https://stackoverflow.com/a/70339424/6257435
extension UIHostingController {
convenience public init(rootView: Content, ignoreSafeArea: Bool) {
self.init(rootView: rootView)
if ignoreSafeArea {
disableSafeArea()
}
}
func disableSafeArea() {
guard let viewClass = object_getClass(view) else { return }
let viewSubclassName = String(cString: class_getName(viewClass)).appending("_IgnoreSafeArea")
if let viewSubclass = NSClassFromString(viewSubclassName) {
object_setClass(view, viewSubclass)
}
else {
guard let viewClassNameUtf8 = (viewSubclassName as NSString).utf8String else { return }
guard let viewSubclass = objc_allocateClassPair(viewClass, viewClassNameUtf8, 0) else { return }
if let method = class_getInstanceMethod(UIView.self, #selector(getter: UIView.safeAreaInsets)) {
let safeAreaInsets: @convention(block) (AnyObject) -> UIEdgeInsets = { _ in
return .zero
}
class_addMethod(viewSubclass, #selector(getter: UIView.safeAreaInsets), imp_implementationWithBlock(safeAreaInsets), method_getTypeEncoding(method))
}
objc_registerClassPair(viewSubclass)
object_setClass(view, viewSubclass)
}
}
}
Then, in viewDidLoad()
in your UIScrollViewViewController
:
override func viewDidLoad() {
super.viewDidLoad()
self.view.addSubview(self.scrollView)
self.pinEdges(of: self.scrollView, to: self.view)
// add this line
self.hostingController.disableSafeArea()
self.hostingController.willMove(toParent: self)
self.scrollView.addSubview(self.hostingController.view)
self.pinEdges(of: self.hostingController.view, to: self.scrollView)
self.hostingController.didMove(toParent: self)
self.hostingController.view.alpha = 0
}
Quick testing (obviously, you'll want to thoroughly test it) seems good... we can scroll all the way to the bottom, and long-press location is back where it should be.
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | DonMag |