interactionOptions does not work...

1544
9
12-15-2017 08:41 AM
WorthSparks
New Contributor III

...if another gesture is already in progress. Or at least that is how the problem appears to me. My app is Draw GIS (similar to Draw Maps in the App Store). It allows the user to freehand draw in the web map. To do so in my code, I add a gesture recognizer to capture drawing strokes. When a stroke begins, I have to disable mapView.interactionOptions. This used to work in earlier versions but somewhere along the way this stopped working. To get around the problem, I disable/enable the mapView's pan, pinch, and rotate gesture recognizers directly.

class MapView: AGSMapView {
    ...
    func interaction(isEnabled: Bool) {
        self.interactionOptions.isEnabled = isEnabled
        guard let gestureRecognizers = gestureRecognizers else { return }
        for gestureRecognizer in gestureRecognizers {
            if let panGestureRecognizer = gestureRecognizer as? UIPanGestureRecognizer {
                panGestureRecognizer.isEnabled = isEnabled
            } else if let pinchGestureRecognizer = gestureRecognizer as? UIPinchGestureRecognizer {
                pinchGestureRecognizer.isEnabled = isEnabled
            } else if let rotationGestureRecognizer = gestureRecognizer as? UIRotationGestureRecognizer {
                rotationGestureRecognizer.isEnabled = isEnabled
            }
        }
    }
    ...
}

I hope this helps someone.

Tags (2)
9 Replies
MarkDostal
Esri Contributor

Thank you for posting the code that works for you.  I'd be interested to learn more about what you are doing when the problem occurs.  Internally, when the user sets the "self.interactionOptions.isEnabled" option to "false" (on the MapView) we are doing what you are doing, which is setting all the gestureRecognizers' isEnabled property to false.

According to the UIGestureRecognizer's interface doc for "isEnabled":  "when changed to NO the gesture recognizer will be cancelled if it's currently recognizing a gesture", which *should* prevent the problem you are seeing.

If you could provide some more detail (or some sample code) that describes what's happening when you encounter the problem, I can see if there's anything we can do in the SDK.

Thanks again,

Mark

0 Kudos
WorthSparks
New Contributor III

Later, when I'm not so deep in other code, I might can make a sample program that demonstrates the problem. But for now, I'll just try to explain it better.

  1. I instantiate my StrokeGestureRecognizer, which subclasses UIGestureRecognizer, when the user raises the pencils to go into edit mode.
  2. I add it to my MapView, which is a subclass of AGSMapView.
  3. When the user begins a stroke, triggering strokeGestureRecognizer, the following action is called:
func strokeUpdated(_ strokeRecognizer: StrokeGestureRecognizer) {
    let workingStroke: WorkingStroke?
    if strokeRecognizer.state != .cancelled {
        workingStroke = strokeRecognizer.workingStroke
        if strokeRecognizer.state == .began ||
           (strokeRecognizer.state == .ended && mapViewModel.activeWorkingStroke.value == nil) {
            mapViewModel.activeWorkingStroke.value = workingStroke
        }
    } else {
        workingStroke = nil
        mapViewModel.activeWorkingStroke.value = nil
    }

    if let workingStroke = workingStroke {
        if strokeRecognizer.state == .ended {
            if strokeRecognizer.isForApplePencil {
                // Make sure we get the final workingStroke update if needed.
                workingStroke.receivedAllNeededUpdatesBlock = { [weak self] in
                    self?.mapView.sketchView.setNeedsDisplay(for: workingStroke)
                    workingStroke.clearUpdateInfo()
                }
            }
            mapViewModel.takeActiveWorkingStroke(mapView: mapView)
            mapViewModel.activeWorkingStroke.value = nil
        }
    }

    if let activeWorkingStroke = mapViewModel.activeWorkingStroke.value {
        mapView.sketchView.setNeedsDisplay(for: activeWorkingStroke)
    }
}
‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍

We want to disable map interaction only if the user is making a stroke. On Line 7 above, activeWorkingStroke is instantiated so it can begin taking touch samples. In the view controller, an observer is triggered whenever activeWorkingStroke changes:

mapViewModel.activeWorkingStroke.asObservable()
    .subscribe(onNext: { [weak self] activeWorkingStroke in
        if activeWorkingStroke == nil {
            self?.mapView.interaction(isEnabled: true)
        } else {
            self?.mapView.interaction(isEnabled: false)
        }
    })‍‍‍‍‍‍‍‍‍

The following is the version of my MapView member function that used to work:

func interaction(isEnabled: Bool) {
    self.interactionOptions.isEnabled = isEnabled
}

But somewhere along the way, it stopped working. Xcode, iOS and AGS have each gone through some recent updates so It is hard to say exactly when this problem began. The following is how I fixed it:

func interaction(isEnabled: Bool) {
    self.interactionOptions.isEnabled = isEnabled
    guard let gestureRecognizers = gestureRecognizers else { return }
    for gestureRecognizer in gestureRecognizers {
        if let panGestureRecognizer = gestureRecognizer as? UIPanGestureRecognizer {
            panGestureRecognizer.isEnabled = isEnabled
        } else if let pinchGestureRecognizer = gestureRecognizer as? UIPinchGestureRecognizer {
            pinchGestureRecognizer.isEnabled = isEnabled
        } else if let rotationGestureRecognizer = gestureRecognizer as? UIRotationGestureRecognizer {
            rotationGestureRecognizer.isEnabled = isEnabled
        }
    }
}

My gesture recognizer, strokeGestureRecognizer, is added to the mapView object, the same object where all the AGS gesture recognizers reside. Is it possible my gesture recognizer has tripped up some code somewhere that assumes all the gesture recognizers are Esri's? Or, perhaps it is something Apple did that is tripping both of us up. When I get a chance, I'll verify the bug is still there by retrying the old interaction(isEnabled:) function.

For now, I'm happy leaving my fix in place. I just wanted you all to know about the problem and, if nothing else, recheck your code to verify it still works as intended for more normal use cases.

Thanks for the help.

MarkDostal
Esri Contributor

Thank you for the detailed explanation, it helps.  I checked our code and we are holding onto the MapView gesture recognizers and then accessing them directly to enable/disable them, so there should be no issues with you adding your own recognizer.  I will write some tests here to try to reproduce your issue and verify things are OK in our code.

Mark

0 Kudos
WorthSparks
New Contributor III

I thought I had this fixed with my "in-the-weeds" fix I posted at the beginning of this thread. However, a new related problem is now happening. Even though I am setting interactionOptions.isEnabled = false and disabling all the gesture recognizers, something is hooking into some (not all) of my drag points and misinterpreting them as zoom.

What's weird is, the misinterpretation is something I wouldn't know how to do in AGS, even if I wanted to. Stroking down zooms in, stroking up zooms out, and stroking left or right has no effect.

What I need is simple. I need to disable AGSMapView user interaction completely during my stroke. That is, as soon as my StrokeGestureRecognizer is triggered, I need a way to disable all user interaction with the rest of the map view.

Have you been able to reproduce this problem?

Thanks, Worth

0 Kudos
Nicholas-Furness
Esri Regular Contributor

Thanks Worth Sparks,

I think there might be something going on with additional gesture recognizers we set up which are only incidentally related to the properties exposed in AGSInteractionOptions. We'll take a look into it.

In the meantime, you could work around this and extend your interaction(isEnabled) function ‌to explicitly modify all the gesture recognizers you find and which aren't yours (as opposed to the 3 you explicitly dig up above), restoring their state when your stroke gesture completes. Or you could overlay a transparent UIView atop of the AGSMapView to capture your strokes with your custom gesture recognizer.

Cheers,

Nick.

0 Kudos
WorthSparks
New Contributor III

Hi Nicholas Furness,

I added the code to recursively iterate through all subviews and disable all gesture recognizers from the AGSMapView and down. Before, I was just getting four. Now that I am recursively iterating through all subviews, I am getting eight. So now I am disabling all gesture recognizers. And the problem still persists.

Near the top of this thread, replying to Mark Dostal over a year ago, I said one day I might would make a stand-alone app to duplicate the problem. That day was yesterday. And I was able to reproduce it easily with just a couple Swift source files. I put it up in GitHub - Worth/AGSMapViewTouchTest: Testing touch interaction with AGSMapView in ArcGIS Runtime SDK ... And I put a video of me recreating the problem here: AGSMapViewTouchTest - YouTube.

I will start now looking into blocking input to the mapView with an overlay view. Near the beginning of this project, back in 2017, the overlay view was my original plan but I struggled to have seamless interaction with both my gesture recognizers and Esri's gesture recognizers when they were in different views. But I have learned a lot since then. I will look again at this possible solution.

Thanks,

Worth

WorthSparks
New Contributor III

Weekend followup with problem apparently FIXED...

This AGSMapViewTouchTest app has made it much easier to chase this bug. Here is what I have learned.

  1. Switching the gesture recognizer to a separate transparent UIView on top of mapView does not fix this problem. It is almost like deep inside the ArcGIS Runtime code, something is using KVO to observe something in the Apple stack and then acting on it. Very weird.
  2. This problem only happens if mapView.interactionOptions.isEnabled is set to false AFTER a gesture has been recognized and begun.
  3. A simple fix is to use mapView's UIView.isUserInteractionEnabled instead. I'm embarrassed it took me until now to try this. Apparently this does not affect my gesture that is already in progress.

So in my production app, Draw GIS, I have changed to using UIView.isUserInteractionEnabled. Now it doesn't use AGSMapView.interactionOptions at all. In testing so far, it looks like the problem is gone. I am going to float this new version to my beta testers and see how it goes.

0 Kudos
Nicholas-Furness
Esri Regular Contributor

Thanks for the follow-up, Worth Sparks.‌ This is helpful info.

Glad you've got a workaround. We'll take a look internally though.

Nicholas-Furness
Esri Regular Contributor

Hey Worth Sparks. We believe we have identified the problem and have a fix. Will be included in 100.6.