Avoiding Conflicts with System Gestures at Screen Edges

If you rely on hiding the status bar to avoid conflicting with system gestures at the screen edges you will need to make some changes when updating for iOS 11. Apple no longer assumes your app wants to override system gestures at the edges when you hide the status bar. Instead you need to tell the system directly which edges you want first shot at handling gestures for.

The Problem with Gestures at the Screen Edges

A quick recap on the way this worked pre-iOS 11. Here is my App which shows a count in the center of the screen. You swipe up anywhere in the yellow play area to count up and down to count down.

iPhone 6S

I will skip the details (see the code) but the count works with two swipe gesture recognizers I added to the yellow view. The gestures are only recognized when they happen inside the yellow view which I constrain to half screen width and height.

I also allow a full screen mode which expands the yellow view to fill the screen. Swipes up or down then work anywhere on the screen. The problem is that it is easy for the user to accidentally trigger the control or notification centers when a swipe starts near the edge of the screen. Here is what happens when running on iOS 9 with a swipe up from the bottom edge.

control center

The system sees the swipe up gesture before our app and shows the control center.

Hiding the Status Bar

Before iOS 11 the way you tell the system not to grab gestures at the edge of the screen is to hide the status bar. Apple engineers made the assumption that if you are hiding the status bar you probably want to see gestures at the screen edges before the system.

Suppose we have a property in our view controller that stores the screen mode:

private var fullScreenMode: Bool = false

We set the property when the user changes the switch:

@IBAction func fullScreen(_ sender: UISwitch) {
  fullScreenMode = sender.isOn
}

To hide the status bar programmatically in our view controller I override prefersStatusBarHidden and return the full screen mode:

override var prefersStatusBarHidden: Bool {
  return fullScreenMode || super.prefersStatusBarHidden
}

Note that we also give the superclass (UIViewController) the chance to hide the status bar. This keeps the system default behaviour which hides the status bar for a compact height (iPhone in landscape).

There is one more step we need to do. When the user changes the screen mode we need to update our appearance and let the system know to handle the status bar. We can do that with a setter for our fullScreenMode property:

private var fullScreenMode: Bool = false {
  didSet {
    updateAppearance()
  }
}

The updateAppearance method takes care of updating the constraints to resize the yellow view (see the code for details). The key for the status bar handling is to call setNeedsStatusBarAppearanceUpdate to let the system know we have changed the value of prefersStatusBarHidden.

private func updateAppearance() {
    view.layoutIfNeeded()
    updateConstraints()
    UIView.animate(withDuration: 0.25) {
        self.setNeedsStatusBarAppearanceUpdate()
        self.view.layoutIfNeeded()
    }
}

Here is what it looks like when in full screen mode with the status bar hidden:

iPhone 6S full screen

The system now gives us the chance to handle up and down swipes at the system edges. Our app gets the first swipe up from the bottom edge. The system shows a small indicator to let the user know that they can still reach the control center with a second swipe up from the edge.

swipe up

Deferring System Gestures (iOS 11)

Apple has changed the way they handle gestures at the screen edges in iOS 11. Hiding the status bar in iOS 11 no longer causes the system to guess that you want to defer the system gestures. To keep the same behaviour we need to override preferredScreenEdgesDeferringSystemGestures in our view controller.

Since we are no longer forced to hide the status bar we can go back to the default handling for iOS 11:

override var prefersStatusBarHidden: Bool {
    if #available(iOS 11.0, *) {
        return super.prefersStatusBarHidden
    } else {
        return fullScreenMode || super.prefersStatusBarHidden
    }
}

Instead of hiding the status bar we override preferredScreenEdgesDeferringSystemGestures and return the edges where we want our gestures to fire first:

override func preferredScreenEdgesDeferringSystemGestures() -> UIRectEdge {
  return fullScreenMode ? [.bottom,.top] : UIRectEdge()
}

The return value is an option set of UIRectEdge constants. Possible values are .top, .left, .bottom, .right and .all. If we are in full screen mode we return .top and .bottom else we return the empty option set.

As with the status bar handling any time you change the value of preferredScreenEdgesDeferringSystemGestures you should let the system know. So for iOS 11 we call setNeedsUpdateOfScreenEdgesDeferringSystemGestures.

Let’s refactor that into a separate method that does the right thing for iOS 11 and falls back to updating the status bar for earlier versions:

private func updateDeferringSystemGestures() {
  if #available(iOS 11.0, *) {
    setNeedsUpdateOfScreenEdgesDeferringSystemGestures()
  } else {
    setNeedsStatusBarAppearanceUpdate()
  }
}

Our updateAppearance method then becomes:

private func updateAppearance() {
  view.layoutIfNeeded()
  updateConstraints()
  UIView.animate(withDuration: 0.25) {
    self.updateDeferringSystemGestures()
    self.view.layoutIfNeeded()
  }
}

We should now have an iOS 11 full screen mode with the status bar visible in regular size classes that defers system gestures on the top and bottom edges.

full screen with iOS 11

This is a small but significant change in iOS 11. If you rely on hiding the status bar to use gestures at the screen edges you should update your app and override setNeedsUpdateOfScreenEdgesDeferringSystemGestures for iOS 11.

Example Code

If you want the full example Xcode project I used for this post you can find it in my GitHub repository:

More Details