Search code examples
iosswiftuiviewuiscrollviewmapkit

How can I mimic the bottom sheet from the Maps app?


Can anyone tell me how I can mimic the bottom sheet in the new Apple Maps app in iOS 10?

In Android, you can use a BottomSheet which mimics this behaviour, but I could not find anything like that for iOS.

Is that a simple scroll view with a content inset, so that the search bar is at the bottom?

I am fairly new to iOS programming so if someone could help me creating this layout, that would be highly appreciated.

This is what I mean by "bottom sheet":

screenshot of the collapsed bottom sheet in Maps screenshot of the expanded bottom sheet in Maps


Solution

  • I don't know how exactly the bottom sheet of the new Maps app, responds to user interactions. But you can create a custom view that looks like the one in the screenshots and add it to the main view.

    I assume you know how to:

    1- create view controllers either by storyboards or using xib files.

    2- use googleMaps or Apple's MapKit.

    Example

    1- Create 2 view controllers e.g, MapViewController and BottomSheetViewController. The first controller will host the map and the second is the bottom sheet itself.

    Configure MapViewController

    Create a method to add the bottom sheet view.

    func addBottomSheetView() {
        // 1- Init bottomSheetVC
        let bottomSheetVC = BottomSheetViewController()
    
        // 2- Add bottomSheetVC as a child view 
        self.addChildViewController(bottomSheetVC)
        self.view.addSubview(bottomSheetVC.view)
        bottomSheetVC.didMoveToParentViewController(self)
    
        // 3- Adjust bottomSheet frame and initial position.
        let height = view.frame.height
        let width  = view.frame.width
        bottomSheetVC.view.frame = CGRectMake(0, self.view.frame.maxY, width, height)
    }
    

    And call it in viewDidAppear method:

    override func viewDidAppear(animated: Bool) {
        super.viewDidAppear(animated)
        addBottomSheetView()
    }
    

    Configure BottomSheetViewController

    1) Prepare background

    Create a method to add blur and vibrancy effects

    func prepareBackgroundView(){
        let blurEffect = UIBlurEffect.init(style: .Dark)
        let visualEffect = UIVisualEffectView.init(effect: blurEffect)
        let bluredView = UIVisualEffectView.init(effect: blurEffect)
        bluredView.contentView.addSubview(visualEffect)
    
        visualEffect.frame = UIScreen.mainScreen().bounds
        bluredView.frame = UIScreen.mainScreen().bounds
    
        view.insertSubview(bluredView, atIndex: 0)
    }
    

    call this method in your viewWillAppear

    override func viewWillAppear(animated: Bool) {
       super.viewWillAppear(animated)
       prepareBackgroundView()
    }
    

    Make sure that your controller's view background color is clearColor.

    2) Animate bottomSheet appearance

    override func viewDidAppear(animated: Bool) {
        super.viewDidAppear(animated)
    
        UIView.animateWithDuration(0.3) { [weak self] in
            let frame = self?.view.frame
            let yComponent = UIScreen.mainScreen().bounds.height - 200
            self?.view.frame = CGRectMake(0, yComponent, frame!.width, frame!.height)
        }
    }
    

    3) Modify your xib as you want.

    4) Add Pan Gesture Recognizer to your view.

    In your viewDidLoad method add UIPanGestureRecognizer.

    override func viewDidLoad() {
        super.viewDidLoad()
    
        let gesture = UIPanGestureRecognizer.init(target: self, action: #selector(BottomSheetViewController.panGesture))
        view.addGestureRecognizer(gesture)
    
    }
    

    And implement your gesture behaviour:

    func panGesture(recognizer: UIPanGestureRecognizer) {
        let translation = recognizer.translationInView(self.view)
        let y = self.view.frame.minY
        self.view.frame = CGRectMake(0, y + translation.y, view.frame.width, view.frame.height)
         recognizer.setTranslation(CGPointZero, inView: self.view)
    }
    

    Scrollable Bottom Sheet:

    If your custom view is a scroll view or any other view that inherits from, so you have two options:

    First:

    Design the view with a header view and add the panGesture to the header. (bad user experience).

    Second:

    1 - Add the panGesture to the bottom sheet view.

    2 - Implement the UIGestureRecognizerDelegate and set the panGesture delegate to the controller.

    3- Implement shouldRecognizeSimultaneouslyWith delegate function and disable the scrollView isScrollEnabled property in two case:

    • The view is partially visible.
    • The view is totally visible, the scrollView contentOffset property is 0 and the user is dragging the view downwards.

    Otherwise enable scrolling.

      func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
          let gesture = (gestureRecognizer as! UIPanGestureRecognizer)
          let direction = gesture.velocity(in: view).y
    
          let y = view.frame.minY
          if (y == fullView && tableView.contentOffset.y == 0 && direction > 0) || (y == partialView) {
              tableView.isScrollEnabled = false
          } else {
            tableView.isScrollEnabled = true
          }
    
          return false
      }
    

    NOTE

    In case you set .allowUserInteraction as an animation option, like in the sample project, so you need to enable scrolling on the animation completion closure if the user is scrolling up.

    Sample Project

    I created a sample project with more options on this repo which may give you better insights about how to customise the flow.

    In the demo, addBottomSheetView() function controls which view should be used as a bottom sheet.

    Sample Project Screenshots

    - Partial View

    enter image description here

    - FullView

    enter image description here

    - Scrollable View

    enter image description here