Skip to content

Repository files navigation

DrawerKit

circleciSwiftPM compatiblePlatformSwift 5License

What is DrawerKit?

DrawerKit is a custom view controller presentation mimicking the kind of behaviour you see in the Apple Maps app. It lets any view controller modally present another arbitrary view controller in such a way that the presented content is only partially shown at first, then allowing the user to interact with it by showing more or less of that content until it's fully presented or fully dismissed. It's not (yet) a complete implementation of the behaviour you see in the Maps app simply because our specific needs dictated something else. We intend to continue working on it to address that limitation.

Please do play with the demo app and try different configuration options because there are so many ways to configure DrawerKit that the gif below is at most a pathetic representation of everything that the library can do.

Saturn image attribution

DrawerKitDrawerKitDrawerKit

What version of iOS does it require or support?

DrawerKit is compatible with iOS 10 and above.

How to use it?

In order for the presenting view controller to present another view controller (the presented view controller) as a drawer, some object needs to conform to the DrawerCoordinating protocol and the presented view controller needs to conform to the DrawerPresentable protocol. The presenting view controller may be the object conforming to DrawerCoordinating but it need not be.

publicprotocolDrawerCoordinating:class{
/// An object vended by the conforming object, whose responsibility is to control
/// the presentation, animation, and interactivity of/with the drawer.
vardrawerDisplayController:DrawerDisplayController?{get}}publicprotocolDrawerPresentable:class{
/// The height at which the drawer must be presented when it's in its
/// partially expanded state. If negative, its value is clamped to zero.
varheightOfPartiallyExpandedDrawer:CGFloat{get}
/// The height at which the drawer must be presented when it's in its
/// collapsed state. If negative, its value is clamped to zero.
/// Default implementation returns 0.
varheightOfCollapsedDrawer:CGFloat{get}}

After that, it's essentially business as usual in regards to presenting a view controller modally. Here's the basic code to get a view controller to present another as a drawer, where the presenting view controller itself conforms to DrawerCoordinating,

extensionPresenterViewController{func doModalPresentation(){guardlet vc = storyboard?.instantiateViewController(withIdentifier:"presented")as?PresentedViewControllerelse{return}
// you can provide the configuration values in the initialiser...
varconfiguration=DrawerConfiguration(/* ..., ..., ..., */)
// ... or after initialisation. All of these have default values so change only
// what you need to configure differently. They're all listed here just so you
// can see what can be configured. The values listed are the default ones,
// except where indicated otherwise.
configuration.totalDurationInSeconds =3 // default is 0.4
configuration.durationIsProportionalToDistanceTraveled =false
// default is UISpringTimingParameters()
configuration.timingCurveProvider =UISpringTimingParameters(dampingRatio:0.8)
configuration.fullExpansionBehaviour =.leavesCustomGap(gap:100) // default is .coversFullScreen
configuration.supportsPartialExpansion =true
configuration.dismissesInStages =true
configuration.isDrawerDraggable =true
configuration.isFullyPresentableByDrawerTaps =true
configuration.numberOfTapsForFullDrawerPresentation =1
configuration.isDismissableByOutsideDrawerTaps =true
configuration.numberOfTapsForOutsideDrawerDismissal =1
configuration.flickSpeedThreshold =3
configuration.upperMarkGap =100 // default is 40
configuration.lowerMarkGap =80 // default is 40
configuration.maximumCornerRadius =15varhandleViewConfiguration=HandleViewConfiguration()
handleViewConfiguration.autoAnimatesDimming =true
handleViewConfiguration.backgroundColor =.gray
handleViewConfiguration.size =CGSize(width:40, height:6)
handleViewConfiguration.top =8
handleViewConfiguration.cornerRadius =.automatic
configuration.handleViewConfiguration = handleViewConfiguration
letborderColor=UIColor(red:205.0/255.0, green:206.0/255.0, blue:210.0/255.0, alpha:1)letdrawerBorderConfiguration=DrawerBorderConfiguration(borderThickness:0.5,
borderColor: borderColor)
configuration.drawerBorderConfiguration = drawerBorderConfiguration
letdrawerShadowConfiguration=DrawerShadowConfiguration(shadowOpacity:0.25,
shadowRadius:4,
shadowOffset:.zero,
shadowColor:.black)
configuration.drawerShadowConfiguration = drawerShadowConfiguration
drawerDisplayController =DrawerDisplayController(presentingViewController:self,
presentedViewController: vc,
configuration: configuration,
inDebugMode:true)present(vc, animated:true)}}

and here's one way to implement the corresponding presented view controller:

extensionPresentedViewController:DrawerPresentable{varheightOfPartiallyExpandedDrawer:CGFloat{guardlet view =self.view as?PresentedViewelse{return0}return view.dividerView.frame.origin.y
}}

Naturally, the presented view controller can dismiss itself at any time, following the usual approach:

extensionPresentedViewController{@IBActionfunc dismissButtonTapped(){dismiss(animated:true)}}

How configurable is it?

DrawerKit has a number of configurable properties, conveniently collected together into a struct, DrawerConfiguration. Here's a list of all the currently supported configuration options:

 /// Intial state of presented drawer. Default is `nil`, If `nil` then
/// state will be computed based on `supportsPartialExpansion` flag.
publicvarinitialState:DrawerState?
/// The total duration, in seconds, for the drawer to transition from its
/// dismissed state to its fully-expanded state, or vice-versa. The default
/// value is 0.4 seconds.
publicvartotalDurationInSeconds:TimeInterval
/// When the drawer transitions between its dismissed and partially-expanded
/// states, or between its partially-expanded and its fully-expanded states, in
/// either direction, the distance traveled by the drawer is some fraction of
/// the total distance traveled between the dismissed and fully-expanded states.
/// You have a choice between having those fractional transitions take the same
/// amount of time as the full transition, and having them take a time that is
/// a fraction of the total time, where the fraction used is the fraction of
/// space those partial transitions travel. In the first case, all transitions
/// have the same duration (`totalDurationInSeconds`) but different speeds, while
/// in the second case different transitions have different durations but the same
/// speed. The default is `false`, that is, all transitions last the same amount
/// of time.
publicvardurationIsProportionalToDistanceTraveled:Bool
/// The type of timing curve to use for the animations. The full set of cubic
/// Bezier curves and spring-based curves is supported. Note that selecting a
/// spring-based timing curve may cause the `totalDurationInSeconds` parameter
/// to be ignored because the duration, for a fully general spring-based timing
/// curve provider, is computed based on the specifics of the spring-based curve.
/// The default is `UISpringTimingParameters()`, which is the system's global
/// spring-based timing curve.
publicvartimingCurveProvider:UITimingCurveProvider
/// Whether the drawer expands to cover the entire screen, the entire screen minus
/// the status bar, or the entire screen minus a custom gap. The default is to cover
/// the full screen.
publicvarfullExpansionBehaviour:FullExpansionBehaviour
/// When `true`, the drawer is presented first in its partially expanded state.
/// When `false`, the presentation is always to full screen and there is no
/// partially expanded state. The default value is `true`.
publicvarsupportsPartialExpansion:Bool
/// When `true`, dismissing the drawer from its fully expanded state can result
/// in the drawer stopping at its partially expanded state. When `false`, the
/// dismissal is always straight to the dismissed state. Note that
/// `supportsPartialExpansion` being `false` implies `dismissesInStages` being
/// `false` as well but you can have `supportsPartialExpansion == true` and
/// `dismissesInStages == false`, which would result in presentations to the
/// partially expanded state but all dismissals would be straight to the dismissed
/// state. The default value is `true`.
publicvardismissesInStages:Bool
/// Whether or not the drawer can be dragged up and down. The default value is `true`.
publicvarisDrawerDraggable:Bool
/// Whether or not the drawer can be fully presentable by tapping on it.
/// The default value is `true`.
publicvarisFullyPresentableByDrawerTaps:Bool
/// How many taps are required for fully presenting the drawer by tapping on it.
/// The default value is 1.
publicvarnumberOfTapsForFullDrawerPresentation:Int
/// Whether or not the drawer can be dismissed by tapping anywhere outside of it.
/// The default value is `true`.
publicvarisDismissableByOutsideDrawerTaps:Bool
/// How many taps are required for dismissing the drawer by tapping outside of it.
/// The default value is 1.
publicvarnumberOfTapsForOutsideDrawerDismissal:Int
/// How fast one needs to "flick" the drawer up or down to make it ignore the
/// partially expanded state. Flicking fast enough up always presents to full screen
/// and flicking fast enough down always collapses the drawer. A typically good value
/// is around 3 points per screen height per second, and that is also the default
/// value of this property.
publicvarflickSpeedThreshold:CGFloat
/// There is a band around the partially expanded position of the drawer where
/// ending a drag inside will cause the drawer to move back to the partially
/// expanded position (subjected to the conditions set by `supportsPartialExpansion`
/// and `dismissesInStages`, of course). Set `inDebugMode` to `true` to see lines
/// drawn at those positions. This value represents the gap *above* the partially
/// expanded position. The default value is 40 points.
publicvarupperMarkGap:CGFloat
/// There is a band around the partially expanded position of the drawer where
/// ending a drag inside will cause the drawer to move back to the partially
/// expanded position (subjected to the conditions set by `supportsPartialExpansion`
/// and `dismissesInStages`, of course). Set `inDebugMode` to `true` to see lines
/// drawn at those positions. This value represents the gap *below* the partially
/// expanded position. The default value is 40 points.
publicvarlowerMarkGap:CGFloat
/// The animating drawer also animates the radius of its top left and top right
/// corners, from 0 to the value of this property. Setting this to 0 prevents any
/// corner animations from taking place. The default value is 15 points.
publicvarmaximumCornerRadius:CGFloat
/// How the drawer should animate its corner radius if specified. The
/// default value is `maximumAtPartialY`.
publicvarcornerAnimationOption:CornerAnimationOption
/// The configuration options for the handle view, should it be shown. Set this
/// property to `nil` to hide the handle view. The default value is
/// `HandleViewConfiguration()`.
publicvarhandleViewConfiguration:HandleViewConfiguration?
/// The configuration options for the drawer's border, should it be shown. Set this
/// property to `nil` so as not to have a drawer border. The default value is `nil`.
publicvardrawerBorderConfiguration:DrawerBorderConfiguration?
/// The configuration options for the drawer's shadow, should it be shown. Set this
/// property to `nil` so as not to have a drawer shadow. The default value is `nil`.
publicvardrawerShadowConfiguration:DrawerShadowConfiguration?
/// In what states touches should be passed through to the presenting view.
/// By default touches will not be passed through only in `fullyExpanded` state.
publicvarpassthroughTouchesInStates:PassthroughOptions
publicenumFullExpansionBehaviour:Equatable{case coversFullScreen
case dosNotCoverStatusBar
case leavesCustomGap(gap:CGFloat)}
publicstructHandleViewConfiguration{
/// Whether or not to automatically dim the handle view as the drawer approaches
/// its collapsed or fully expanded states. The default is `true`. Set it to `false`
/// when configuring the drawer not to cover the full screen so that the handle view
/// is always visible in that case.
publicvarautoAnimatesDimming:Bool
/// The handle view's background color. The default value is `UIColor.gray`.
publicvarbackgroundColor:UIColor
/// The handle view's bounding rectangle's size. The default value is
/// `CGSize(width: 40, height: 6)`.
publicvarsize:CGSize
/// The handle view's vertical distance from the top of the drawer. In other words,
/// the constant to be used when setting up the layout constraint
/// `handleView.topAnchor.constraint(equalTo: presentedView.topAnchor, constant: top)`
/// The default value is 8 points.
publicvartop:CGFloat
/// The handle view's corner radius. The default is `CornerRadius.automatic`, which
/// results in a corner radius equal to half the handle view's height.
publicvarcornerRadius:CornerRadius}
publicstructDrawerBorderConfiguration{
/// The drawer's layer’s border thickness. The default value is 0,
/// so effectively the default is not to have any border at all.
publicletborderThickness:CGFloat
/// The drawer's layer’s border's color. The default value is `nil`, so
/// effectively the default is not to have any border at all.
publicletborderColor:UIColor?publicinit(borderThickness:CGFloat=0, borderColor:UIColor?=nil)}
publicstructDrawerShadowConfiguration{
/// The drawer's layer’s shadow's opacity. The default value is 0, so
/// effectively the default is not to have any shadow at all.
publicletshadowOpacity:CGFloat
/// The blur radius (in points) used to render the drawer's layer’s shadow.
/// The default value is 0, so effectively the default is not to have any
/// shadow at all.
publicletshadowRadius:CGFloat
/// The offset (in points) of the drawer's layer’s shadow. The default value is
/// `CGSize.zero`, so effectively the default is not to have any shadow at all.
publicletshadowOffset:CGSize
/// The drawer's layer’s shadow's color. The default value is `nil`, so
/// effectively the default is not to have any shadow at all.
publicletshadowColor:UIColor?publicinit(shadowOpacity:CGFloat=0,
shadowRadius:CGFloat=0,
shadowOffset:CGSize=.zero,
shadowColor:UIColor?=nil)}

What's the actual drawer behaviour logic?

The behaviour of how and under what situations the drawer gets fully presented, partially presented, or collapsed (dismissed) is summarised by the pseudo-code below:

 if isMovingUpQuickly{ show fully expanded }
if isMovingDownQuickly{collapse all the way (ie, dismiss)}
if isAboveUpperMark {
if isMovingUp || isNotMoving{
show fully expanded
} else { // is moving down
collapse to the partially expanded state or all the way (ie, dismiss),
depending on the values of `supportsPartialExpansion` and `dismissesInStages`
}}
if isAboveLowerMark{ // ie, in the band surrounding the partially expanded state
if isMovingDown{collapse all the way (ie, dismiss)} else { // not moving or moving up
expand to the partially expanded state or all the way (ie, full-screen),depending on the value of `supportsPartialExpansion`
}}
// below the band surrounding the partially expanded state
collapse all the way (ie, dismiss)

Swift Package Manager

DrawerKit is distributed via Swift Package Manager. In Xcode, choose File ▸ Add Packages… and enter the repository URL:

https://github.com/Babylonpartners/DrawerKit

Or add it as a dependency in your own Package.swift:

dependencies:[.package(url:"https://github.com/Babylonpartners/DrawerKit", from:"1.0.0"),],targets:[.target(name:"YourTarget", dependencies:["DrawerKit"]),]

Minimum requirements: iOS 12, Swift 5.

About

DrawerKit lets an UIViewController modally present another UIViewController in a manner similar to the way Apple's Maps app works.

Resources

Stars

0 stars

Watchers

5 watching

Forks

Releases

Packages

Used by

Contributors

Languages