Skip to content

A SwiftUI library that makes easier to develop overlay based interfaces, such as the one presented in the Apple Maps app.

License

Notifications You must be signed in to change notification settings

FiveSixTwoInc/DynamicOverlay

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

DynamicOverlay

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.

Platform Swift5 CocoaPods Carthage Build Status License


Requirements

DynamicOverlay is written in Swift 5. Compatible with iOS 13.0+.

Getting started

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.

Examples

Min Max

Magnetic notch overlay

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.

Specifying the 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
}

Drag gesture support

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)
}

Scroll view support

A magnetic notch overlay can coordinate its motion with the scrolling of a scroll view.

Mark the ScrollView or List that should dictate the overlays movement with divingScrollView().

var myOverlayContent: some View {
    VStack {
        Text("Header").draggable()
        List {
            Text("Row 1")
            Text("Row 2")
            Text("Row 3")
        }
        .drivingScrollView()
    }
}

Responding to overlay updates

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)
}

Moving the overlay

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
    }
}

Disabling notches

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)
}

Under the hood

DynamicOverlay is built on top of OverlayContainer. If you need more control, consider using it or open an issue.

Installation

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

Cocoapods

pod 'DynamicOverlay'

Carthage

Add the following to your Cartfile:

github "https://github.com/fabernovel/DynamicOverlay"

Swift Package Manager

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")

Release

  • Create a release branch for the new version (release/#version#)
  • Update the CHANGELOG.md (Be sure to spell your release version correctly)
  • Push your release branch
  • Run the release workflow from your release branch

Author

@gaetanzanella, gaetan.zanella@fabernovel.com

License

DynamicOverlay is available under the MIT license. See the LICENSE file for more info.

About

A SwiftUI library that makes easier to develop overlay based interfaces, such as the one presented in the Apple Maps app.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages

  • Swift 95.4%
  • Ruby 4.1%
  • Objective-C 0.5%