0

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!

Kukiwon
  • 1,222
  • 12
  • 20
  • It's difficult to tell what's going on from that video clip. Best bet is to put together a [mre] (post it somewhere like GitHub) so we can see exactly what's happening. – DonMag Apr 27 '22 at 14:30
  • @DonMag Here you go: https://github.com/Kukiwon/sample-project-zoom-long-press-location – Kukiwon May 04 '22 at 08:12

1 Answers1

0

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:

enter image description here

As you zoom in, keeping the scroll at bottom-left, it will look like this:

enter image description here

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:

enter image description here

we can no longer see the bottom edge of the image.

Zooming back in makes it more obvious:

enter image description here

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:

enter image description here

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.

DonMag
  • 69,424
  • 5
  • 50
  • 86