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..
See AdaptiveModalConfigDemoPresets
file for the config used for the modal.
Video version of the demo gifs.
render.03.-.demo-01-04.mp4
render.04.-.demo-05-06.mp4
render.05.-.demo-09-12.mp4
AdaptiveModal
is available through CocoaPods. To install it, simply add the following line to your Podfile
:
pod 'AdaptiveModal'
Method: #1: Via Xcode GUI:
- File > Swift Packages > Add Package Dependency
- Add
https://github.com/dominicstop/AdaptiveModal.git
Method: #2: Via Package.swift
:
- Open your project's
Package.swift
file. - Update
dependencies
inPackage.swift
, and add the following:
dependencies: [
.package(url: "https://github.com/dominicstop/AdaptiveModal.git",
.upToNextMajor(from: "1.0.5"))
]
// β¨ 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
);
};
};
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. |
TBA
TBA
TBA
TBA
TBA
TBA
TBA
TBA
TBA
Config-Related
TBA
Gesture-Related
TBA
General
TBA
TBA
TBA
TBA
TBA
TBA
TBA
TBA
TBA
TBA
TBA
TBA
- π€ Twitter/X:
@GoDominic
- π Email:
[email protected]
- π Website: dominicgo.dev