Skip to content

An all-in-one UIKit component for making interactive modals, sheets, drawers, dialogs, and overlays.

License

Notifications You must be signed in to change notification settings

dominicstop/adaptive-modal

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

adaptive-modal

demo-01-to-15

An all-in-one, "config-based" UIViewController modal presentation UIKit component for making: interactive modals, sheets, drawers, dialogs, and overlays, with built in support for:

  • πŸ’– Gesture-driven modal presentation and animation.
  • ❀️ Snapping points, and keyframe-based animations (blurs, 3d transforms, color, alpha, shadows, drag handle, etc).
  • 🧑 "Adaptive" modal config (i.e. modal config that changes based on the current: device attributes/capabilities, size class, rotation, accessibility, etc).
  • πŸ’› "Adaptive" layout (i.e. composable layout values, e.g. percentages, constants, safe area insets, keyboard rects, conditional layout values, etc).
  • πŸ’š Consolidated modal events, and unified/simplified modal state.
  • πŸ’™ Paginated modal content (i.e. each snap point can have an associated "page" view, and the modal content changes based on the current snap point).
  • πŸ’œ Custom/override snapping points, keyboard avoidance, adaptive layout config, custom present/dismiss animations, custom drag handle, etc..



Demo Gifs

See AdaptiveModalConfigDemoPresets file for the config used for the modal.

Demo 01 to 04

Demo 01 to 04

Demo 01 to 04

Demo 13 to 15

paginated-demo-01-02-03-04-05



Demo Videos

Video version of the demo gifs.

render.03.-.demo-01-04.mp4
render.04.-.demo-05-06.mp4
render.05.-.demo-09-12.mp4



Installation

Cocoapods

AdaptiveModal is available through CocoaPods. To install it, simply add the following line to your Podfile:

pod 'AdaptiveModal'

Swift Package Manager (SPM)

Method: #1: Via Xcode GUI:

  1. File > Swift Packages > Add Package Dependency
  2. Add https://github.com/dominicstop/AdaptiveModal.git

Method: #2: Via Package.swift:

  • Open your project's Package.swift file.
  • Update dependencies in Package.swift, and add the following:
dependencies: [
  .package(url: "https://github.com/dominicstop/AdaptiveModal.git",
  .upToNextMajor(from: "1.0.5"))
]



Basic Usage

πŸ”— Full Example

// ✨ Code omitted for brevity

import AdaptiveModal
import ComputableLayout

class AdaptiveModalBasicUsage01 : UIViewController {
  
  @objc func onPressButtonPresentViewController(_ sender: UIButton) {
    let modalConfig = AdaptiveModalConfig(
      snapPoints: [
        AdaptiveModalSnapPointConfig(
          layoutConfig: ComputableLayout(
            horizontalAlignment: .center,
            verticalAlignment: .bottom,
            width: .stretch,
            height: .percent(percentValue: 0.7)
          ),
          keyframeConfig: AdaptiveModalKeyframeConfig(
            modalShadowColor: .black,
            modalShadowOpacity: 0.1,
            modalShadowRadius: 10,
            modalCornerRadius: 15,
            modalMaskedCorners: .topCorners,
            backgroundColor: .black,
            backgroundOpacity: 0.2
          )
        ),
      ],
      snapDirection: .bottomToTop,
      undershootSnapPoint: .automatic,
      overshootSnapPoint: AdaptiveModalSnapPointPreset(
        layoutPreset: .fitScreenVertically
      )
    );
  
    let modalManager = AdaptiveModalManager(
      presentingViewController: self,
      staticConfig: modalConfig
    );
    
    // this can be any `UIViewController` instance...
    let modalVC = ModalViewController();
    modalVC.modalManager = modalManager;
    
    modalManager.presentModal(
      viewControllerToPresent: modalVC,
      presentingViewController: self
    );
  };
};



Documentation

Struct - AdaptiveModalConfig

This struct is uses to configure the modal.


AdaptiveModalConfig Properties - Raw Config

Property Description
πŸ”€ baseSnapPoints
βš›οΈ [AdaptiveModalSnapPointConfig]
Required. Accepts an array of AdaptiveModalSnapPointConfig enum values.

This property defines the various snapping points for the modal. There must be at least one snap point config in this array (i.e. it cannot be empty).

A snap point defines the position of the modal (i.e. layout, e.g. size, position, padding, etc)., as well as the modal's appearance (i.e. keyframe, e.g. shadows, transforms. background blur/opacity/color, etc)., and behavior (e.g. background tap interaction, gesture damping, etc).

As the user swipes the modal (or when you programmatically tell the modal manager instance to snap to a new snap point), it'll interpolate/animate between each snap point.

This means that if "Snap Point A" has a corner radius value of 10, and "Snap Point B" has a corner radius value of 20, then it'll interpolate the corner radius of the modal between the values of 10 and 20 as it gets dragged around (or as the current snap point is changed programmatically).

For more details, see: AdaptiveModalSnapPointConfig.

πŸ“ Note: In the docs, the first item is in the baseSnapPoint array is referred to as the "first snap point", and conversely, the last item is referred to as the "last snap point". This distinction exists to differentiate "undershoot" snap points (i.e. baseUndershootSnapPoint) and "overshoot" snap points (i.e. baseOvershootSnapPoint) .

Eventually, all these snap points will bel combined into a single array in AdaptiveModalConfig.snapPoints. As such, this distinction is useful to clarify which snap point we are referring to.
πŸ”€ baseUndershootSnapPoint
βš›οΈ AdaptiveModalSnapPointPreset
✳️ Default: .automatic
Accepts a AdaptiveModalSnapPointPreset struct value.

This property defines the initial/starting point of the modal; i.e. when the modal is about to be presented, this property defines where the modal will first appear from.

By default, this is set to .automatic (i.e. AdaptiveModalSnapPointPreset.automatic; it's an alias for .init(layoutPreset: .automatic).

A value of .automatic means that the initial starting of the modal will be inferred based on the current modal config's snapDirection.

E.g. if the snapDirection is .bottomToTop, then the undershoot snap point will be: AdaptiveModalSnapPointPreset(
layoutPreset: .offscreenBottom), meaning that the initial position of the modal will be just below the visible area at the bottom of the presenting view.

Because of this, as the the user swipes the modal down to the bottom edge of the "presenting view" in a .bottomToTop modal, the modal will enter a "dismissing" state as it animates to the undershoot snap point (eventually entering the "dismissed" state once it fully snaps to the undershoot snap point).

A AdaptiveModalSnapPointPreset value is similar to a AdaptiveModalSnapPointConfig value, in the sense that they both define a "snapping point". The difference is that AdaptiveModalSnapPointPreset uses a pre-defined layout position via a ComputableLayoutPreset value (e.g. .offscreenLeft, .edgeRight, etc).

πŸ’‘ Note A: In most cases, the "presenting view" is the entire screen.

πŸ’‘ Note B: An "undershoot snap point" is a "derived snap point", meaning that (unless explicitly specified), the existing configuration/properties from it's base/parent snap point will be carried over.

As such, if the base/parent snap point has a corner radius of 10, then the derived/child snap point will also have a corner radius of 10, and so on. This is true for all other attributes of the modal (e.g. layout position/size, keyframe values like opacity, etc).

In other words, a derived snap point is based on a pre-existing snap point (e.g. inheriting/copying over values, and only selectively changing/overwriting some of those values when explicitly provided a new value).

πŸ’‘ Note C: In the case of an "undershoot snap point", it is based on (or derived from) the first snapping point in baseSnapPoints. This is to say, values from the first snap point will be implicitly carried/copied over to the undershoot snap point.

As such, if the first snapping point has background color of red, then the undershoot snap point will also have a background color of red, and so on unless you explicitly overwrite those values.

πŸ’‘ Note D: The undershoot snap point, in conjunction with the first snap point in baseSnapPoints, defines how the modal will be presented.

When the modal is about to be presented, the undershoot snap point defines the starting position of the modal (e.g. that's why it always configured to be offscreen or invisible), and the first snap point in baseSnapPoints defines the final position of the modal.

In other words, these two snap points define the starting and ending keyframes of the modal during presentation.
πŸ”€ baseOvershootSnapPoint
βš›οΈ AdaptiveModalSnapPointPreset?
✳️ Default: nil
Optional. Accepts a AdaptiveModalSnapPointPreset struct value.

Similar to "over-scrolling" in a scroll view, this property defines what happens when the user swipes too far, i.e. when the user swipes past the last snapping point in AdaptiveModalConfig.baseSnapPoints.

In other words, this property defines the "max" snap point (i.e. final position). As such, you would usually configure this such that the modal will be "full screen" (i.e. AdaptiveModalSnapPointPreset(
layoutPreset: .fitScreen)).

This way, when the user "over-scrolls" ("over-swipes"?), the modal will grow bigger, and bigger; such that, when the user's finger reaches the very edge of the presenting view, the modal will be fill the entire area.

On the other hand, if you set this to AdaptiveModalSnapPointPreset(
layoutPreset: .edgeTop) on a .bottomToTop modal, this would define that the final position of the modal will be at the very top edge of the screen.

In other words, when the user "over-scrolls", it would appear as if the user is dragging the modal to the very top of the screen.

πŸ’‘ Note A: If the value of this property is set to nil, then the "last snap point" in AdaptiveModalConfig.baseSnapPoints will be extrapolated (i.e. extended linearly) as the user continues to drag the modal past the last snap point (this behavior can be disabled via: AdaptiveModalManager.shouldEnableOverShooting, or selectively toggled via: AdaptiveModalConfig.interpolationClampingConfig).

πŸ’‘ Note: B: While it is possible to leave this property set to nil, in most cases, explicitly defining a "overshoot" snap point is better due to the fact that, extrapolating the final snap point indefinitely as the user swipes continuously past the last snap point, will often lead to undesirable results (or worse: layout bugs).

For example, let's say we have a .leftToRight modal, and when the user continues swiping to the right, such that the we have to extrapolate the final snap point, then the width of the modal will increase way past the bounds of the presenting view.

If we explicitly set AdaptiveModalSnapPointPreset.layoutPreset to either: .fitScreen, .fitScreenHorizontally, or .fitScreenVertically, then we can ensure that the modal will stay inside the presenting view's bounds, no matter how much the user swipes the modal.

Conversely, if we instead set layoutPreset to explicitly be 80% of the presenting view's width, then the modal's height will never exceed that value.

πŸ’‘ Note C: Similar to an undershoot snap point, an overshoot snap point is also a "derived snap point" (see: baseUndershootSnapPoint + "Note B").

In the case of an overshoot snap point, the base/parent snap point of the modal is the last element in AdaptiveModalConfig.baseSnapPoints. This is to say that the values from the "last snap point" will be implicitly carried/copied over to the overshoot snap point, unless you explicitly provide a value.

This means that if the last snap point has a keyframe opacity of 0.5, then the overshoot snap point will also have a keyframe opacity of 0.5, and so on.

πŸ’‘ Note D: If you do choose to leaving this property set to nil, then you can control the extrapolation behavior of the modal on a per keyframe-basis via the interpolationClampingConfig property.

E.g. via the interpolationClampingConfig property, you can choose to keep extrapolating the modal's height, but choose to clamp the width, etc.
πŸ”€ baseDragHandlePosition
βš›οΈ DragHandlePosition
✳️ Default: .automatic
Accepts an DragHandlePosition enum value.

This property controls the placement of the drag handle relative to the modal content.

By default, this property is set to .automatic. A value of .automatic means that the placement of the drag handle will be automatically inferred based on the AdaptiveModalConfig.snapDirection of the modal.

If you don't want to show a drag handle, set this property to .none. Alternatively, you can also use AdaptiveModalKeyframeConfig.modalDragHandleOpacity property to temporarily hide the drag handle (this is useful if you don't want to selectively show/hide the drag handle for a particular snap point).

AdaptiveModalConfig Properties

Property Description
πŸ”€ snapDirection
βš›οΈ SnapDirection
Required. Accepts a SnapDirection enum value.

This property defines the presentation, transition and swipe direction of the modal, as well as its orientation. E.g. an enum value of .bottomToTop means that modal will be shown starting from the bottom, then upwards, and its orientation is vertical; as such, the primary swipe axis of the modal will be Y).

To re-iterate, the undershoot snap point, in conjunction with the first snap point in baseSnapPoints, defines how the modal will be presented (see baseUndershootSnapPoint + "Note D").

As such these two snap points must match the snapDirection. E.g. a enum value of .bottomToTop means that the undershoot snap point must be above the final position of the first snap point in baseSnapPoints.
πŸ”€ snapPercentStrategy
βš›οΈ SnapPercentStrategy
✳️ Default: .position
Experimental. Accepts a SnapPercentStrategy enum value.

Each snap point has a computed percent value. By default, the percent value is determined based on the snapping point's position in respect to the presenting view's size.
πŸ”€ snapAnimationConfig
βš›οΈ AdaptiveModalSnapAnimationConfig
✳️ Default: .default
Accepts a AdaptiveModalSnapAnimationConfig struct value.

This property configures how the modal will be animated (e.g. duration, easing) when it snaps to a new snap point (i.e. when the user drags the modal, and lets go).

A value of .default (i.e. AdaptiveModalSnapAnimationConfig
.default) means that it'll be configured to use a .springGesture.
πŸ”€ entranceAnimationConfig
βš›οΈ AdaptiveModalSnapAnimationConfig
✳️ Default: .default
Accepts a AdaptiveModalSnapAnimationConfig struct value.

When the modal is presented programmatically, this property will be used to configure the presentation transition (e.g. duration, easing).

A value of .default (i.e. AdaptiveModalSnapAnimationConfig
.default) means that it'll be configured to use a .springGesture.
πŸ”€ exitAnimationConfig
βš›οΈ AdaptiveModalSnapAnimationConfig
✳️ Default: default
Accepts a AdaptiveModalSnapAnimationConfig struct value.

When the modal is dismissed programmatically, this property will be used to configure the dismissal transition (e.g. duration, easing, etc).

A value of .default (i.e. AdaptiveModalSnapAnimationConfig
.default) means that it'll be configured to use a .springGesture.
πŸ”€ interpolationClampingConfig
βš›οΈ AdaptiveModalClampingConfig
✳️ Default: .init()
Accepts a AdaptiveModalClampingConfig struct value.

When no undershoot and/or overshoot config is specified, this property controls how the modal keyframe values (e.g. modal size, position, keyframes) will extrapolated during interpolation (i.e. when the modal is be dragged around).
πŸ”€ initialSnapPointIndex
βš›οΈ Int
✳️ Default: 1
Accepts an Int value.

This property controls which snap point the modal will first snap to when it's presented.

The index value provided must be within the range of the the combined elements of baseSnapPoints, undershoot + overshoot snap points (i.e. AdaptiveModalConfig.snapPoints), where in the element in index 0 is the undershoot snap point, and element in index 1 is the first snap point in baseSnapPoints, etc.
πŸ”€ dragHandleHitSlop
βš›οΈ CGPoint
✳️ Default: CGPoint(x: 15, y: 15)
Accepts a CGPoint value.

"Hit Slop" increases the touch area of a view (w/o affecting layout). This property increases the touch area of the drag handle view.

For example, when you configure the modal drag handle to be very thin/small, you can make it's touch area bigger so that it is easier to drag around.
πŸ”€ modalSwipeGestureEdgeHeight
βš›οΈ CGFloat
✳️ Default: 20
Accepts a CGFloat value.

When the modal's content is a scrollview, the gesture recognizer in the scrollview will prevent the modal's pan gesture recognizer from firing, because the scrollview eats up all the touch events (i.e. the scrollview's gesture recognizer takes precedence over the modal's gesture recognizer).

This property overrides that precedence, and lets the modal's gesture recognizer respond to the touch events that are located at the leading edge of the modal.

In other words, when the user drags on the leading edge of the modal, it will always drag the modal around instead of scrolling the scrollview.

The value you provide to this property determines the height (or width) of the modal's leading edge touch area.

πŸ’‘ Note A: E.g. the leading edge of a .bottomToTop modal, is the topmost edge, and conversely the leading edge of a .topToBottom modal is the bottom edge, etc.
πŸ”€ shouldSetModalScrollView
ContentInsets
βš›οΈ Bool
✳️ Default: false
This property controls whether the AdaptiveModalKeyframeConfig
.modalScrollViewContentInsets modal keyframe is enabled.
πŸ”€ shouldSetModalScrollView
VerticalScrollIndicatorInsets
βš›οΈ Bool
✳️ Default: true
This property controls whether the AdaptiveModalKeyframeConfig
.modalScrollViewVerticalScrollIndicatorInsets modal keyframe is enabled.
πŸ”€ shouldSetModalScrollView
HorizontalScrollIndicatorInsets
βš›οΈ Bool
✳️ Default: true
This property controls whether the AdaptiveModalKeyframeConfig
.modalScrollViewHorizontalScrollIndicatorInsets modal keyframe is enabled.

AdaptiveModalConfig Computed Properties - Derived Config

Property Description
πŸ”€ undershootSnapPoint
βš›οΈ AdaptiveModalSnapPointPreset
Returns the undershoot snap point preset config that will be used by the modal based on the value provided to baseUndershootSnapPoint.

If baseUndershootSnapPoint property is set to .automatic, then the initial starting of the modal will be inferred based on the current modal config's snapDirection.
πŸ”€ overshootSnapPoint
βš›οΈ AdaptiveModalSnapPointPreset?
Returns the overshoot snap point preset config that will be used by the modal based on the value provided to baseOvershootSnapPoint.
πŸ”€ snapPoints
βš›οΈ [AdaptiveModalSnapPointConfig]
This property defines the combined snapping points for the modal.

As mentioned in baseSnapPoints, all of the provided snapping points (i.e. undershoot, base and overshoot snap points), will be combined into a single array. The first item in the array is always the undershoot snap point, and if an overshoot snap point is provided, then the last item is the overshoot snap point. Conversely, the items after undershoot snap point (the second item), and before the overshoot snap point are the baseSnapPoint items.
πŸ”€ dragHandlePosition
βš›οΈ DragHandlePosition
Returns the undershoot snap point preset config that will be used by the modal based on the value provided to baseDragHandlePosition.

If baseDragHandlePosition property is set to .automatic, then the placement of the drag handle will be automatically inferred based on the snapDirection of the modal.

AdaptiveModalConfig Functions

Function Description
πŸ”€ init

Parameters:
πŸ”€ snapPoints
βš›οΈ [AdaptiveModalSnapPointConfig]

πŸ”€ snapDirection
βš›οΈ SnapDirection

πŸ”€ snapPercentStrategy
βš›οΈ SnapPercentStrategy?
✳️ Default: nil

πŸ”€ snapAnimationConfig
βš›οΈ AdaptiveModalSnapAnimationConfig?
✳️ Default: nil

πŸ”€ entranceAnimationConfig
βš›οΈ AdaptiveModalSnapAnimationConfig?
✳️ Default: nil

πŸ”€ exitAnimationConfig
βš›οΈ AdaptiveModalSnapAnimationConfig?
✳️ Default: nil

πŸ”€ interpolationClampingConfig
βš›οΈ AdaptiveModalClampingConfig?
✳️ Default: nil

πŸ”€ initialSnapPointIndex
βš›οΈ Int?
✳️ Default: nil

πŸ”€ undershootSnapPoint
βš›οΈ AdaptiveModalSnapPointPreset?
✳️ Default: nil

πŸ”€ overshootSnapPoint
βš›οΈ AdaptiveModalSnapPointPreset?
✳️ Default: nil

πŸ”€ dragHandlePosition
βš›οΈ DragHandlePosition?
✳️ Default: nil

πŸ”€ dragHandleHitSlop
βš›οΈ CGPoint?
✳️ Default: nil

πŸ”€ modalSwipeGestureEdgeHeight
βš›οΈ CGFloat?
✳️ Default: nil

πŸ”€ shouldSetModalScrollViewContentInsets
βš›οΈ Bool?
✳️ Default: nil

πŸ”€ shouldSetModalScrollView
VerticalScrollIndicatorInsets
βš›οΈ Bool?
✳️ Default: nil

πŸ”€ shouldSetModalScrollView
HorizontalScrollIndicatorInsets
βš›οΈ Bool?
✳️ Default: nil
Each parameter directly initializes a property with the same name in AdaptiveModalConfig. As such, please refer to the AdaptiveModalConfig property docs for more info.

The snapPoints parameter initializes the baseSnapPoints property.


Struct - AdaptiveModalClampingConfig

TBA



Struct - AdaptiveModalConfigMode

TBA



Struct - AdaptiveModalConstrainedConfig

TBA



Struct - AdaptiveModalInterpolationPoint

TBA



Struct - AdaptiveModalKeyframeConfig

TBA



Struct - AdaptiveModalSnapPointPreset

TBA



Enum - AdaptiveModalState

TBA



Enum - AdaptiveModalSnapAnimationConfig

TBA



Enum - AdaptiveModalSnapPointConfig

TBA



AdaptiveModalManager Properties

Config-Related

TBA


Gesture-Related

TBA


General

TBA

AdaptiveModalManager Functions

TBA



Class - AdaptiveModalDragHandleView

TBA



Protocol - AdaptiveModalAnimationEventsNotifiable

TBA



Protocol - AdaptiveModalGestureEventsNotifiable

TBA



Protocol - AdaptiveModalPresentationEventsNotifiable

TBA



Protocol - AdaptiveModalStateEventsNotifiable

TBA



Protocol - AdaptiveModalBackgroundTapDelegate

TBA



Struct - Angle

TBA



Struct - Transform3D

TBA



Topics + Discussion

TBA



Examples

TBA



Misc and Contact

  • 🐀 Twitter/X: @GoDominic
  • πŸ’Œ Email: dominicgo@dominicgo.dev
  • 🌐 Website: dominicgo.dev

About

An all-in-one UIKit component for making interactive modals, sheets, drawers, dialogs, and overlays.

Resources

License

Stars

Watchers

Forks

Packages

No packages published