How can I simulate the bottom sheet from the Maps application?

Can someone tell me how I can imitate the bottom sheet in the new Maps app in iOS 10?

On Android, you can use a BottomSheet that mimics this behavior, but I could not find anything like it for iOS.

This is a simple scroll view with a content attachment, so the search bar is at the bottom?

I am new to iOS programming, so if someone can help me create this layout, it will 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

+142
ios swift uiview mapkit uiscrollview
Jun 22 '16 at 12:06 on
source share
7 answers

I don’t know how exactly the bottom sheet of the new Maps application responds to user interaction. But you can create your own view, similar to the one in the screenshots, and add it to the main view.

I suppose you know how to:

1- creates view controllers either using storyboards or using XIB files.

2- use googleMaps or Apple MapKit.

example

1- Create 2 view controllers, for example, MapViewController and BottomSheetViewController . The first controller will place the card, and the second - the bottom sheet itself.

Configure MapViewController

Create a method to add a 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 the viewDidAppear method:

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

Configure BottomSheetViewController

1) Prepare the background

Create a method to add blur and brightness 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 your controller background color is clearColor.

2) An animated view of the bottom sheet

 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) Change your XIB as you want.

4) Add Pan Gesture Recognizer to your view.

In your viewDidLoad method add a UIPanGestureRecognizer.

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

And implement your gesture behavior:

 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, you have two options:

The first:

Create a view with a title view and add panGesture to the title. (poor user experience) .

Secondly:

1 - Add panGesture to the bottom sheet view.

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

3- Implement the delegate shouldRecognizeSimralleluallyWith function and disable the scrollView isScrollEnabled property in two cases:

  • The view is partially visible.
  • The view is fully visible, the scrollView contentOffset property is 0, and the user drags the view down.

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, as in the example project, you need to enable scrolling when closing the completion of the animation, if the user scrolls up.

Project example

I created an example project with a lot of options in this repo, which can give you a better idea of ​​how to set up a stream.

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

Project Example Screenshots

- Partial view

enter image description here

- Full review

enter image description here

- scrollable view

enter image description here

+235
Jul 01 '16 at 19:10
source share

December 4, 2018 Patch

I released a library based on this solution https://github.com/applidium/ADOverlayContainer .

It simulates an overlay of the Shortcuts application. See this article for details.

The main component of the library is OverlayContainerViewController . It defines the area where the view controller can be dragged up and down to hide or reveal the content below it.

 let contentController = MapsViewController() let overlayController = SearchViewController() let containerController = OverlayContainerViewController() containerController.delegate = self containerController.viewControllers = [ contentController, overlayController ] window?.rootViewController = containerController 

OverlayContainerViewControllerDelegate to specify the OverlayContainerViewControllerDelegate number of OverlayContainerViewControllerDelegate :

 enum OverlayNotch: Int, CaseIterable { case minimum, medium, maximum } func numberOfNotches(in containerViewController: OverlayContainerViewController) -> Int { return OverlayNotch.allCases.count } func overlayContainerViewController(_ containerViewController: OverlayContainerViewController, heightForNotchAt index: Int, availableSpace: CGFloat) -> CGFloat { switch OverlayNotch.allCases[index] { case .maximum: return availableSpace * 3 / 4 case .medium: return availableSpace / 2 case .minimum: return availableSpace * 1 / 4 } } 



Previous answer

I think that there is an important point that is not considered in the proposed solutions: the transition between scrolling and translation.

Maps transition between the scroll and the translation

In Maps, as you may have noticed, when the tableView reaches contentOffset.y == 0 , the bottom sheet either slides up or down.

The point is tricky because we can't just turn on / off scrolling when our pan gesture starts translating. This will stop scrolling until a new touch begins. This applies to most of the solutions offered here.

Here is my attempt to realize this movement.

Starting point: Maps application

To begin our exploration, let us visualize the hierarchy of Maps views (run Maps on the simulator and choose Debug > Attach to process by PID or Name > Maps in Xcode 9).

Maps debug view hierarchy

This does not say how the movement works, but it helped me understand the logic of this. You can play with lldb and the view hierarchy debugger.

Our ViewController Stacks

Let's create a basic version of the Maps ViewController architecture.

We will start with the BackgroundViewController (our map view):

 class BackgroundViewController: UIViewController { override func loadView() { view = MKMapView() } } 

We put the tableView in a dedicated UIViewController :

 class OverlayViewController: UIViewController, UITableViewDataSource, UITableViewDelegate { lazy var tableView = UITableView() override func loadView() { view = tableView tableView.dataSource = self tableView.delegate = self } [...] } 

Now we need a VC to embed the overlay and manage its translation. To simplify the task, we believe that it can transfer the overlay from one static point OverlayPosition.maximum to another OverlayPosition.minimum .

At the moment, he has only one public method for animating position changes and a transparent view:

 enum OverlayPosition { case maximum, minimum } class OverlayContainerViewController: UIViewController { let overlayViewController: OverlayViewController var translatedViewHeightContraint = ... override func loadView() { view = UIView() } func moveOverlay(to position: OverlayPosition) { [...] } } 

Finally, we need a ViewController to embed everything:

 class StackViewController: UIViewController { private var viewControllers: [UIViewController] override func viewDidLoad() { super.viewDidLoad() viewControllers.forEach { gz_addChild($0, in: view) } } } 

In our AppDelegate, our launch sequence is as follows:

 let overlay = OverlayViewController() let containerViewController = OverlayContainerViewController(overlayViewController: overlay) let backgroundViewController = BackgroundViewController() window?.rootViewController = StackViewController(viewControllers: [backgroundViewController, containerViewController]) 

Overlay translation complexity

Now, how to translate our overlay?

Most of the proposed solutions use a special pan gesture recognizer, but we already have one: a panning gesture of the table view. Moreover, we need to synchronize scrolling and translation, and UIScrollViewDelegate has all the necessary events!

A naive implementation will use the second Gesture pan and try to reset the table contentOffset table view when performing the translation:

 func panGestureAction(_ recognizer: UIPanGestureRecognizer) { if isTranslating { tableView.contentOffset = .zero } } 

But that does not work. The TableView updates its contentOffset when the contentOffset native action of the pan gesture recognizer or when its displayLink callback is called. There is no chance that our resolver will work right after them to successfully override the contentOffset . Our only chance is to either take part in the layout phase (by overriding the layoutSubviews the scroll view in each frame of the scroll view), or respond to the didScroll method of the delegate called each time the contentOffset changes. Let's try it.

Translation Implementation

We add a delegate to our OverlayVC to send scrollview events to our translation handler, OverlayContainerViewController :

 protocol OverlayViewControllerDelegate: class { func scrollViewDidScroll(_ scrollView: UIScrollView) func scrollViewDidStopScrolling(_ scrollView: UIScrollView) } class OverlayViewController: UIViewController { [...] func scrollViewDidScroll(_ scrollView: UIScrollView) { delegate?.scrollViewDidScroll(scrollView) } func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { delegate?.scrollViewDidStopScrolling(scrollView) } } 

In our container, we track the transfer using the transfer:

 enum OverlayInFlightPosition { case minimum case maximum case progressing } 

The calculation of the current position looks like this:

 private var overlayInFlightPosition: OverlayInFlightPosition { let height = translatedViewHeightContraint.constant if height == maximumHeight { return .maximum } else if height == minimumHeight { return .minimum } else { return .progressing } } 

We need 3 methods to process the translation:

The first tells us if we need to start a translation.

 private func shouldTranslateView(following scrollView: UIScrollView) -> Bool { guard scrollView.isTracking else { return false } let offset = scrollView.contentOffset.y switch overlayInFlightPosition { case .maximum: return offset < 0 case .minimum: return offset > 0 case .progressing: return true } } 

The second is translating. It uses the translation(in:) panView gesture gesture scrollView.

 private func translateView(following scrollView: UIScrollView) { scrollView.contentOffset = .zero let translation = translatedViewTargetHeight - scrollView.panGestureRecognizer.translation(in: view).y translatedViewHeightContraint.constant = max( Constant.minimumHeight, min(translation, Constant.maximumHeight) ) } 

The third animates the end of the translation when the user releases his finger. We calculate the position using the speed and current position of the view.

 private func animateTranslationEnd() { let position: OverlayPosition = // ... calculation based on the current overlay position & velocity moveOverlay(to: position) } 

Our overlay delegate implementation looks just like this:

 class OverlayContainerViewController: UIViewController { func scrollViewDidScroll(_ scrollView: UIScrollView) { guard shouldTranslateView(following: scrollView) else { return } translateView(following: scrollView) } func scrollViewDidStopScrolling(_ scrollView: UIScrollView) { // prevent scroll animation when the translation animation ends scrollView.isEnabled = false scrollView.isEnabled = true animateTranslationEnd() } } 

Last issue: sending overlay container touches

Translation is now quite effective. But there is another problem: strokes are not transmitted in the background. They are all intercepted by the presentation of the overlay container. We cannot set isUserInteractionEnabled to false because it would also disable the interaction in our table view. A solution that is widely used in the PassThroughView Maps PassThroughView :

 class PassThroughView: UIView { override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { let view = super.hitTest(point, with: event) if view == self { return nil } return view } } 

He removes himself from the respondent’s chain.

In OverlayContainerViewController :

 override func loadView() { view = PassThroughView() } 

Result

Here is the result:

Result

You can find the code here .

Please, if you see any errors let me know! Note that your implementation may, of course, use a second pan gesture, especially if you add a title to the overlay.

Update 23/08/18

We can replace scrollViewDidEndDragging with willEndScrollingWithVelocity instead of enable / disable scrolling when the user finishes dragging:

 func scrollView(_ scrollView: UIScrollView, willEndScrollingWithVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) { switch overlayInFlightPosition { case .maximum: break case .minimum, .progressing: targetContentOffset.pointee = .zero } animateTranslationEnd(following: scrollView) } 

We can use spring animation and allow user interaction during the animation to improve movement:

 func moveOverlay(to position: OverlayPosition, duration: TimeInterval, velocity: CGPoint) { overlayPosition = position translatedViewHeightContraint.constant = translatedViewTargetHeight UIView.animate( withDuration: duration, delay: 0, usingSpringWithDamping: velocity.y == 0 ? 1 : 0.6, initialSpringVelocity: abs(velocity.y), options: [.allowUserInteraction], animations: { self.view.layoutIfNeeded() }, completion: nil) } 
+37
Aug 09 '18 at 13:21
source share

Try Pulley :

Pulley is an easy-to-use drawer library designed to simulate a drawer in iOS 10 Maps. It provides a simple API that allows you to use any subclass of UIViewController as the contents of a box or main content.

Pulley preview

https://github.com/52inc/Pulley

+15
Jul 11 '17 at 17:37
source share

You can try my answer https://github.com/SCENEE/FloatingPanel . It provides a container view controller for displaying the bottom sheet interface.

It is easy to use and you do not mind any gesture recognition processing! In addition, you can track the scroll view (or related view) on the bottom sheet, if necessary.

This is a simple example. Note that you need to prepare a view controller to display your content on the bottom sheet.

 import UIKit import FloatingPanel class ViewController: UIViewController { var fpc: FloatingPanelController! override func viewDidLoad() { super.viewDidLoad() fpc = FloatingPanelController() // Add "bottom sheet" in self.view. fpc.add(toParent: self) // Add a view controller to display your contents in "bottom sheet". let contentVC = ContentViewController() fpc.set(contentViewController: contentVC) // Track a scroll view in "bottom sheet" content if needed. fpc.track(scrollView: contentVC.tableView) } ... } 

Here is another example code to display the bottom sheet for a location search, such as Apple Maps.

Sample App like Apple Maps

+7
17 Oct '18 at 3:55
source share

None of the above works for me, because panning both the table view and the appearance at the same time does not work. So I wrote my own code to achieve the intended behavior in the ios Maps application.

https://github.com/OfTheWolf/UBottomSheet

ubottom sheet

+2
Aug 19 '18 at 7:34
source share

Perhaps you can try my answer https://github.com/AnYuan/AYPannel inspired by Bullets. Smooth transition from moving a box to scrolling through a list. I added a panorama gesture in the scroll view of the container and set shouldRecognizeSimultaneousWithGestureRecognizer to return YES. More details in my github link above. I wish to help.

+1
Dec 14 '17 at 7:32
source share

Use https://github.com/shantaramk/BottomSheet

It is easy to implement. just follow the instructions

0
Jan 28 '19 at 11:58
source share



All Articles