With my iPhone X in my hand, I’m checking the weather for my current location in Partly Sunny, and I want to see my full locations list. What do I do? I swipe from the left edge of the screen to go back. I’m done checking the weather, and I want to go to my home screen. What do I do? I swipe up from the bottom.
I open Photos, swipe through my Camera Roll, favorite some photos, and now I want to go to the Favorites album. What do I do? I swipe down on the current photo to get back to the Camera Roll, I swipe from the left edge to go back, and now I can tap on the Favorites album. With all that navigating, I didn’t press any buttons. I only used gestures.
Gestures are an increasingly important part of iOS. Whether they’re a simple tap to dismiss, a swipe to go back, or a pinch to close, gestures not only offer shortcuts, they offer increased usability.
There is one particular place in iOS that hasn’t been given some gesture love but dearly needs some: modal form sheets. No doubt you’ve seen and interacted with form sheets in iOS before. On iPhone, they slide up from the bottom of the screen and fully cover the other view you were interacting with. On iPad, they slide up from the bottom and occupy a portion of the width and height of the screen. The area of the other view not covered by the form sheet is dimmed underneath.
By default, when modal form sheets are displayed, there is one way to dismiss them: the Done button in the top-left or top-right corner. This can make dismissing form sheets user-unfriendly. On iPads, this button occupies a tiny portion of the screen—meaning there’s just a tiny portion of the screen that can dismiss the modal. And on taller iPhones, reaching for the Done button can be challenging. How can dismissing form sheets be made easier? Gestures!
Before we get to that, let’s define a few things. First, what’s a modal? In its basic definition, a modal is a view that covers up another view and prevents interaction with that other view (the parent view) until an action occurs on the modal view. What are some examples of modal views?
In iOS, there are a variety of modal views. There are alerts (centered on the screen with usually a message and one more more buttons), action sheets (anchored to the bottom of the screen and usually show two or more choices to act on something), and activity sheets (commonly know as “share sheets” to copy, send, or share content).
These types of modal views are defined by iOS. One more type of modal view is defined by developers. When showing (called “presenting” in iOS parlance) a custom view—say a settings view with options and submenus—developers can choose to present the view modally. By default, the view will slide up from the bottom of the screen and cover the view the user was interacting with.
When presenting a view modally, developers can choose a few styles for the modal view. From the iOS Human Interface Guidelines, the styles are “full screen”, “page sheet”, and “form sheet”. On smaller screens, page sheets and form sheets cover the whole screen, but on larger screens, they cover a portion of the screen. The portion of the parent view not covered is dimmed underneath.
I’m going to focus specifically on form sheets though the improvements I discuss would also apply to page sheets and mostly to full screen modal views too.
What are some examples of form sheets? In App Store, viewing your Apple ID account presents a form sheet.
In Settings > Apple ID, tapping “Set Up Family Sharing” presents a form sheet.
And in Partly Sunny, viewing settings, editing the locations list, and viewing radar all present a form sheet.
So how can dismissing form sheets be made easier with gestures? Here are two ways.
First, on iPad, tapping anywhere outside the form sheet where the parent view is dimmed underneath should dismiss it. Here’s the current active area of the screen where tapping will dismiss the form sheet: just the Done button.
And here’s what the active area to dismiss the form sheet should be: both the Done button AND the space outside the view (the status bar is still reserved for its interactions like scrolling a view to the top).
The space outside the form sheet currently is a touch dead zone; the dimmed area merely prevents taps to the view underneath. Why not use this space to dismiss the form sheet?
Tapping outside a view to dismiss it already exists elsewhere in iOS—even for dismissing modal views. When an action sheet or activity sheet is presented, tapping above or outside the buttons or view will dismiss the sheet.
In the iOS 11 App Store, tapping a Today story tile or list tile opens a sort-of page sheet where, on iPad, tapping to the left or right of the view in the blurred area will dismiss the view.
This example isn’t a modal view per se, but it applies. On iPad when pulling down on a Messages notification banner to reply to the message, tapping to the left or right of the messages view in the blurred area will dismiss the view.
With the latter two examples, perhaps these relatively new interaction methods will inform an updated modal system in a future iOS version.
In the meantime, there’s a workaround to capture taps outside the form sheet and have them dismiss the modal. This Stack Overflow thread discusses the solution. Since I first discovered this workaround, there’s been an update for Swift 4. I’ve reproduced and tweaked it here:
import UIKit
class FormSheetViewController: UIViewController, UIGestureRecognizerDelegate {
// gesture recognizer to test taps outside the form sheet
var backgroundTapGestureRecognizer: UITapGestureRecognizer!
// dismiss the modal and perform any other related actions (e.g. inform delegate)
func done() {
dismiss(animated: true, completion: nil)
}
// check if the tap was outside the form sheet
@objc func handleTap(_ sender: UITapGestureRecognizer) {
if sender.state == .ended {
let location: CGPoint = sender.location(in: view)
// if outside, dismiss the view
if !view.point(inside: location, with: nil) {
view.window?.removeGestureRecognizer(sender)
done()
}
}
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// set up gesture recognizer
if backgroundTapGestureRecognizer == nil {
backgroundTapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTap(_:)))
backgroundTapGestureRecognizer.delegate = self
backgroundTapGestureRecognizer.numberOfTapsRequired = 1
backgroundTapGestureRecognizer.cancelsTouchesInView = false
view.window?.addGestureRecognizer(backgroundTapGestureRecognizer)
}
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
// remove gesture recognizer
if backgroundTapGestureRecognizer != nil {
view.window?.removeGestureRecognizer(backgroundTapGestureRecognizer)
backgroundTapGestureRecognizer = nil
}
}
// don't forget the delegate method!
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
}
Partly Sunny uses this workaround, so in any of the previously mentioned form sheets, tapping outside the view will dismiss it.
This works for iPad, but what about iPhone where the form sheets cover the full screen and there isn’t any dimmed area? The second way to make dismissing form sheets easier is dragging. Once the view is dragged down a certain amount, the view should dismiss. This would work on iPads too.
This idea of dragging down to dismiss something exists elsewhere in iOS. In Photos, when tapping on a photo in the Camera Roll or an album, the photo goes full screen. Tapping the back button goes back to the album, but also dragging down on the photo will shrink it and fade it to reveal the album—in other words, dragging down dismisses it.
While Maps doesn’t use traditional modal views, the system it uses allows for dragging the cards down to get back to the content underneath.
Third-party apps have started to employ dragging down modals to dismiss as well. I’m not sure where I first saw it, but I know of several apps that have this functionality. Partly Sunny is one of them. In any form sheet, dragging down on the view will dismiss it. Here it is in action:
I added a sort-of guard to help prevent accidental dismissals. If you scroll the view and then scroll back to the top, if when you reach the top you’re still dragging down (so that the view is doing the iOS rubber-banding effect), the view won’t dismiss. But once you release, if you drag down again, the view will dismiss. I didn’t want anyone to be casually scrolling up and suddenly the view disappeared on them. Here’s what that code looks like added to the code above:
import UIKit
class FormSheetViewController: UIViewController, UIGestureRecognizerDelegate {
// scroll view must be dragged down this distance to be dismissed
var scrollDistanceToDismiss: CGFloat = 50
// tracks whether or not the scroll view should be dismissed if dragged down from top boundary
var dragScrollViewToDismiss = true
// tracks whether or not dragScrollViewToDismiss is ready to be set
var dragScrollViewToDismissIsReady = false
// stores the initial offset of the scroll view
var scrollViewInitialOffset: CGFloat = 0
// stores if the scroll view was loaded; guards overwriting scrollViewInitialOffset
var scrollViewLoaded = false
// gesture recognizer to test taps outside the form sheet
var backgroundTapGestureRecognizer: UITapGestureRecognizer!
// dismiss the modal and perform any other related actions (e.g. inform delegate)
func done() {
dismiss(animated: true, completion: nil)
}
[…]
}
extension FormSheetViewController: UIScrollViewDelegate {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
// store initial content offset of scroll view
if !scrollViewLoaded {
scrollViewLoaded = true
scrollViewInitialOffset = scrollView.contentOffset.y
}
if dragScrollViewToDismiss {
// if scrolling up, cancel dismiss
if scrollView.contentOffset.y - scrollViewInitialOffset > -scrollView.contentInset.top {
dragScrollViewToDismiss = false
dragScrollViewToDismissIsReady = false
}
// if scrolling down, dismiss view controller
else if scrollView.contentOffset.y - scrollViewInitialOffset <= -scrollView.contentInset.top - scrollDistanceToDismiss {
done()
}
}
}
// if scroll view released beyond top boundary, set dismiss ready
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
if scrollView.contentOffset.y <= -scrollView.contentInset.top {
dragScrollViewToDismissIsReady = true
}
}
// if scroll view drifts beyond top boundary, set dismiss ready
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
if scrollView.contentOffset.y <= -scrollView.contentInset.top {
dragScrollViewToDismissIsReady = true
}
}
// if scroll-to-top activated, set dismiss ready
func scrollViewDidScrollToTop(_ scrollView: UIScrollView) {
dragScrollViewToDismissIsReady = true
}
// when tapping again on scroll view, set dismiss active
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
if dragScrollViewToDismissIsReady {
dragScrollViewToDismiss = true
}
}
}
Form sheets and modal views as a whole are important and useful tools for iOS app developers. Dismissing them could be a bit more user-friendly, and gestures—tapping outside the view and dragging down on the view—can accomplish that. I hope a future version of iOS gives developers out-of-the-box tools to do both and thus standardizes these interaction methods.
Until then, developers can use their own solutions like those above. If you have suggestions how I can improve tapping outside the view or dragging down on the view, please let me know. Now it’s time for me to swipe down on this and get back to work.