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

Overview

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'
Comments
  • Introducing DrawerKit

    Introducing DrawerKit

    This PR introduces our implementation of "drawer views" that behave similarly to those in the Apple Maps app.

    The architecture is based on a custom presentation to present and dismiss the drawer view, coupled with a pan gesture recogniser to support dragging the drawer up and down. There is also a tap recogniser to dismiss the drawer when the user taps outside of it.

    ~There are several configuration parameters and you're urged to play with the demo app to explore them.~

    ~Speaking of the demo app, it's still incomplete in terms of supporting some of those configurations, although the library itself does support them. For instance, the library lets you set which animation timing curve to use but the demo app doesn't support that yet.~

    I've since simplified the demo app to its bare minimum. In order to play with different configuration parameters, you'll need to change the code, recompile, rebuild, and rerun the app.

    ready for review 
    opened by wltrup 19
  • Configurable initial state and collapsed state height

    Configurable initial state and collapsed state height

    This PR adds support for configurable initial state and collapsed state height. Currently it's only possible to completely hide drawer in collapsed state which also results in dismissing view controller. With this change it's possible to make drawer visible not just in partial or fully expanded state, but also in collapsed state without introducing any additional state.

    20181024211114

    When collapsed state is chosen as initial and it has non-zero height then dismissing on collapsed state becomes unneeded so it is not happening in this case. ~Also added ability to not animate corner radius as with current implementation it will be animated in between collapsed and partially expanded state which also seems to be undesirable when collapsed state has non-zero height. Though it can be made configurable with minimumCornerRadius which will correspond to corner radius in collapsed state. But this can be done as a separate PR.~ This is now part of #77

    opened by ilyapuchka 16
  • Introducing DrawerKit

    Introducing DrawerKit

    This PR introduces DrawerKit and adds a few minor fixes at the same time.

    Overall architecture

    This implementation uses view controller containment to remove the view of the content view controller and add it to the drawer view of an intermediate view controller which is then presented modally by the presenter view controller. That way the drawer content is not owned by the presenter view controller (as it would have to be in a plain view animation architecture).

    (If you're paying attention, "intermediate view controller which is then presented modally by the presenter view controller" means that what the presenter thinks its presenting, it is not!)

    You might rightfully ask why not use the APIs for custom view controller transitions? Well, I tried. In addition to some extremely frustrating undocumented gotchas, such as:

    • UIViewPropertyAnimator's fractionComplete isn't updated through a setter method despite the fact that the class is open to subclassing and that property is open to overriding
    • the documentation says fractionComplete can go outside the range [0, 1] but, in fact, it cannot; any such changes are clamped to the unit interval
    • subclassing UIViewPropertyAnimator when using it with UIPercentDrivenInteractiveTransition is tricky because the latter hijacks the behaviour of the former in undocumented ways, again in relation to updating fractionComplete

    there was the ultimate issue for what we want to achieve with the drawers, namely, that a (modal) presentation is a one-shot event. Say that you use a custom presentation to present the drawer to its partially-expanded state. If you then try to present it to its fully-expanded state, you get a runtime error because UIKit doesn't let you present a view controller that is already being presented. Dismissing is even worse in that there's no notion of a partial dismissal. If you try to dismiss a view controller with a custom presentation that only dismisses it partially (as the transition from fully-expanded to partially-expanded would require), you're out of luck. A dismissal is a dismissal is a dismissal and your view controller is no longer active after one, whether or not its view should remain partially visible.

    So, after much struggle and frustration, the only working solution I found is to use view controller containment. In a way, it goes back to the plain view animation approach, except that it doesn't require the drawer content to live with the view controller that presents it.

    But...

    ... I had to drop support for animating either or both view controller contents alongside the drawer animation. The reason is that there is no single, obvious, or clear approach to how those animations should be performed. An interactive and scrubbable animation using UIViewPropertyAnimator (the only API supporting interaction and scrubbing, currently) relies on the fractionComplete property. The animation starts at 0 and ends at 1, and the progress of any and all animations is tied to the value of that property. In addition to the fact that the actual behaviour contradicts the documented behaviour regarding its range (see previous section above), imagine that you animate the drawer from collapsed to partially-expanded. Any view controller animations could be done then and that's fine. Say that we're animating the transparency of the drawer content as the drawer animates up so that the drawer content starts at 0 and ends at 1 as it moves from collapsed to partially-expanded. But now the user drags the drawer up to fully-expanded. Now what? There's nothing else to animate. However, since the second animation (from partial to full) is also driven by UIViewPropertyAnimator's fractionComplete, it would start at value 0 at the partially-expanded state and end at 1 at the fully-expanded state, which means the moment the user starts dragging the drawer up from the partial state, the drawer content suddenly becomes transparent and starts becoming opaque as the drawer reaches the fully-expanded state. There are other subtle situations like that one.

    I believe that there is a solution to this problem but it isn't a simple one in that it will require more effort from the two view controllers. It will require them to know more about the drawer transitions than simply which state the drawer is in. I'm still musing on this issue but decided, for the time being, to remove support for those animations.

    How to use it

    • Any view controller wanting to present a drawer needs to conform to the DrawerPresenting protocol and any view controller allowing itself to be presented as a drawer needs to conform to DrawerPresentable.
    • At the time of presentation, the presenter view controller instantiates a drawerTransitionController object (an instance of TransitionController), passing to it itself, the presented view controller (the view controller having the content to be added to the drawer), and a configuration object (an instance of TransitionConfiguration) for the controller.
    • To present the drawer (it always starts at the collapsed state), call drawerTransitionController?.present() from the presenter view controller
    • To dismiss the drawer, call drawerTransitionController?.dismiss() from the presented (content) view controller
    • Note that there are no completion closures at the level of these calls because preparation, cleanup, and completion closures (and, formerly, also animate-along closures) are provided to the controller through an instance of the TransitionActions struct. This allows both view controllers to perform those actions on their respective views on a drawer-state by drawer-state basis.

    Other things to consider when reviewing this PR

    • Try the test app, preferably on a device since I've noticed that the simulator sometimes creates visual artefacts
    • Look at the TODO list at the top of DrawerViewController

    What does the fixed-height content do?

    It simulates the situation where the drawer content isn't tall enough to occupy the full screen so it wouldn't make sense to expand it all the way up. In that case, the fully-expanded state of the drawer expands the drawer content only as far as it can go.

    What does the supports partial expansion do?

    If you toggle that off then it's like a normal full-screen presentation/dismissal. The drawer never stops at the partially-expanded position.

    What does the staged dismissal do?

    If partial expansion is turned on, then when dismissing from fully-expanded the drawer would stop at partially-expanded and the user would need to dismiss it again to get it collapsed. If this option is off, then a dismissal goes all the way from fully-expanded to collapsed, in one go. Note that you could have staged presentation but non-staged dismissal.

    Taps outside the drawer

    Tap on the number on the right to change the number of taps from 1 (the default) to 2 to 3 to 0. Zero turns the feature off so tapping outside of the drawer does nothing.

    But... how do I get the drawer to appear??

    Tap on the big red square, of course!

    demo_app

    opened by wltrup 6
  • DrawerKit with RxCocoa, UITableView `didSelectRow` not called

    DrawerKit with RxCocoa, UITableView `didSelectRow` not called

    I use DrawerKit to display a UIViewController with an UITableView. I use RxSwift / RxCocoa to handle UITableView data / events.

    I've noticed that DrawerKit PullToDismissManager can't forward event to RxTableViewDelegateProxy as it does with UITableViewDelegate:

    • If I don't implement scrollViewForPullToDismiss, tableView.rx.itemSelected works as usual.
    • If I implement scrollViewForPullToDismiss with a classic UITableViewDelegate approach, implementation of didSelectRowAt:index works as usual.
    • If I implement scrollViewForPullToDismiss, tableView.rx.itemSelected doesn't work.

    I'm not sure if it's more a question to address in RxCocoa side or DrawerKit. I wonder if you had this issue before and have a hint to proceed except reimplementing UITableViewDelegate for that specific view.

    Thanks

    opened by popei69 4
  • CornerRadius only affecting border, not drawer itself

    CornerRadius only affecting border, not drawer itself

    I'm playing with the demo app and the corner radius doesn't seem to be affecting the actual VC itself. This is how it looks in the simulator on 11.3 Xcode 9.3.1. Border is there, but it wont affect the drawer itself. Am I missing something? image

    bug question 
    opened by Umar-M-Haroon 3
  • Add config to disable dismissal of collapsed drawer

    Add config to disable dismissal of collapsed drawer

    Hello!

    Here's a PR to allow users disable dismissal of a drawer view and keep some height of it always on screen.

    I think i can reference this issue here.

    opened by georgmay 3
  • Blur background, Question

    Blur background, Question

    Hey, Thanks for the library! Great job!

    I was wondering what's the best way to modify to add blur background when the view not fully presented, to cover upper part?

    Thanks!

    question 
    opened by AlexeyIS 3
  • Presented view controller captures gestures

    Presented view controller captures gestures

    Using DrawerKit to present a UICollectionView or UITableView controller means that the presented controller captures touches and prevents being able to pull the draw up/down. In the Apple Maps example, touches are ignored by the table view until the draw is 100%.

    help wanted 
    opened by cameroncooke 3
  • Drawer corners are not shown on initial presentation.

    Drawer corners are not shown on initial presentation.

    On initial presentation, the drawer corners are not displayed. They only appear (and then remain) after moving the drawer:

    drawercorners

    I'm using the following DrawerConfiguration:

    DrawerConfiguration(
            fullExpansionBehaviour: .leavesCustomGap(gap: 300),
            cornerAnimationOption: .none,
            handleViewConfiguration: HandleViewConfiguration(autoAnimatesDimming: false)
     )
    

    And my DrawerPresentable implementation has just the following:

    var heightOfPartiallyExpandedDrawer = CGFloat(200)
    
    bug 
    opened by mluisbrown 2
  • Support interactions on presenting view

    Support interactions on presenting view

    Currently the presented view is not responding to touches when drawer view is presented because it appears behind UITransitionView added by UIKit, which intercepts all the touches. One option to allow interactions of presented view could be to add its view as subview of transitionContext.containerView but that will make it to collide with tap recogniser and can result to unexpected behaviour when dismissing drawer with tap (if drawer is attempted to be presented twice then after it is dismissed UIKit does not call presentation controller methods) Instead we put container view inside a subclass of UIView that implements hitTest and delegates it to presentation controller to decide what view, presented or container view should receive touches. The gif shows that the view on background is accessible in partial expanded state but not in full screen, which can be configured.

    20181024233738

    opened by ilyapuchka 2
  • Reduce API surface.

    Reduce API surface.

    Depends on #2.

    The public DrawerKit API has been reduced to three pieces: DrawerConfiguration, DrawerController and PartiallyExpandableDrawer.

    A view controller that wishes to be presented as a drawer can simply:

    1. create and retain DrawerController with its desired configuration; and
    2. set the DrawerController as its transitioning delegate.

    If it wishes to support partial expansion, on top of enabling the setting in DrawerConfiguration, the drawer view controller needs to conform to also PartiallyExpandableDrawer.

    How does it work?

    DrawerController tracks if it has ever created any PresentationController. Since UIKit pretty much guarantees that it asks for a presentation controller before the animators, we can explore this in order to retrieve the presenting VC for the animator dynamically.

    It also works when the view controller is recycled. Since UIKit would release the presentation controller upon conclusion of a presentation, this means the weak reference in DrawerController would be zeroed, and hence a new PresentationController would be created next time the drawer is presented.

    Example

    https://github.com/Babylonpartners/DrawerKit/pull/3/files#diff-797161087489cdf11d7916a4480c4f93R4

    Basic hello world example:

    drawerController = DrawerController(configuration: DrawerConfiguration())
    modalPresentationStyle = .custom
    transitioningDelegate = drawerController
    
    refactoring 
    opened by andersio 2
  • Content size awareness

    Content size awareness

    Similarly to the issues described in #101 there seems to be no good way to consider content size of the view presented in the drawer for it's partially expanded state (as well as for fully expanded). I've tried to solve that by adopting DrawerPresentable and return the content size from its heightOfPartiallyExpandedDrawer like this:

    extension DrawerMapPlaceDetailsViewController: DrawerPresentable, DrawerAnimationParticipant {
        public var heightOfPartiallyExpandedDrawer: CGFloat {
            return self.tableView.contentSize.height + (view.window?.safeAreaInsets.bottom ?? 0)
        }
    }
    

    This seems to work visually without issues but internally the state of the drawer changes and it is not in the partially expanded state because during transition the value returned by this method changes from 0 to final value (for some reason it changes one more time adding 4 pixels, but that's probably for reasons unrelated to DrawerKit) and as a result drawer is in the transitioning state, which breaks interactions with background view a they are only considering "final" states (collapsed, partially expanded and fully expanded). One fix for that might be to consider transition state while detecting touches, but that does not seem like a solution of underlying problem.

    opened by ilyapuchka 1
  • CustomGap doesn't affect the final size of presented view

    CustomGap doesn't affect the final size of presented view

    I'm trying to place UI component depending of the size of the presented view, for instance having a button at the bottom of the drawer.

    When testing it, it seems that the size of the presented view is never matching the size left from the custom gap. For instance, if I set a .leavesCustomGap(gap: 100), the view presented frame won't get shorter of 100. Therefore any elements at the bottom might not be visible.

    I guess what I would expect is that if the drawer can have a maximum size of 500, its presented view and contained one would reflect the same size. That's not the case at the moment. Is it something available with DrawerKit?

    To replicate it, inspect the view frame in a presented view controller (in viewDidAppear for instance), it will be the size of the screen regardless the maximum size available of the drawer.

    opened by popei69 4
  • Add `DrawerBackgroundConfiguration` to customize arbitrary background

    Add `DrawerBackgroundConfiguration` to customize arbitrary background

    This PR adds customizability of background view via DrawerBackgroundConfiguration. Currently supporting DrawerBackgroundConfiguration.dark as a built-in style, and can be tested from Demo app.

    Also, this PR will supersede #63 by importing 3rd party dynamic blur view library e.g. https://github.com/efremidze/VisualEffectView and add the following configuration manually:

    extension DrawerBackgroundConfiguration {
        public static func visualEffect(style: UIBlurEffectStyle, blurRadius: CGFloat) -> DrawerBackgroundConfiguration {
            return DrawerBackgroundConfiguration(
                make: { () -> VisualEffectView in
                    let effect = UIBlurEffect(style: style)
                    let blurView = VisualEffectView(effect: effect)
                    return blurView
                },
                handle: {
                    $0.blurRadius = blurRadius * (1 - $1.currentY / $1.containerHeight)
                }
            )
        }
    }
    
    feature 
    opened by inamiy 0
  • Refactor notification types

    Refactor notification types

    Deprecated enum DrawerNotification in favor of new protocol DrawerNotificationInfo with struct-based conforming types.

    Note: This is non-breaking changes

    Why?

    Current enum DrawerNotification is a bad practice of using sum type due to the following reasons:

    • Pattern matching is useless
    • Breaking change for every addition of notification cases
    refactoring 
    opened by inamiy 0
  • Changing state after previously changing it to dismissed results in drawer

    Changing state after previously changing it to dismissed results in drawer "jumping" before it is being dismissed

    Drawer state change animation is implemented in a way that does not forbid changing to a different state while previous transition animation is still in progress (related to #86). Because of that when dismissing drawer (by calling setDrawerState(.dismissed)) and then changing state to something else, i.e. partially expanded, you can see that it is animating to this new state even though the intention is for it to be dismissed.

    20190320184605

    bug 
    opened by ilyapuchka 0
Releases(v0.7.0)
  • v0.7.0(Oct 30, 2018)

    • DrawerKit now supports interactions with the presenting view. By default, the presenting view will receive touch events when the drawer is in the collapsed or partiallyExpanded state. This can be configured via the new passthroughTouchesInStates configuration.

    • Drawer corner radius animation can now be disabled.

    • DrawerKit now has a new state, dismissed, to differentiate between being collapsed (to a particular non-zero height) and being actually dismissed.

    • It's possible to make the drawer visible on screen when in the collapsed state, via the new property heightOfCollapsedDrawer in the DrawerPresentable protocol.

    • The drawer can now be presented in a particular initial state, via the new initialState configuration.

    Source code(tar.gz)
    Source code(zip)
  • 0.6.0(Apr 12, 2018)

    • DrawerKit now supports pull-to-dismiss driven by a UIScrollView inside the drawer content. (#58)

      You may specify the UIScrollView to DrawerKit through its presentation controller:

      override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
      
        // Install `tableView` as the pull-to-dismiss participant.
        drawerPresentationController?.scrollViewForPullToDismiss = tableView
      }
      
    • Drawer corners may now be configured in DrawerConfiguration to be always visible below the status bar. (#61)

    Source code(tar.gz)
    Source code(zip)
  • 0.5.0(Apr 6, 2018)

    • DrawerKit now supports overCurrentContext and overFullScreen modal presentations over the drawer. (#56)

    • Fixed the issue of touches on the drawer being cancelled by the DrawerKit internal gesture recognizers. (#57)

    • UIControls within the drawer are now interactive when the drawer is partially expanded. (#57)

    Source code(tar.gz)
    Source code(zip)
  • 0.4.1(Nov 27, 2017)

    • Reverses the fix for the issue about safe areas. The fix broke other things and the issue will need to be re-opened.
    • Changes the minimum deployment target to 10.2.
    Source code(tar.gz)
    Source code(zip)
  • 0.4.0(Nov 24, 2017)

    • Drawers can now have borders and shadows, all configurable.
    • Fixed a bug by which dragging the drawer all the way to the top would not execute the animation completion block.
    • Fixed a reported issue by which safe area insets were misbehaving.
    • Removed the configuration parameter hasHandleView since it can be inferred from the value of handleViewConfiguration, which is now an optional.
    • Fixed incorrect spelling in an enumeration case (DrawerConfigurationFullExpansionBehaviour.doesNotCoverStatusBar)
    Source code(tar.gz)
    Source code(zip)
  • 0.3.4(Nov 15, 2017)

  • 0.3.3(Nov 9, 2017)

    • Fixes an issue where the presented view controller's view might not be laid out properly by the time the drawer's height is requested from it.
    Source code(tar.gz)
    Source code(zip)
  • 0.3.2(Nov 8, 2017)

    • Better support for concurrent animations: in previous versions, the actual presenting view controller wasn't necessarily what you'd think is the presenting view controller, which caused problems when trying to animate its view concurrently with the drawer animation. Now, although it's still the case that the presenting view controller may not be the view controller you think it is, the view controller that you think is the presenting view controller and which you add conformance to DrawerAnimationParticipant is the view controller whose animation closures get invoked.

    • Notifications: it's now possible to subscribe to notifications indicating when the drawer is tapped (both in its interior and in its exterior), when drawer transitions are about to start, and when they're completed.

    Source code(tar.gz)
    Source code(zip)
  • 0.3.0(Nov 6, 2017)

    Specific changes and new features are as follows:

    • Concurrent animations: it's now possible for either or both view controllers (presenting and presented) to participate in the drawer animation so that their views can be animated while the drawer is moving up and down.

    • Automatic display of a "handle view": it's now possible to have the drawer add a "gray bar" near its top. This bar, referred to as the "handle view", can be customised in its size, background color, and corner radius, and can be automatically dimmed as the drawer moves towards its collapsed or fully-expanded states. Or you can turn that off and throw your own.

    • Support for not expanding to cover the entire screen. It's now possible to select the behaviour of the drawer when it fully-expands itself. You may choose from covering the entire screen (the default), not covering the status bar, and leaving a gap at the top, of any desired size.

    • Partial transitions (collapsed to partially-expanded and partially-expanded to fully-expanded, and vice-versa) can now have durations that are equal to, or fractions of, the duration for a full-size transition (collapsed to fully-expanded, and vice-versa). This allows for transitions to have the same speed, if desired.

    Source code(tar.gz)
    Source code(zip)
  • 0.1(Oct 17, 2017)

    This release has no functional changes. It only:

    • adds a gif to the README
    • fixes the circle CI badge
    • updates the version number (it was mistakenly set to 0.0.1 before)
    Source code(tar.gz)
    Source code(zip)
  • 0.0.1(Oct 17, 2017)

Owner
Babylon Health
Putting an accessible and affordable health service in the hands of every person on earth.
Babylon Health
A better way to present a SFSafariViewController or start a ASWebAuthenticationSession in SwiftUI.

BetterSafariView A better way to present a SFSafariViewController or start a ASWebAuthenticationSession in SwiftUI. Contents Motivation Requirements U

Dongkyu Kim 392 Dec 31, 2022
UI Component. This is a copy swipe-panel from app: Apple Maps, Stocks. Swift version

ContainerController UI Component. This is a copy swipe-panel from app: https://www.apple.com/ios/maps/ Preview Requirements Installation CocoaPods Swi

Rustam 419 Dec 12, 2022
A SwiftUI bottom-up controller, like in the Maps app. Drag to expand or minimize.

SwiftUI Drawer A SwiftUI bottom-up controller, like in the Maps app. Drag to expand or minimize. Contents Add the Package Basic Usage Examples Credits

Michael Verges 695 Jan 3, 2023
iOS custom controller used in Jobandtalent app to present new view controllers as cards

CardStackController iOS custom controller used in the Jobandtalent app to present new view controllers as cards. This controller behaves very similar

jobandtalent 527 Dec 15, 2022
A library to imitate the iOS 10 Maps UI.

Pulley A library to imitate the drawer in Maps for iOS 10/11. The master branch follows the latest currently released version of Swift. If you need an

52inc 2k Dec 29, 2022
Pull up controller with multiple sticky points like in iOS Maps

PullUpController Create your own pull up controller with multiple sticky points like in iOS Maps Features Multiple sticky points Landscape support Scr

Mario Iannotta 1.2k Dec 22, 2022
Confetti View lets you create a magnificent confetti view in your app

ConfettiView Confetti View lets you create a magnificent confetti view in your app. This was inspired by House Party app's login screen. Written in Sw

Or Ron 234 Nov 22, 2022
A panel component similar to the iOS Airpod battery panel or the Share Wi-Fi password panel.

A SwiftUI panel component similar to the iOS Airpod battery panel or the Share Wi-Fi password panel.

Red Davis 12 Feb 7, 2022
A page control similar to that used in Instagram

ISPageControl ISPageControl has a page control similar to that used in the Instagram Contents Requirements Installation Usage Communication Credits Li

Interactive 291 Dec 5, 2022
Twinkle is a Swift and easy way to make any UIView in your iOS or tvOS app twinkle.

Twinkle ✨ Twinkle is a Swift and easy way to make any UIView in your iOS or tvOS app twinkle. This library creates several CAEmitterLayers and animate

patrick piemonte 600 Nov 24, 2022
A way to quickly add a notification badge icon to any view. Make any view of a full-fledged animated notification center.

BadgeHub A way to quickly add a notification badge icon to any view. Demo/Example For demo: $ pod try BadgeHub To run the example project, clone the r

Jogendra 772 Dec 28, 2022
Fashion is your helper to share and reuse UI styles in a Swifty way.

Fashion is your helper to share and reuse UI styles in a Swifty way. The main goal is not to style your native apps in CSS, but use a set

Vadym Markov 124 Nov 20, 2022
An easy way to add a shimmering effect to any view with just one line of code. It is useful as an unobtrusive loading indicator.

LoadingShimmer An easy way to add a shimmering effect to any view with just single line of code. It is useful as an unobtrusive loading indicator. Thi

Jogendra 1.4k Jan 4, 2023
☠️ An elegant way to show users that something is happening and also prepare them to which contents they are awaiting

Features • Guides • Installation • Usage • Miscellaneous • Contributing ?? README is available in other languages: ???? . ???? . ???? . ???? . ???? To

Juanpe Catalán 11.7k Jan 6, 2023
Custom emojis are a fun way to bring more life and customizability to your apps.

Custom emojis are a fun way to bring more life and customizability to your apps. They're available in some of the most popular apps, such as Slack, Di

Stream 244 Dec 11, 2022
List tree data souce to display hierachical data structures in lists-like way. It's UI agnostic, just like view-model and doesn't depend on UI framework

SwiftListTreeDataSource List tree data souce to display hierachical data structures in lists-like way. It's UI agnostic, just like view-model, so can

Dzmitry Antonenka 26 Nov 26, 2022
The Bloc Pattern is a way to separate UI and Logic in SwiftUI codes;

The Bloc Pattern is a way to separate UI and Logic in SwiftUI codes. The Bloc is like a state machine where it accepts an event and produce a state.

mehdi sohrabi 3 Apr 20, 2022
A simple way to hide the notch on the iPhone X

NotchKit NotchKit is a simple way to hide the notch on the iPhone X, and create a card-like interface for your apps. Inspired by this tweet from Sebas

Harshil Shah 1.8k Dec 5, 2022