How can I mimic the bottom sheet from the Maps app?
Asked Answered
T

10

257

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

Thurstan answered 22/6, 2016 at 12:6 Comment(13)
You have to build it yourself I'm afraid. It's a view with blurred background view and some gesture recognisers.Pusey
@KhanhNguyen yeah I know I have to build it myself, but what I'm wondering is which views are used to archieve this effect and how they are ordered and so onThurstan
It's visual effect view with blur effect. See this SO questionPusey
@KhanhNguyen so how can I archieve the scrolling from the bottom? So that the search bar starts at the bottom of the screen and can be scrolled up when touching it, without interfering with the movement of the map?Thurstan
I think you can add a pan gesture recogniser to the bottom view and move the view accordingly (by observing the changes by storing the y coordinates between the gesture recogniser's events, and calculate the difference).Pusey
@KhanhNguyen Sorry but without code I can barely do something with your recommendations, as I'm new to iOS and swift developmentThurstan
I like your questions. I'm planning to implement something like this. If someone could write some example would be nice.Dulcie
@Dulcie Check my updated answer regarding scrollable sheets.Toggery
What about SwiftUI?Cenis
@Cenis have you find any library or code for drawer in SwiftUI?Nonfiction
@YogendraPatel no lib for it. I did talked with Apple team and they replied, that here is no this kind of possibility to do using SWiftUI.Cenis
iOS 15 in 2021 finally adds official support via UISheetPresentationController.Unessential
@Thurstan how did you manage to outline such precise outline? is this from some APIs or from bare geojson?Broder
T
395

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

Toggery answered 1/7, 2016 at 19:10 Comment(20)
This is actually a good tutorial for childViews and subViews. Thank you :)Kutaisi
@AhmedElassuty how can i add tableview in xib and load as bottom sheet ?if you could help would be great Thanks in advance!Haslam
@nikhilnangia I just updated the repo with another viewController "ScrollableBottomSheetViewController.swift" that contains a tableView. I will update the answer as soon as possible. Check this too github.com/AhmedElassuty/IOS-BottomSheet/issues/1Toggery
for ScrollableBottomSheet careful to use UITableViewController you will end up with none scrollable table view, you have to change it to UIViewController and have UITableView in it.Dulcinea
@AhmedElassuty this is amazing. Learned a lot by looking at your repo. Thanks againComatose
Hey! Awesome explanation but can you tell me, why the "sheet" loses its pan gesture if you add it as subview on the mapview?Threepiece
any chance to put it below navigation bar and drag it down?Vivavivace
I noticed that the Apple Maps bottom sheet handles transitioning from the sheet drag gesture to the scroll view scroll gesture in one smooth motion, i.e. the user doesn't need to stop one gesture and start a new one in order to start scrolling the view. Your example does not do this. Do you have any thoughts on how that might be accomplished? Thanks, and thanks a ton for posting the example in a repo!Tolu
@Tolu Thank you for your feedback. Please run the new demo of the develop branch, I think the demo is more responsive now. All what I did is that i changed my implementation of the gesture delegate methods to this.Toggery
@AhmadElassuty this is wonderful, but i'm troubling implement it for top sheet, any idea would be appreciated.Provencher
Anyone knows how to use it in react native?Driftage
@AhmadElassuty can you update same thing in objective-c? please help me I want in objective -cColumbus
This is extremely helpful. Also helps out understanding the pan gesture. Make a Medium article out of it 😊Instill
Any reason why a tableview parent's separator lines are displaying over the bottom sheet? Everything else on a cell is hidden by the sheet except for the separators... :(Durer
I used this as a template and rewrote it completely in Swift 5 without storyboards / IB but using AutoLayout. If anyone is interested I could copy & paste together a little "sample App" like in this solution :)Tlemcen
Thanks for this :) I found I needed to change the equality test of tableView.contentOffset.y == 0.0 to tableView.contentOffset.y == -tableView.adjustedContentInset.top (You would not need this change, I think, if tableView.contentInsetAdjustmentBehavior were set to .never).Ger
@Tlemcen can you please post your sample project? I"m trying to do that, because the problem with this solution is that the background viewcontroller is not clickable.... anyone know how to solve that problem?Malcommalcontent
Why does the color not appear as expected? It's all darkened, no matter what color I make the bottom view?Ambriz
thanks, this one really helped me. the version with "Scrollable Bottom Sheet".Mccarthy
Thanks, how to remove bottomSheet?Jim
V
90

Update iOS 15

In iOS 15, you can now use the native UISheetPresentationController.

if let sheet = viewControllerToPresent.sheetPresentationController {
    sheet.detents = [.medium(), .large()]
    // your sheet setup
}
present(viewControllerToPresent, animated: true, completion: nil)

Notice that you can even reproduce its navigation stack using the overcurrentcontext presentation mode:

let nextViewControllerToPresent: UIViewController = ...
nextViewControllerToPresent.modalPresentationStyle = .overCurrentContext
viewControllerToPresent.present(nextViewControllerToPresent, animated: true, completion: nil)

Legacy

I released a library based on my answer below.

It mimics the Shortcuts application overlay. See this article for details.

The main component of the library is the OverlayContainerViewController. It defines an area where a view controller can be dragged up and down, hiding or revealing the content underneath it.

let contentController = MapsViewController()
let overlayController = SearchViewController()

let containerController = OverlayContainerViewController()
containerController.delegate = self
containerController.viewControllers = [
    contentController,
    overlayController
]

window?.rootViewController = containerController

Implement OverlayContainerViewControllerDelegate to specify the number of notches wished:

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
    }
}

SwiftUI (12/29/20)

A SwiftUI version of the library is now available.

Color.red.dynamicOverlay(Color.green)

Previous answer

I think there is a significant point that is not treated in the suggested solutions: the transition between the scroll and the 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 goes down.

The point is tricky because we can not simply enable/disable the scroll when our pan gesture begins the translation. It would stop the scroll until a new touch begins. This is the case in most of the proposed solutions here.

Here is my try to implement this motion.

Starting point: Maps App

To start our investigation, let's visualize the view hierarchy of Maps (start Maps on a simulator and select Debug > Attach to process by PID or Name > Maps in Xcode 9).

Maps debug view hierarchy

It doesn't tell how the motion works, but it helped me to understand the logic of it. You can play with the lldb and the view hierarchy debugger.

Our view controller stacks

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

We start with a 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 problem, we consider that it can translate the overlay from one static point OverlayPosition.maximum to another OverlayPosition.minimum.

For now it only has one public method to animate the position change and it has 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 the all:

class StackViewController: UIViewController {

    private var viewControllers: [UIViewController]

    override func viewDidLoad() {
        super.viewDidLoad()
        viewControllers.forEach { gz_addChild($0, in: view) }
    }
}

In our AppDelegate, our startup sequence looks like:

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

The difficulty behind the overlay translation

Now, how to translate our overlay?

Most of the proposed solutions use a dedicated pan gesture recognizer, but we actually already have one : the pan gesture of the table view. Moreover, we need to keep the scroll and the translation synchronised and the UIScrollViewDelegate has all the events we need!

A naive implementation would use a second pan Gesture and try to reset the contentOffset of the table view when the translation occurs:

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

But it does not work. The tableView updates its contentOffset when its own pan gesture recognizer action triggers or when its displayLink callback is called. There is no chance that our recognizer triggers right after those to successfully override the contentOffset. Our only chance is either to take part of the layout phase (by overriding layoutSubviews of the scroll view calls at each frame of the scroll view) or to respond to the didScroll method of the delegate called each time the contentOffset is modified. Let's try this one.

The translation Implementation

We add a delegate to our OverlayVC to dispatch the scrollview's events to our translation handler, the 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 keep track of the translation using a enum:

enum OverlayInFlightPosition {
    case minimum
    case maximum
    case progressing
}

The current position calculation looks like :

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 handle the translation:

The first one tells us if we need to start the 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 one performs the translation. It uses the translation(in:) method of the scrollView's pan gesture.

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 one animates the end of the translation when the user releases its finger. We calculate the position using the velocity & the current position of the view.

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

Our overlay's delegate implementation simply looks like :

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()
    }
}

Final problem: dispatching the overlay container's touches

The translation is now pretty efficient. But there is still a final problem: the touches are not delivered to our background view. They are all intercepted by the overlay container's view. We can not set isUserInteractionEnabled to false because it would also disable the interaction in our table view. The solution is the one used massively in the Maps app, 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
    }
}

It removes itself from the responder 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 bugs, let me know ! Note that your implementation can of course use a second pan gesture, specially if you add a header in your overlay.

Update 23/08/18

We can replace scrollViewDidEndDragging with willEndScrollingWithVelocity rather than enabling/disabling the scroll when the user ends 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 a spring animation and allow user interaction while animating to make the motion flow better:

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)
}
Viminal answered 9/8, 2018 at 13:21 Comment(9)
This approach is not interactive during the animation stages. For example, in Maps, I can catch the sheet with my finger as it's expanding. I can then scrub the animation by panning up or down. It will return to whichever spot is closest. I believe this can be solved using UIViewPropertyAnimator (which has the ability to pause), then use the fractionComplete property to perform the scrub.Kiloliter
You're right @aust. I forgot to push my previous change. It should be good now. Thanks.Sloat
This is a great idea, however I can see one problem right away. In the translateView(following:) method you calculate the height based on the translation, but that could start from a different value than 0.0 (e.g. The table view is scrolled a little bit and the overlay is maximized when you start dragging). That would result in the initial jump. You could solve this by the scrollViewWillBeginDragging callback where you would remember the initial contentOffset of the table view and use it in the translateView(following:)'s calculation.Maness
If the scroll view offset is other than 0.0 while maximized, the overlay won't start moving until the offset gets to 0.0, otherwise you wouldn't be able to determine if the user intends to scroll the scroll view or interact with the overlay.Kiloliter
This should be the first answer.Baleen
@Viminal I was interested in attaching debugger to Maps.app on Simulator too, but get an error "Could not attach to pid : “9276”" followed with "Ensure “Maps” is not already running, and <username> has permission to debug it." - How did you trick Xcode to allow attaching to Maps.app?Ecclesiasticus
@Ecclesiasticus unfortunately it looks like the trick does not work anymore with Xcode 10Sloat
@Ecclesiasticus @Viminal worked for me on Xcode 10.1/Mojave -> just need to disable to disable System Integrity Protection (SIP). You need to: 1) Restart your mac; 2) Hold down cmd+R; 3) From the menu select Utilities then Terminal; 4) In the Terminal window type: csrutil disable && reboot. You will then have all the powers that root should normally give you including being able to attach to an Apple process.Asher
As of iOS 16, there is now a custom detent for UISheetPresentationController. Mini example code: .custom(identifier: .small) { context in 0.3 * context.maximumDetentValue }Quondam
W
22

Try Pulley:

Pulley is an easy to use drawer library meant to imitate the drawer in iOS 10's Maps app. It exposes a simple API that allows you to use any UIViewController subclass as the drawer content or the primary content.

Pulley Preview

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

Wickliffe answered 11/7, 2017 at 17:37 Comment(5)
Does it support scrolling in the underlying list?Governor
@RichardLindhout - After running the demo it looks like it supports scrolling, but not the smooth transition from moving the drawer to scrolling the list.Tolu
The example appears to hide Apples legal link in the bottom left.Chungchungking
Super simple question, how did you get the indicator at the top of the bottom sheet?Daune
If somebody interested I've rewritten Pulley in C# for my project purposes <github.com/iGeX/Xamarin.Pulley.Ios>. I don't want to deal with outdated binding libraries and decided to convert it to pure Xamarin.Ios C# code, so I can alter and upgrade it easily.Giselegisella
S
19

I wrote my own library to achieve the intended behaviour in ios Maps app. It is a protocol oriented solution. So you don't need to inherit any base class instead create a sheet controller and configure as you wish. It also supports inner navigation/presentation with or without UINavigationController.

See below link for more details.

https://github.com/OfTheWolf/UBottomSheet

ubottom sheet

Stoddart answered 19/8, 2018 at 7:34 Comment(2)
THIS should be the selected answer, since you can still click on the VC behind the popup.Malcommalcontent
Super simple question, how did you get the indicator at the top of the bottom sheet?Daune
N
16

You can try my answer https://github.com/SCENEE/FloatingPanel. It provides a container view controller to display a "bottom sheet" interface.

It's easy to use and you don't mind any gesture recognizer handling! Also you can track a scroll view's(or the sibling view) in a bottom sheet if needed.

This is a simple example. Please note that you need to prepare a view controller to display your content in a 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 a bottom sheet to search a location like Apple Maps.

Sample App like Apple Maps

Neilla answered 17/10, 2018 at 3:55 Comment(5)
fpc.set(contentViewController: newsVC) , method missing in the libraryPlanula
Super simple question, how did you get the indicator at the top of the bottom sheet?Daune
@AIsrafil I think its just a UIViewObjurgate
I think it is a really nice solution, but there is a problem in iOS 13 with presenting modal view controller with this floating panel.Objurgate
Tried this yesterday and didn't like it: overcomplicated with too many options, non-standard delegate function naming convention (use of vc and fps what should have been floatingPanel for both), Maps example showed panel jump way up briefly when dragging it down, example code is messy.Rwanda
U
8

iOS 15 in 2021 adds UISheetPresentationController, which is Apple's first public release of an Apple Maps-style "bottom sheet":

UISheetPresentationController

UISheetPresentationController lets you present your view controller as a sheet. Before you present your view controller, configure its sheet presentation controller with the behavior and appearance you want for your sheet.

Sheet presentation controllers specify a sheet's size based on a detent, a height where a sheet naturally rests. Detents allow a sheet to resize from one edge of its fully expanded frame while the other three edges remain fixed. You specify the detents that a sheet supports using detents, and monitor its most recently selected detent using selectedDetentIdentifier.

https://developer.apple.com/documentation/uikit/uisheetpresentationcontroller

This new bottom sheet control is explored in WWDC Session 10063: Customize and Resize Sheets in UIKit

UISheetPresentationController


Unfortunately....

In iOS 15, the UISheetPresentationController has launched with only medium and large detents.

A small detent is notably absent from the iOS 15 API, which would be required to display an always-presented "collapsed" bottom sheet like Apple Maps:

Custom smaller Detents in UISheetPresentationController?

The medium detent was released to handle use cases such as the Share Sheet or the "••• More" menu in Mail: a button-triggered half sheet.

In iOS 15, Apple Maps is now using this UIKit sheet presentation rather than a custom implementation, which is a huge step in the right direction. Apple Maps in iOS 15 continues to show the "small" bar, as well as a 1/3rd height bar. But those view sizes are not public API available to developers.

UIKit engineers at the WWDC 2021 Labs seemed to know that a small detent would be a hugely popular UIKit component. I would expect to see an API expansion for iOS 16 next year.

Unessential answered 7/6, 2021 at 20:21 Comment(1)
As of iOS 16, there is now a custom detent for UISheetPresentationController. Mini example code: .custom(identifier: .small) { context in 0.3 * context.maximumDetentValue }Quondam
P
4

**for iOS 15 Native Support available for this **

@IBAction func openSheet() { 
        let secondVC = self.storyboard?.instantiateViewController(withIdentifier: "SecondViewController")
        // Create the view controller.
        if #available(iOS 15.0, *) {
            let formNC = UINavigationController(rootViewController: secondVC!)
            formNC.modalPresentationStyle = UIModalPresentationStyle.pageSheet
            guard let sheetPresentationController = formNC.presentationController as? UISheetPresentationController else {
                return
            }
            sheetPresentationController.detents = [.medium(), .large()]
            sheetPresentationController.prefersGrabberVisible = true
            present(formNC, animated: true, completion: nil)
        } else {
            // Fallback on earlier versions
        }
   }

enter image description here

Praenomen answered 24/6, 2021 at 13:5 Comment(0)
P
3

We’ve just released a pure Swift Package supporting iOS 11.4+ which provides you a BottomSheet with theme and behavior options you can customize. This component is easy to use, and flexible. You can find it here: https://github.com/LunabeeStudio/LBBottomSheet. A demo project is available in this repository too.

For example, it supports different ways to manage the needed height, and also adds to the controller behind it the ability to detect height changes and adapt its bottom content inset.

You can find more information on the GitHub repository and in the documentation: https://lbbottomsheet.lunabee.studio.

I think it can help you to do what you’re looking for. Don’t hesitate to tell me if you have comments/questions :)

Here you can see one of all the possible BottomSheet configurations:

BottomSheet

Propound answered 28/10, 2021 at 18:56 Comment(4)
Why is the grabber handle at a non-standard place? (Immediate red flag for me.)Rwanda
You can completely customize where the grabber is positioned. You can choose its top inset to make it nearer the top if you want to. This screenshot is just an example of what you can do.Propound
Okay, that helps :-) But why should this be configurable? I think one should stick to how Apple is doing it and perhaps have the option to hide the grabber. Slight variants like this creates subliminal noise, given users a feeling something is wrong/weird. We all know that adding features, options, and making things configurable is a common programmer's pitfall ;-)Rwanda
Very good job, it was 1/2 hour only to implementCookie
G
2

iOS 15 finally adds a native UISheetPresentationController!

Official documentation https://developer.apple.com/documentation/uikit/uisheetpresentationcontroller

Goneness answered 8/6, 2021 at 2:52 Comment(0)
C
1

I recently created a component called SwipeableView as subclass of UIView, written in Swift 5.1 . It support all 4 direction, has several customisation options and can animate and interpolate different attributes and items ( such as layout constraints, background/tint color, affine transform, alpha channel and view center, all of them demoed with the respective show case ). It also supports the swiping coordination with the inner scroll view if set or auto detected. Should be pretty easy and straightforward to be used ( I hope 🙂)

Link at https://github.com/LucaIaco/SwipeableView

proof of concept:

enter image description here

Hope it helps

Comines answered 14/4, 2020 at 15:23 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.