DynamicOverlay is a SwiftUI library. It makes easier to develop overlay based interfaces, such as the one presented in the Apple Maps, Stocks or Shortcuts apps.
DynamicOverlay
is written in Swift 5. Compatible with iOS 13.0+.
A dynamic overlay is an overlay that dynamically reveals or hides the content underneath it.
You add a dynamic overlay as a regular one using a view modifier:
Color.blue.dynamicOverlay(Color.red)
Its behavior is defined by the DynamicOverlayBehavior
associated to it if any.
Color.blue
.dynamicOverlay(Color.red)
.dynamicOverlayBehavior(myOverlayBehavior)
var myOverlayBehavior: some DynamicOverlayBehavior {
...
}
If you do not specify a behavior in the overlay view hierarchy, it uses a default one.
Min | Max |
---|---|
MagneticNotchOverlayBehavior
is a DynamicOverlayBehavior
instance. It is the only behavior available for now.
It describes an overlay that can be dragged up and down alongside predefined notches. Whenever a drag gesture ends, the overlay motion will continue until it reaches one of its notches.
The preferred way to define the notches is to declare an CaseIterable
enum:
enum Notch: CaseIterable, Equatable {
case min, max
}
You specify the dimensions of each notch when you create a MagneticNotchOverlayBehavior
instance:
@State var isCompact = false
var myOverlayBehavior: some DynamicOverlayBehavior {
MagneticNotchOverlayBehavior<Notch> { notch in
switch notch {
case .max:
return isCompact ? .fractional(0.5) : .fractional(0.8)
case .min:
return .fractional(0.3)
}
}
}
There are two kinds of dimension:
extension NotchDimension {
/// Creates a dimension with an absolute point value.
static func absolute(_ value: Double) -> NotchDimension
/// Creates a dimension that is computed as a fraction of the height of the overlay parent view.
static func fractional(_ value: Double) -> NotchDimension
}
By default, all the content of the overlay is draggable but you can limit this behavior using the draggable
view modifier.
Here only the list header is draggable:
var body: some View {
Color.green
.dynamicOverlay(myOverlayContent)
.dynamicOverlayBehavior(myOverlayBehavior)
}
var myOverlayContent: some View {
VStack {
Text("Header").draggable()
List {
Text("Row 1")
Text("Row 2")
Text("Row 3")
}
}
}
var myOverlayBehavior: some DynamicOverlayBehavior {
MagneticNotchOverlayBehavior<Notch> { ... }
}
Here we disable the drag gesture entirely:
var myOverlayContent: some View {
VStack {
Text("Header")
List {
Text("Row 1")
Text("Row 2")
Text("Row 3")
}
}
.draggable(false)
}
A magnetic notch overlay can coordinate its motion with the scrolling of a scroll view.
Mark a view of the overlay content as a drivingScrollView
to do so.
var myOverlayContent: some View {
VStack {
Text("Header").draggable()
List {
Text("Row 1")
Text("Row 2")
Text("Row 3")
}
.drivingScrollView()
}
}
You can track the overlay motions using the onTranslation(_:)
view modifier. It is a great occasion to update your UI based on the current overlay state.
Here we define a control that should be right above the overlay:
struct ControlView: View {
let height: CGFloat
let action: () -> Void
var body: some View {
VStack {
Button("Action", action: action)
Spacer().frame(height: height)
}
}
}
We make sure the control is always visible thanks to the translation parameter:
@State var height: CGFloat = 0.0
var body: some View {
ZStack {
Color.blue
ControlView(height: height, action: {})
}
.dynamicOverlay(Color.red)
.dynamicOverlayBehavior(myOverlayBehavior)
}
var myOverlayBehavior: some DynamicOverlayBehavior {
MagneticNotchOverlayBehavior<Notch> { ... }
.onTranslation { translation in
height = translation.height
}
}
You can also be notified when a notch is reached using a binding:
@State var notch: Notch = .min
var body: some View {
Color.blue
.dynamicOverlay(Text("\(notch)"))
.dynamicOverlayBehavior(myOverlayBehavior)
}
var myOverlayBehavior: some DynamicOverlayBehavior {
MagneticNotchOverlayBehavior<Notch> { ... }
.notchChange($notch)
}
You can move explicitly the overlay using a notch binding.
@State var notch: Notch = .min
var body: some View {
ZStack {
Color.green
Button("Move to top") {
notch = .max
}
}
.dynamicOverlay(Color.red)
.dynamicOverlayBehavior(myOverlayBehavior)
}
var myOverlayBehavior: some DynamicOverlayBehavior {
MagneticNotchOverlayBehavior<Notch> { ... }
.notchChange($notch)
}
Wrap the change in an animation block to animate the change.
Button("Move to top") {
withAnimation {
notch = .max
}
}
When a notch is disabled, the overlay will ignore it. Here we block the overlay in its min
position:
@State var notch: Notch = .max
var myOverlayBehavior: some DynamicOverlayBehavior {
MagneticNotchOverlayBehavior<Notch> { ... }
.notchChange($notch)
.disable(.max, notch == .min)
}
DynamicOverlay
is built on top of OverlayContainer. If you need more control, consider using it or open an issue.
DynamicOverlay
is available through CocoaPods. To install it, simply add the following line to your Podfile:
pod 'DynamicOverlay'
Add the following to your Cartfile:
github "https://github.com/fabernovel/DynamicOverlay"
DynamicOverlay
can be installed as a Swift Package with Xcode 11 or higher. To install it, add a package using Xcode or a dependency to your Package.swift file:
.package(url: "https://github.com/fabernovel/DynamicOverlay.git")
@gaetanzanella, gaetan.zanella@fabernovel.com
DynamicOverlay
is available under the MIT license. See the LICENSE file for more info.