How to properly use swipeWithEvent to navigate a webView, Obj-C

From the 10.7 release notes:

Fluid Swipe Tracking - API

The following API allows for tracking gesture scroll wheel events as a fluid swipe gesture. Similar to iOS, NSScrollView will bounce once if needed, then optionally pass on the gesture scroll wheel events so your controller can use this API to track the scroll gesture as a fluid swipe gesture.

ScrollWheel NSEvents now respond to the -phase message. There are 3 types of scrolls:

1) Gesture scrolls - these begin with NSEventPhaseBegan, have a number of NSEventPhaseChanged events and terminate when the user lifts their fingers with an NSEventPhaseEnded.

2) Momentum scrolls - these have a phase of NSEventPhase none, but they have a momentumPhase of NSEventPhaseBegan/NSEventPhaseChanged/NSEventPhaseEnded.

3) Legacy scrolls - these scroll wheel events have a phase of NSEventPhaseNone and a momentumPhase of NSEventPhaseNone. There is no way to determine when the user starts, nor stops, performing legacy scrolls.

NSScrollView processes all gesture scroll wheel events and does not pass them up the responder chain. Often, tracking a swipe is done higher in the responder chain, for example at the WindowController level. To achieve an iOS style "bounce when not at the edge, otherwise swipe" behavior, you need to inform NSScrollView that it should forward scroll wheel messages when appropriate. Instead of manually setting a property on NSScrollView, your NSResponder can implement the following method and return YES.

- (BOOL)wantsScrollEventsForSwipeTrackingOnAxis:(NSEventGestureAxis)axis;

When the appropriate controller receives a -scrollWheel: message with a non NSEventNone phase, you can call the following message on that event instance to track the swipe or scroll to both user completion of the event, and animation completion.

enum {
    NSEventSwipeTrackingLockDirection = 0x1 << 0,
    NSEventSwipeTrackingClampGestureAmount = 0x1 << 1
};

typedef NSUInteger NSEventSwipeTrackingOptions;

@interface NSEvent ...
- (void)trackSwipeEventWithOptions:(NSEventSwipeTrackingOptions)options
          dampenAmountThresholdMin:(CGFloat)minDampenThreshold
                               max:(CGFloat)maxDampenThreshold
                      usingHandler:(void (^)(CGFloat gestureAmount, NSEventPhase phase, > BOOL isComplete, BOOL *stop))handler;
...

Below is a pseudo code example of swiping a collection of pictures like the iOS Photo app.

- (BOOL)wantsScrollEventsForSwipeTrackingOnAxis:(NSEventGestureAxis)axis {
    return (axis == NSEventGestureAxisHorizontal) ? YES : NO; }
- (void)scrollWheel:(NSEvent *)event {
    // NSScrollView is instructed to only forward horizontal scroll gesture events (see code above). However, depending
    // on where your controller is in the responder chain, it may receive other scrollWheel events that we don't want
    // to track as a fluid swipe because the event wasn't routed though an NSScrollView first.
    if ([event phase] == NSEventPhaseNone) return; // Not a gesture scroll event.
    if (fabsf([event scrollingDeltaX]) <= fabsf([event scrollingDeltaY])) return; // Not horizontal
    // If the user has disabled tracking scrolls as fluid swipes in system preferences, we should respect that.
    // NSScrollView will do this check for us, however, depending on where your controller is in the responder chain,
    // it may scrollWheel events that are not filtered by an NSScrollView.
    if (![NSEvent isSwipeTrackingFromScrollEventsEnabled]) return;
    if (_swipeAnimationCanceled && *_swipeAnimationCanceled == NO) {
        // A swipe animation is still in gestureAmount. Just kill it.
        *_swipeAnimationCanceled = YES;
        _swipeAnimationCanceled = NULL;
    }
    CGFloat numPhotosToLeft = // calc num of photos we can move to the left and negate
    CGFloat numPhotosToRight = // calc num of photos we can move to the right
    __block BOOL animationCancelled = NO;
    [event trackSwipeEventWithOptions:0 dampenAmountThresholdMin:numPhotosToLeft max:numPhotosToRight
                         usingHandler:^(CGFloat gestureAmount, NSEventPhase phase, BOOL isComplete, BOOL *stop) {
        if (animationCancelled) {
            *stop = YES;
            // Tear down animation overlay
            return;
        }
        if (phase == NSEventPhaseBegan) {
            // Setup animation overlay layers
        }
        // Update animation overlay to match gestureAmount
        if (phase == NSEventPhaseEnded) {
            // The user has completed the gesture successfully.
            // This handler will continue to get called with updated gestureAmount
            // to animate to completion, but just in case we need
            // to cancel the animation (due to user swiping again) setup the
            // controller / view to point to the new content / index / whatever
        } else if (phase == NSEventPhaseCancelled) {
            // The user has completed the gesture un-successfully.
            // This handler will continue to get called with updated gestureAmount
            // But we don't need to change the underlying controller / view settings.
        }
        if (isComplete) {
            // Animation has completed and gestureAmount is either -1, 0, or 1.
            // This handler block will be released upon return from this iteration.
            // Note: we already updated our view to use the new (or same) content
            // above. So no need to do that here. Just...
            // Tear down animation overlay here
            self->_swipeAnimationCanceled = NULL;
        }
    }];
    // We keep a pointer to a BOOL __block variable so that we can cancel our
    // block handler at any time. Note: You must assign the BOOL pointer to your
    // ivar after block creation and copy!
    self->_swipeAnimationCanceled = &animationCancelled; }

Other solution would be to directly handle touch event directly in order to recognize gestures. For more information consider to take a look at documentation.


For modern OSes (Lion and newer), this is one of those "How do I do X with Y?" questions where the answer is "Don't use Y."

-swipeWithEvent: is used to implement 10.6-style trackpad swiping, where the content on the screen doesn't move with the swipe. Most Mac trackpads are not configured to allow this kind of swiping anymore; the "Swipe between pages" preference in Trackpad pref pane has to be set to "swipe with three fingers" for it to be available, and that's neither the default setting nor a common setting for users to change.

Lion-style "fluid swipes" instead come in as scroll events. The PictureSwiper sample project in your Xcode SDK is a good place to start, but as an overview, here's what you do:

If you only need to support 10.8+

Use an NSPageController in history stack mode.

If you need to support 10.7

  1. Seriously consider supporting only 10.8+, at least for this feature. Implementing it manually is a huge mess. Don't say I didn't warn you.

  2. Create a custom view that is a superview to whatever views you want to be swipeable.

  3. Override -wantsScrollEventsForSwipeTrackingOnAxis: to return YES for the appropriate axis. If you're doing Safari-style back/forward navigation, that's NSEventGestureAxisHorizontal.

  4. Take a deep breath; this one's a doozy.

  5. Override -scrollWheel: in your custom view. Your override should call -trackSwipeEventWithOptions:dampenAmountThresholdMin:max:usingHandler:. Roughly, the dampen amount threshold minimum is how many viewfuls of data the user can swipe to the left, and the maximum is how many they can swipe to the right. The handler is a block that's called repeatedly as the user swipes; it should:

    1. Place an NSImageView with a screenshot of the page you're going back/forward to behind your content view.
    2. Move the content view to match the user's movements. Note that the gestureAmount parameter is, like the dampen amount thresholds, a (fractional and possibly negative) number of items; you have to multiply it by the view width to correctly position the content view.
    3. If the gesture phase is NSEventPhaseEnded, evaluate the gestureAmount to determine if the user completed the gesture. If they didn't, animate the content view back into place; if they did, put the content view back in place with no animation and update it to match the screenshot.

As you can see, actually implementing the handler is very involved, and I haven't even described all of the specifics. Even with all of these details, a highly skilled programmer will probably have to spend a few days getting this just right. The PictureSwiper sample in the 10.7 SDK is a good place to start. (The 10.8 version of PictureSwiper uses NSPageController. Just like you ought to do.)