• Stars
    star
    780
  • Rank 58,299 (Top 2 %)
  • Language
    Swift
  • License
    MIT License
  • Created about 7 years ago
  • Updated over 1 year ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

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

DrawerKit

circleci Carthage Version Platform Swift 4.0.x Xcode License

What is DrawerKit?

DrawerKit is a custom view controller presentation mimicking the kind of behaviour in the Apple Maps app. It lets any view controller modally present another arbitrary view controller. Hence, content is partially shown at first, then more or less content will show by user interaction until it's fully presented or dismissed. It's not (yet) a complete implementation because our 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

DrawerKit DrawerKit DrawerKit

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.

public protocol DrawerCoordinating: class {
    /// An object vended by the conforming object, whose responsibility is to control
    /// the presentation, animation, and interactivity of/with the drawer.
    var drawerDisplayController: DrawerDisplayController? { get }
}

public protocol DrawerPresentable: 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.
    var heightOfPartiallyExpandedDrawer: 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.
    var heightOfCollapsedDrawer: 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,

extension PresenterViewController {
    func doModalPresentation() {
        guard let vc = storyboard?.instantiateViewController(withIdentifier: "presented")
            as? PresentedViewController else { return }

        // you can provide the configuration values in the initialiser...
        var configuration = 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 = 15

        var handleViewConfiguration = HandleViewConfiguration()
        handleViewConfiguration.autoAnimatesDimming = true
        handleViewConfiguration.backgroundColor = .gray
        handleViewConfiguration.size = CGSize(width: 40, height: 6)
        handleViewConfiguration.top = 8
        handleViewConfiguration.cornerRadius = .automatic
        configuration.handleViewConfiguration = handleViewConfiguration

        let borderColor = UIColor(red: 205.0/255.0, green: 206.0/255.0, blue: 210.0/255.0, alpha: 1)
        let drawerBorderConfiguration = DrawerBorderConfiguration(borderThickness: 0.5,
                                                                  borderColor: borderColor)
        configuration.drawerBorderConfiguration = drawerBorderConfiguration

        let drawerShadowConfiguration = 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:

extension PresentedViewController: DrawerPresentable {
    var heightOfPartiallyExpandedDrawer: CGFloat {
        guard let view = self.view as? PresentedView else { return 0 }
        return view.dividerView.frame.origin.y
    }
}

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

extension PresentedViewController {
    @IBAction func 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.
    public var initialState: 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.
    public var totalDurationInSeconds: 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.
    public var durationIsProportionalToDistanceTraveled: 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.
    public var timingCurveProvider: 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.
    public var fullExpansionBehaviour: 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`.
    public var supportsPartialExpansion: 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`.
    public var dismissesInStages: Bool

    /// Whether or not the drawer can be dragged up and down. The default value is `true`.
    public var isDrawerDraggable: Bool

    /// Whether or not the drawer can be fully presentable by tapping on it.
    /// The default value is `true`.
    public var isFullyPresentableByDrawerTaps: Bool

    /// How many taps are required for fully presenting the drawer by tapping on it.
    /// The default value is 1.
    public var numberOfTapsForFullDrawerPresentation: Int

    /// Whether or not the drawer can be dismissed by tapping anywhere outside of it.
    /// The default value is `true`.
    ///
    /// **NOTE:** this only works for states where taps are *not* being passed through
    /// to the presenting view, as the gesture recognizer for these taps is on the
    /// drawer container view. See the `passthroughTouchesInStates` property.
    public var isDismissableByOutsideDrawerTaps: Bool

    /// How many taps are required for dismissing the drawer by tapping outside of it.
    /// The default value is 1.
    public var numberOfTapsForOutsideDrawerDismissal: 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.
    public var flickSpeedThreshold: 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.
    public var upperMarkGap: 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.
    public var lowerMarkGap: 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.
    public var maximumCornerRadius: CGFloat

    /// How the drawer should animate its corner radius if specified. The
    /// default value is `maximumAtPartialY`.
    public var cornerAnimationOption: 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()`.
    public var handleViewConfiguration: 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`.
    public var drawerBorderConfiguration: 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`.
    public var drawerShadowConfiguration: 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.
    ///
    /// **NOTE:** the functionality of `isDismissableByOutsideDrawerTaps` is affected
    /// by how these options are configured.
    public var passthroughTouchesInStates: PassthroughOptions
    public enum FullExpansionBehaviour: Equatable {
        case coversFullScreen
        case dosNotCoverStatusBar
        case leavesCustomGap(gap: CGFloat)
    }
public struct HandleViewConfiguration {
    /// 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.
    public var autoAnimatesDimming: Bool

    /// The handle view's background color. The default value is `UIColor.gray`.
    public var backgroundColor: UIColor

    /// The handle view's bounding rectangle's size. The default value is
    /// `CGSize(width: 40, height: 6)`.
    public var size: 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.
    public var top: 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.
    public var cornerRadius: CornerRadius
}
public struct DrawerBorderConfiguration {
    /// The drawer's layer’s border thickness. The default value is 0,
    /// so effectively the default is not to have any border at all.
    public let borderThickness: 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.
    public let borderColor: UIColor?

    public init(borderThickness: CGFloat = 0, borderColor: UIColor? = nil)
}
public struct DrawerShadowConfiguration {
    /// 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.
    public let shadowOpacity: 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.
    public let shadowRadius: 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.
    public let shadowOffset: 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.
    public let shadowColor: UIColor?

    public init(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)

Carthage

If you use Carthage to manage your dependencies, simply add DrawerKit to your Cartfile:

github "Babylonpartners/DrawerKit"

If you use Carthage to build your dependencies, make sure you have added DrawerKit.framework to the "Linked Frameworks and Libraries" section of your target, and have included them in your Carthage framework copying build phase.

CocoaPods

If you use CocoaPods to manage your dependencies, simply add DrawerKit to your Podfile:

pod 'DrawerKit'

More Repositories

1

fastText_multilingual

Multilingual word vectors in 78 languages
Jupyter Notebook
1,195
star
2

ios-playbook

Ruby
397
star
3

orbit-mvi

An MVI framework for Kotlin and Android
Kotlin
384
star
4

Bento

Swift library for building component-based interfaces on top of UITableView and UICollectionView 🍱
Swift
372
star
5

iOS-Interview-Demo

Interview Demo Project for babylon health
Objective-C
200
star
6

certificate-transparency-android

Certificate transparency for Android and Java
Kotlin
198
star
7

ReactiveFeedback

Unidirectional reactive architecture
Swift
158
star
8

rgat

A TensorFlow implementation of Relational Graph Attention Networks, paper: https://arxiv.org/abs/1904.05811
Python
114
star
9

hmrb

Python
70
star
10

android-playbook

Babylon Health Android Team Playbook
68
star
11

Stevenson

Stevenson is a Vapor framework designed to build integrations between Slack apps, GitHub, JIRA and CI services (CircleCI).
Swift
58
star
12

counterfactual-diagnosis

Python
54
star
13

Tota11y

Accessibility visualization toolkit for web content creators and editors.
JavaScript
44
star
14

fuzzymax

Code for the paper: Don't Settle for Average, Go for the Max: Fuzzy Sets and Max-Pooled Word Vectors, ICLR 2019.
Python
43
star
15

corrsim

Code for the papers: Correlation Coefficients and Semantic Textual Similarity, NAACL-HLT 2019 & Correlations between Word Vector Sets, EMNLP-IJCNLP 2019.
Python
35
star
16

primock57

Dataset of 57 mock medical primary care consultations: audio, consultation notes, human utterance-level transcripts.
Python
35
star
17

lit-fhir

Opinionated library for easily constructuring FHIR (http://hl7.org/fhir) resources in Scala and Java.
Scala
35
star
18

Wall-E

A bot that monitors and manages your pull requests.
Swift
32
star
19

github-proxy

A minimal caching proxy to GitHub's REST & GraphQL APIs
Python
29
star
20

neuralTPPs

Shell
27
star
21

medisim

Medical Similarity Dataset creation from SNOMED
Python
27
star
22

simba

Semantic similarity measures from Babylon Health
Python
16
star
23

decoding-decoders

Python
12
star
24

EHR-Rel

Biomedical concept relatedness benchmark sampled from electronic health records
10
star
25

multiverse

MultiVerse: Probabilistic Programming Language for Causal Reasoning
Python
9
star
26

TwinNetworks

A library for handling Structural Causal Models and performing interventional and counterfactual inference on them.
Python
9
star
27

MCSG

Python
8
star
28

web-interview

JavaScript
6
star
29

nameko-extras

Nameko run with autoloading, logging file CLI option
Python
6
star
30

event-stream-registry-ui

A React component for observing and monitoring event streams.
TypeScript
5
star
31

slack011y-bus

Python
4
star
32

sticky-layoutmanager

Java
2
star
33

fhir-hydrant

FHIR templating engine
Scala
2
star
34

snow-owl

🦉 Snow Owl - production ready, scalable terminology server (SNOMED CT, ICD-10, LOINC, dm+d, ATC and others)
Dockerfile
2
star
35

terraform-provider-aws-babylon

Go
1
star
36

laymaker

1
star