Repository: faberNovel/DynamicOverlay
Branch: main
Commit: 2276a60762a8
Files: 69
Total size: 145.5 KB
Directory structure:
gitextract_s05uf70j/
├── .github/
│ └── workflows/
│ ├── ci.yml
│ └── release.yml
├── .gitignore
├── CHANGELOG.md
├── DynamicOverlay.podspec
├── DynamicOverlay_Example/
│ ├── DynamicOverlay_Example/
│ │ ├── Classes/
│ │ │ └── MapApp.swift
│ │ ├── Configuration/
│ │ │ └── Info.plist
│ │ ├── Resources/
│ │ │ ├── Assets.xcassets/
│ │ │ │ ├── AppIcon.appiconset/
│ │ │ │ │ └── Contents.json
│ │ │ │ └── Contents.json
│ │ │ └── Base.lproj/
│ │ │ └── LaunchScreen.storyboard
│ │ ├── UIKit/
│ │ │ └── UIKitAppDelegate.swift
│ │ └── View/
│ │ ├── ActionCell.swift
│ │ ├── BackdropView.swift
│ │ ├── FavoriteCell.swift
│ │ ├── MapRootView.swift
│ │ ├── MapView.swift
│ │ ├── OverlayBackgroundView.swift
│ │ ├── OverlayView.swift
│ │ └── SearchBar.swift
│ └── DynamicOverlay_Example.xcodeproj/
│ ├── project.pbxproj
│ ├── project.xcworkspace/
│ │ ├── contents.xcworkspacedata
│ │ └── xcshareddata/
│ │ ├── IDEWorkspaceChecks.plist
│ │ └── swiftpm/
│ │ └── Package.resolved
│ └── xcshareddata/
│ └── xcschemes/
│ └── DynamicOverlay_Example.xcscheme
├── Gemfile
├── LICENSE
├── Package.resolved
├── Package.swift
├── Package.xcconfig
├── README.md
├── Source/
│ ├── DynamicOverlay.h
│ ├── Info.plist
│ ├── Internal/
│ │ ├── DynamicOverlayBehaviorValue.swift
│ │ ├── DynamicOverlayNotchTransition+DynamicOverlayBehavior.swift
│ │ ├── Handle/
│ │ │ ├── ActivatedOverlayArea.swift
│ │ │ ├── ActiveOverlayAreaViewModifier.swift
│ │ │ ├── Drag/
│ │ │ │ ├── DynamicOverlayDragArea.swift
│ │ │ │ └── OnDragAreaChangeViewModifier.swift
│ │ │ ├── DrivingScrollView/
│ │ │ │ ├── DynamicOverlayScrollViewProxy.swift
│ │ │ │ └── OnDrivingScrollViewChangeViewModifier.swift
│ │ │ └── OverlayContainerCoordinateSpace.swift
│ │ ├── MagneticNotchOverlayBehaviorValue.swift
│ │ ├── OverlayContainer/
│ │ │ ├── DynamicOverlayContainerAnimationController.swift
│ │ │ ├── OverlayContainerCoordinator.swift
│ │ │ ├── OverlayContainerDynamicOverlayView.swift
│ │ │ ├── OverlayContainerRepresentableAdaptor.swift
│ │ │ ├── OverlayContainerStateDiffer.swift
│ │ │ └── SwiftUIOverlayContainerRepresentableAdaptor.swift
│ │ ├── OverlayContentModifier.swift
│ │ ├── OverlayNotchIndexMapper.swift
│ │ └── Utils/
│ │ └── Binding+CaseIterable.swift
│ └── Public/
│ ├── DynamicOverlayBehavior.swift
│ ├── DynamicOverlayHandle.swift
│ ├── DynamicOverlayModifier.swift
│ └── MagneticNotchOverlayBehavior.swift
├── Tests/
│ └── DynamicOverlayTests/
│ ├── DragHandleViewModifierTests.swift
│ ├── DrivingScrollViewModifierTests.swift
│ ├── MagneticNotchOverlayBehaviorValueTests.swift
│ ├── NotchBindingDynamicOverlayTests.swift
│ ├── NotchDimensionDynamicOverlayTests.swift
│ ├── NotchTranslationDynamicOverlayTests.swift
│ ├── OverlayContainerRepresentableAdaptorTests.swift
│ ├── OverlayNotchIndexMapperTests.swift
│ └── Utils/
│ ├── ValuePublisher.swift
│ ├── View+Measure.swift
│ ├── ViewInspector.swift
│ └── ViewRenderer.swift
└── fastlane/
├── Fastfile
└── Pluginfile
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/workflows/ci.yml
================================================
name: CI
on: [push]
jobs:
build:
runs-on: macOS-latest
steps:
- uses: actions/checkout@v2
- name: Bundle install
working-directory: ./
run: bundle install
- name: Unit tests
run: bundle exec fastlane tests
- name: SPM lint
run: bundle exec fastlane spm_lint
- name: Carthage lint
run: bundle exec fastlane carthage_lint
- name: Pod lint
run: bundle exec fastlane pod_lint
================================================
FILE: .github/workflows/release.yml
================================================
name: Release
on:
workflow_dispatch:
inputs:
name:
description: 'Version name'
required: true
jobs:
build:
runs-on: macOS-latest
steps:
- uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: latest-stable
- name: Checkout
uses: actions/checkout@v2
- name: Bundle install
working-directory: ./
run: bundle install
- name: Release
env:
LC_ALL: en_US.UTF-8
LANG: en_US.UTF-8
GITHUB_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN_CI }}
COCOAPODS_TRUNK_TOKEN: ${{ secrets.COCOAPODS_GZ_TOKEN }}
GIT_COMMITTER_NAME: Bot Fabernovel
GIT_AUTHOR_NAME: Bot Fabernovel
GIT_COMMITTER_EMAIL: ci@fabernovel.com
GIT_AUTHOR_EMAIL: ci@fabernovel.com
run: bundle exec fastlane release version:${{ github.event.inputs.name }} bypass_confirmations:true
================================================
FILE: .gitignore
================================================
# OS X
.DS_Store
# Xcode
## Build generated
DerivedData/
## Various settings
*.pbxuser
!default.pbxuser
*.mode1v3
!default.mode1v3
*.mode2v3
!default.mode2v3
*.perspectivev3
!default.perspectivev3
xcuserdata/
*.xccheckout
profile
## Other
*.moved-aside
*.xccheckout
DerivedData
*.xcscmblueprint
## Obj-C/Swift specific
*.hmap
*.ipa
*.dSYM.zip
*.dSYM
!Carthage/Build/**/*.dSYM
# Bundler
.bundle
# Swift Package Manager
.build
.swiftpm
Example/Pods/
Pods/
.idea/
# fastlane specific
fastlane/report.xml
# deliver temporary files
fastlane/Preview.html
# snapshot generated screenshots
fastlane/screenshots/**/*.png
fastlane/screenshots/screenshots.html
# scan temporary files
fastlane/test_output
test_output
fastlane/.env
pre-change.yml
.build
fastlane/README.md
================================================
FILE: CHANGELOG.md
================================================
## [1.0.2]
### Fixed
- Jumping animation issue. #42
## [1.0.0]
### Fixed
- Overlay with multiple scroll views #16
## [1.0.0-beta.10]
### Fixed
- Crash with drivingScrollView a List and a condition #21
- Bump OverlayContainer to `3.5.2`
================================================
FILE: DynamicOverlay.podspec
================================================
#
# Be sure to run `pod lib lint DynamicOverlay.podspec' to ensure this is a
# valid spec before submitting.
#
# Any lines starting with a # are optional, but their use is encouraged
# To learn more about a Podspec see https://guides.cocoapods.org/syntax/podspec.html
#
Pod::Spec.new do |s|
s.name = 'DynamicOverlay'
s.version = '1.0.2'
s.summary = 'OverlayContainer is a SwiftUI library which makes it easier to develop overlay based interfaces.'
s.swift_version = "5.0"
s.description = <<-DESC
OverlayContainer is a SwiftUI library written in Swift. It makes it easier to develop overlay based interfaces, such as the one presented in the Apple Maps, Stocks or Shortcuts apps.
DESC
s.homepage = 'https://github.com/fabernovel/DynamicOverlay'
s.license = { :type => 'MIT', :file => 'LICENSE' }
s.author = { 'gaetanzanella' => 'gaetan.zanella@fabernovel.com' }
s.source = { :git => 'https://github.com/fabernovel/DynamicOverlay.git', :tag => s.version.to_s }
s.dependency 'OverlayContainer', '~> 3.5'
s.ios.deployment_target = '13.0'
s.source_files = 'Source/**/*.swift'
end
================================================
FILE: DynamicOverlay_Example/DynamicOverlay_Example/Classes/MapApp.swift
================================================
//
// MapApp.swift
// DynamicOverlay_Example
//
// Created by Gaétan Zanella on 05/03/2019.
// Copyright © 2019 Fabernovel. All rights reserved.
//
import UIKit
import SwiftUI
import DynamicOverlay
@main
struct MapApp: App {
@UIApplicationDelegateAdaptor(UIKitAppDelegate.self)
private var delegate: UIKitAppDelegate
var body: some Scene {
WindowGroup {
MapRootView()
}
}
}
================================================
FILE: DynamicOverlay_Example/DynamicOverlay_Example/Configuration/Info.plist
================================================
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>armv7</string>
</array>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
</dict>
</plist>
================================================
FILE: DynamicOverlay_Example/DynamicOverlay_Example/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json
================================================
{
"images" : [
{
"idiom" : "iphone",
"size" : "20x20",
"scale" : "2x"
},
{
"idiom" : "iphone",
"size" : "20x20",
"scale" : "3x"
},
{
"idiom" : "iphone",
"size" : "29x29",
"scale" : "2x"
},
{
"idiom" : "iphone",
"size" : "29x29",
"scale" : "3x"
},
{
"idiom" : "iphone",
"size" : "40x40",
"scale" : "2x"
},
{
"idiom" : "iphone",
"size" : "40x40",
"scale" : "3x"
},
{
"idiom" : "iphone",
"size" : "60x60",
"scale" : "2x"
},
{
"idiom" : "iphone",
"size" : "60x60",
"scale" : "3x"
},
{
"idiom" : "ipad",
"size" : "20x20",
"scale" : "1x"
},
{
"idiom" : "ipad",
"size" : "20x20",
"scale" : "2x"
},
{
"idiom" : "ipad",
"size" : "29x29",
"scale" : "1x"
},
{
"idiom" : "ipad",
"size" : "29x29",
"scale" : "2x"
},
{
"idiom" : "ipad",
"size" : "40x40",
"scale" : "1x"
},
{
"idiom" : "ipad",
"size" : "40x40",
"scale" : "2x"
},
{
"idiom" : "ipad",
"size" : "76x76",
"scale" : "1x"
},
{
"idiom" : "ipad",
"size" : "76x76",
"scale" : "2x"
},
{
"idiom" : "ipad",
"size" : "83.5x83.5",
"scale" : "2x"
},
{
"idiom" : "ios-marketing",
"size" : "1024x1024",
"scale" : "1x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}
================================================
FILE: DynamicOverlay_Example/DynamicOverlay_Example/Resources/Assets.xcassets/Contents.json
================================================
{
"info" : {
"version" : 1,
"author" : "xcode"
}
}
================================================
FILE: DynamicOverlay_Example/DynamicOverlay_Example/Resources/Base.lproj/LaunchScreen.storyboard
================================================
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="14460.31" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<device id="retina4_7" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14460.20"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="EHf-IW-A2E">
<objects>
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<viewLayoutGuide key="safeArea" id="6Tk-OE-BBY"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="53" y="375"/>
</scene>
</scenes>
</document>
================================================
FILE: DynamicOverlay_Example/DynamicOverlay_Example/UIKit/UIKitAppDelegate.swift
================================================
//
// UIKitAppDelegate.swift
// DynamicOverlay_Example
//
// Created by Gaétan Zanella on 18/04/2021.
// Copyright © 2021 Fabernovel. All rights reserved.
//
import Foundation
import UIKit
class UIKitAppDelegate: NSObject, UIApplicationDelegate {
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
UITableView.appearance().backgroundColor = .systemBackground
return true
}
}
================================================
FILE: DynamicOverlay_Example/DynamicOverlay_Example/View/ActionCell.swift
================================================
//
// ActionCell.swift
// DynamicOverlay_Example
//
// Created by Gaétan Zanella on 19/04/2021.
// Copyright © 2021 Fabernovel. All rights reserved.
//
import Foundation
import SwiftUI
struct ActionCell: View {
var body: some View {
Label("New Guide…", systemImage: "plus")
}
}
================================================
FILE: DynamicOverlay_Example/DynamicOverlay_Example/View/BackdropView.swift
================================================
//
// Backdropview.swift
// DynamicOverlay_Example
//
// Created by Gaétan Zanella on 19/04/2021.
// Copyright © 2021 Fabernovel. All rights reserved.
//
import Foundation
import SwiftUI
struct BackdropView: View {
var body: some View {
Color.black.opacity(0.3)
}
}
================================================
FILE: DynamicOverlay_Example/DynamicOverlay_Example/View/FavoriteCell.swift
================================================
//
// FavoriteCell.swift
// DynamicOverlay_Example
//
// Created by Gaétan Zanella on 19/04/2021.
// Copyright © 2021 Fabernovel. All rights reserved.
//
import Foundation
import SwiftUI
struct FavoriteCell: View {
let imageName: String
let title: String
var body: some View {
VStack {
Circle()
.foregroundColor(Color(.secondarySystemFill))
.frame(width: 70, height: 70)
.overlay(Image(systemName: imageName).font(.title2).foregroundColor(.blue))
Text(title)
}
}
}
================================================
FILE: DynamicOverlay_Example/DynamicOverlay_Example/View/MapRootView.swift
================================================
//
// MapRootView.swift
// DynamicOverlay_Example
//
// Created by Gaétan Zanella on 17/04/2021.
// Copyright © 2021 Fabernovel. All rights reserved.
//
import Foundation
import SwiftUI
import DynamicOverlay
enum Notch: CaseIterable, Equatable {
case min, max
}
struct MapRootView: View {
struct State {
var notch: Notch = .min
var isEditing = false
var progress = 0.0
}
@SwiftUI.State
private var state = State()
// MARK: - View
var body: some View {
background
.dynamicOverlay(overlay)
.dynamicOverlayBehavior(behavior)
.ignoresSafeArea()
}
// MARK: - Private
private var behavior: some DynamicOverlayBehavior {
MagneticNotchOverlayBehavior<Notch> { notch in
switch notch {
case .max:
return .fractional(0.8)
case .min:
return .fractional(0.3)
}
}
.disable(.min, state.isEditing)
.notchChange($state.notch)
.onTranslation { translation in
state.progress = translation.progress
}
}
private var background: some View {
ZStack {
MapView()
BackdropView().opacity(state.progress)
}
.ignoresSafeArea()
}
private var overlay: some View {
OverlayView { event in
switch event {
case .didBeginEditing:
state.isEditing = true
withAnimation { state.notch = .max }
case .didEndEditing:
state.isEditing = false
withAnimation { state.notch = .min }
}
}
.drivingScrollView()
}
}
================================================
FILE: DynamicOverlay_Example/DynamicOverlay_Example/View/MapView.swift
================================================
//
// MapView.swift
// DynamicOverlay_Example
//
// Created by Gaétan Zanella on 17/04/2021.
// Copyright © 2021 Fabernovel. All rights reserved.
//
import Foundation
import MapKit
import SwiftUI
struct MapView: View {
var body: some View {
MapViewAdaptor().ignoresSafeArea()
}
}
private struct MapViewAdaptor: UIViewRepresentable {
func makeUIView(context: Context) -> MKMapView {
MKMapView()
}
func updateUIView(_ uiView: MKMapView, context: Context) {}
}
================================================
FILE: DynamicOverlay_Example/DynamicOverlay_Example/View/OverlayBackgroundView.swift
================================================
//
// OverlayBackgroundView.swift
// DynamicOverlay_Example
//
// Created by Gaétan Zanella on 19/04/2021.
// Copyright © 2021 Fabernovel. All rights reserved.
//
import SwiftUI
struct OverlayBackgroundView: View {
var body: some View {
Color(.systemBackground)
.cornerRadius(8.0, corners: [.topLeft, .topRight])
.shadow(color: Color.black.opacity(0.3), radius: 8.0)
}
}
private extension View {
func cornerRadius(_ radius: CGFloat, corners: UIRectCorner) -> some View {
clipShape(RoundedCorner(radius: radius, corners: corners))
}
}
private struct RoundedCorner: Shape {
var radius: CGFloat = 0.0
var corners: UIRectCorner = .allCorners
func path(in rect: CGRect) -> Path {
Path(
UIBezierPath(
roundedRect: rect,
byRoundingCorners: corners,
cornerRadii: CGSize(width: radius, height: radius)
)
.cgPath
)
}
}
================================================
FILE: DynamicOverlay_Example/DynamicOverlay_Example/View/OverlayView.swift
================================================
//
// OverlayView.swift
// DynamicOverlay_Example
//
// Created by Gaétan Zanella on 17/04/2021.
// Copyright © 2021 Fabernovel. All rights reserved.
//
import Foundation
import SwiftUI
import DynamicOverlay
struct OverlayView: View {
enum Event {
case didBeginEditing
case didEndEditing
}
let eventHandler: (Event) -> Void
// MARK: - View
var body: some View {
VStack(spacing: 0.0) {
header.draggable()
list
}
.background(OverlayBackgroundView())
}
// MARK: - Private
private var list: some View {
List {
Section(header: Text("Favorites")) {
ScrollView(.horizontal) {
HStack {
FavoriteCell(imageName: "house.fill", title: "House")
FavoriteCell(imageName: "briefcase.fill", title: "Work")
FavoriteCell(imageName: "plus", title: "Add")
}
}
}
Section(header: Text("My Guides")) {
ActionCell()
}
}
.listStyle(GroupedListStyle())
}
private var header: some View {
SearchBar { event in
switch event {
case .didBeginEditing:
eventHandler(.didBeginEditing)
case .didCancel:
eventHandler(.didEndEditing)
}
}
}
}
================================================
FILE: DynamicOverlay_Example/DynamicOverlay_Example/View/SearchBar.swift
================================================
//
// SearchBar.swift
// DynamicOverlay_Example
//
// Created by Gaétan Zanella on 18/04/2021.
// Copyright © 2021 Fabernovel. All rights reserved.
//
import Foundation
import SwiftUI
import UIKit
struct SearchBar: View {
enum Event {
case didBeginEditing
case didCancel
}
let eventHandler: (Event) -> Void
var body: some View {
SearchBarAdaptor(
didBeginEditing: { eventHandler(.didBeginEditing) },
didCancel: { eventHandler(.didCancel) }
)
}
}
private class SearchBarCoordinator: NSObject, UISearchBarDelegate {
var didBeginEditing: (() -> Void)?
var didCancel: (() -> Void)?
// MARK: - UISearchBarDelegate
func searchBarTextDidBeginEditing(_ searchBar: UISearchBar) {
didBeginEditing?()
}
func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
searchBar.endEditing(true)
didCancel?()
}
}
private struct SearchBarAdaptor: UIViewRepresentable {
let didBeginEditing: () -> Void
let didCancel: () -> Void
func makeCoordinator() -> SearchBarCoordinator {
let coordinator = SearchBarCoordinator()
coordinator.didBeginEditing = didBeginEditing
coordinator.didCancel = didCancel
return coordinator
}
func makeUIView(context: Context) -> UISearchBar {
let searchBar = UISearchBar()
searchBar.searchBarStyle = .minimal
searchBar.showsCancelButton = true
searchBar.placeholder = "Search for a place or address"
searchBar.delegate = context.coordinator
return searchBar
}
func updateUIView(_ uiView: UISearchBar, context: Context) {}
}
================================================
FILE: DynamicOverlay_Example/DynamicOverlay_Example.xcodeproj/project.pbxproj
================================================
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 52;
objects = {
/* Begin PBXBuildFile section */
E739AD94291D45F00076B2AC /* DynamicOverlay in Frameworks */ = {isa = PBXBuildFile; productRef = E739AD93291D45F00076B2AC /* DynamicOverlay */; };
E73A7CE9262B2A8400959344 /* MapView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E73A7CE8262B2A8400959344 /* MapView.swift */; };
E73A7CEC262B2AA400959344 /* MapRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E73A7CEB262B2AA400959344 /* MapRootView.swift */; };
E750EE4E262B2B4800E79C6B /* OverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E750EE4D262B2B4800E79C6B /* OverlayView.swift */; };
E750EE51262C30F600E79C6B /* SearchBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = E750EE50262C30F600E79C6B /* SearchBar.swift */; };
E750EE63262C463100E79C6B /* MapApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = E742E3C422302B4B002A2BED /* MapApp.swift */; };
E750EE68262C69A900E79C6B /* UIKitAppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E750EE67262C69A900E79C6B /* UIKitAppDelegate.swift */; };
E750EE96262D772700E79C6B /* OverlayBackgroundView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E750EE95262D772700E79C6B /* OverlayBackgroundView.swift */; };
E750EE98262D798F00E79C6B /* ActionCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = E750EE97262D798F00E79C6B /* ActionCell.swift */; };
E750EE9A262D799B00E79C6B /* FavoriteCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = E750EE99262D799B00E79C6B /* FavoriteCell.swift */; };
E750EE9C262D7C9000E79C6B /* BackdropView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E750EE9B262D7C9000E79C6B /* BackdropView.swift */; };
E7691574222EA78B00FDEE7F /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = E7691573222EA78B00FDEE7F /* Assets.xcassets */; };
E7691577222EA78B00FDEE7F /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = E7691575222EA78B00FDEE7F /* LaunchScreen.storyboard */; };
E79705A0292F83100047839F /* OverlayContainerRepresentableAdaptorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7970598292F83100047839F /* OverlayContainerRepresentableAdaptorTests.swift */; };
E79705A1292F83100047839F /* NotchDimensionDynamicOverlayTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7970599292F83100047839F /* NotchDimensionDynamicOverlayTests.swift */; };
E79705A2292F83100047839F /* OverlayNotchIndexMapperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E797059A292F83100047839F /* OverlayNotchIndexMapperTests.swift */; };
E79705A3292F83100047839F /* DrivingScrollViewModifierTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E797059B292F83100047839F /* DrivingScrollViewModifierTests.swift */; };
E79705A4292F83100047839F /* DragHandleViewModifierTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E797059C292F83100047839F /* DragHandleViewModifierTests.swift */; };
E79705A5292F83100047839F /* NotchTranslationDynamicOverlayTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E797059D292F83100047839F /* NotchTranslationDynamicOverlayTests.swift */; };
E79705A6292F83100047839F /* MagneticNotchOverlayBehaviorValueTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E797059E292F83100047839F /* MagneticNotchOverlayBehaviorValueTests.swift */; };
E79705A7292F83100047839F /* NotchBindingDynamicOverlayTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E797059F292F83100047839F /* NotchBindingDynamicOverlayTests.swift */; };
E79705AC292F83190047839F /* ViewInspector.swift in Sources */ = {isa = PBXBuildFile; fileRef = E79705A8292F83190047839F /* ViewInspector.swift */; };
E79705AD292F83190047839F /* ValuePublisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = E79705A9292F83190047839F /* ValuePublisher.swift */; };
E79705AE292F83190047839F /* View+Measure.swift in Sources */ = {isa = PBXBuildFile; fileRef = E79705AA292F83190047839F /* View+Measure.swift */; };
E79705AF292F83190047839F /* ViewRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = E79705AB292F83190047839F /* ViewRenderer.swift */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
E7970593292F817F0047839F /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = E7691561222EA78A00FDEE7F /* Project object */;
proxyType = 1;
remoteGlobalIDString = E7691568222EA78A00FDEE7F;
remoteInfo = DynamicOverlay_Example;
};
/* End PBXContainerItemProxy section */
/* Begin PBXFileReference section */
E739AD92291D45B10076B2AC /* DynamicOverlay */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = DynamicOverlay; path = ..; sourceTree = "<group>"; };
E73A7CE8262B2A8400959344 /* MapView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapView.swift; sourceTree = "<group>"; };
E73A7CEB262B2AA400959344 /* MapRootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapRootView.swift; sourceTree = "<group>"; };
E741EE722576B10D0073FF6B /* DynamicOverlay.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = DynamicOverlay.framework; sourceTree = BUILT_PRODUCTS_DIR; };
E742E3C422302B4B002A2BED /* MapApp.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = MapApp.swift; path = Classes/MapApp.swift; sourceTree = "<group>"; };
E750EE4D262B2B4800E79C6B /* OverlayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverlayView.swift; sourceTree = "<group>"; };
E750EE50262C30F600E79C6B /* SearchBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchBar.swift; sourceTree = "<group>"; };
E750EE67262C69A900E79C6B /* UIKitAppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIKitAppDelegate.swift; sourceTree = "<group>"; };
E750EE95262D772700E79C6B /* OverlayBackgroundView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverlayBackgroundView.swift; sourceTree = "<group>"; };
E750EE97262D798F00E79C6B /* ActionCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionCell.swift; sourceTree = "<group>"; };
E750EE99262D799B00E79C6B /* FavoriteCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoriteCell.swift; sourceTree = "<group>"; };
E750EE9B262D7C9000E79C6B /* BackdropView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackdropView.swift; sourceTree = "<group>"; };
E7691569222EA78A00FDEE7F /* DynamicOverlay_Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = DynamicOverlay_Example.app; sourceTree = BUILT_PRODUCTS_DIR; };
E7691573222EA78B00FDEE7F /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
E7691576222EA78B00FDEE7F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
E7691578222EA78B00FDEE7F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
E797058F292F817F0047839F /* DynamicOverlay_ExampleTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = DynamicOverlay_ExampleTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
E7970598292F83100047839F /* OverlayContainerRepresentableAdaptorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = OverlayContainerRepresentableAdaptorTests.swift; path = ../../Tests/DynamicOverlayTests/OverlayContainerRepresentableAdaptorTests.swift; sourceTree = "<group>"; };
E7970599292F83100047839F /* NotchDimensionDynamicOverlayTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = NotchDimensionDynamicOverlayTests.swift; path = ../../Tests/DynamicOverlayTests/NotchDimensionDynamicOverlayTests.swift; sourceTree = "<group>"; };
E797059A292F83100047839F /* OverlayNotchIndexMapperTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = OverlayNotchIndexMapperTests.swift; path = ../../Tests/DynamicOverlayTests/OverlayNotchIndexMapperTests.swift; sourceTree = "<group>"; };
E797059B292F83100047839F /* DrivingScrollViewModifierTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = DrivingScrollViewModifierTests.swift; path = ../../Tests/DynamicOverlayTests/DrivingScrollViewModifierTests.swift; sourceTree = "<group>"; };
E797059C292F83100047839F /* DragHandleViewModifierTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = DragHandleViewModifierTests.swift; path = ../../Tests/DynamicOverlayTests/DragHandleViewModifierTests.swift; sourceTree = "<group>"; };
E797059D292F83100047839F /* NotchTranslationDynamicOverlayTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = NotchTranslationDynamicOverlayTests.swift; path = ../../Tests/DynamicOverlayTests/NotchTranslationDynamicOverlayTests.swift; sourceTree = "<group>"; };
E797059E292F83100047839F /* MagneticNotchOverlayBehaviorValueTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = MagneticNotchOverlayBehaviorValueTests.swift; path = ../../Tests/DynamicOverlayTests/MagneticNotchOverlayBehaviorValueTests.swift; sourceTree = "<group>"; };
E797059F292F83100047839F /* NotchBindingDynamicOverlayTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = NotchBindingDynamicOverlayTests.swift; path = ../../Tests/DynamicOverlayTests/NotchBindingDynamicOverlayTests.swift; sourceTree = "<group>"; };
E79705A8292F83190047839F /* ViewInspector.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ViewInspector.swift; path = ../../Tests/DynamicOverlayTests/Utils/ViewInspector.swift; sourceTree = "<group>"; };
E79705A9292F83190047839F /* ValuePublisher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ValuePublisher.swift; path = ../../Tests/DynamicOverlayTests/Utils/ValuePublisher.swift; sourceTree = "<group>"; };
E79705AA292F83190047839F /* View+Measure.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "View+Measure.swift"; path = "../../Tests/DynamicOverlayTests/Utils/View+Measure.swift"; sourceTree = "<group>"; };
E79705AB292F83190047839F /* ViewRenderer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ViewRenderer.swift; path = ../../Tests/DynamicOverlayTests/Utils/ViewRenderer.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
E7691566222EA78A00FDEE7F /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
E739AD94291D45F00076B2AC /* DynamicOverlay in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
E797058C292F817F0047839F /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
E739AD91291D45B10076B2AC /* Packages */ = {
isa = PBXGroup;
children = (
E739AD92291D45B10076B2AC /* DynamicOverlay */,
);
name = Packages;
sourceTree = "<group>";
};
E73A7CEA262B2A8B00959344 /* View */ = {
isa = PBXGroup;
children = (
E73A7CEB262B2AA400959344 /* MapRootView.swift */,
E73A7CE8262B2A8400959344 /* MapView.swift */,
E750EE4D262B2B4800E79C6B /* OverlayView.swift */,
E750EE50262C30F600E79C6B /* SearchBar.swift */,
E750EE95262D772700E79C6B /* OverlayBackgroundView.swift */,
E750EE97262D798F00E79C6B /* ActionCell.swift */,
E750EE99262D799B00E79C6B /* FavoriteCell.swift */,
E750EE9B262D7C9000E79C6B /* BackdropView.swift */,
);
path = View;
sourceTree = "<group>";
};
E741EE712576B10D0073FF6B /* Frameworks */ = {
isa = PBXGroup;
children = (
E741EE722576B10D0073FF6B /* DynamicOverlay.framework */,
);
name = Frameworks;
sourceTree = "<group>";
};
E750EE66262C699C00E79C6B /* UIKit */ = {
isa = PBXGroup;
children = (
E750EE67262C69A900E79C6B /* UIKitAppDelegate.swift */,
);
path = UIKit;
sourceTree = "<group>";
};
E7691560222EA78A00FDEE7F = {
isa = PBXGroup;
children = (
E739AD91291D45B10076B2AC /* Packages */,
E769156B222EA78A00FDEE7F /* DynamicOverlay_Example */,
E7970590292F817F0047839F /* DynamicOverlay_ExampleTests */,
E769156A222EA78A00FDEE7F /* Products */,
E741EE712576B10D0073FF6B /* Frameworks */,
);
sourceTree = "<group>";
};
E769156A222EA78A00FDEE7F /* Products */ = {
isa = PBXGroup;
children = (
E7691569222EA78A00FDEE7F /* DynamicOverlay_Example.app */,
E797058F292F817F0047839F /* DynamicOverlay_ExampleTests.xctest */,
);
name = Products;
sourceTree = "<group>";
};
E769156B222EA78A00FDEE7F /* DynamicOverlay_Example */ = {
isa = PBXGroup;
children = (
E742E3C422302B4B002A2BED /* MapApp.swift */,
E750EE66262C699C00E79C6B /* UIKit */,
E73A7CEA262B2A8B00959344 /* View */,
E7691582222EA7C100FDEE7F /* Resources */,
E7691581222EA7B400FDEE7F /* Configuration */,
);
path = DynamicOverlay_Example;
sourceTree = "<group>";
};
E7691581222EA7B400FDEE7F /* Configuration */ = {
isa = PBXGroup;
children = (
E7691578222EA78B00FDEE7F /* Info.plist */,
);
path = Configuration;
sourceTree = "<group>";
};
E7691582222EA7C100FDEE7F /* Resources */ = {
isa = PBXGroup;
children = (
E7691575222EA78B00FDEE7F /* LaunchScreen.storyboard */,
E7691573222EA78B00FDEE7F /* Assets.xcassets */,
);
path = Resources;
sourceTree = "<group>";
};
E7970590292F817F0047839F /* DynamicOverlay_ExampleTests */ = {
isa = PBXGroup;
children = (
E797059C292F83100047839F /* DragHandleViewModifierTests.swift */,
E797059B292F83100047839F /* DrivingScrollViewModifierTests.swift */,
E797059E292F83100047839F /* MagneticNotchOverlayBehaviorValueTests.swift */,
E797059F292F83100047839F /* NotchBindingDynamicOverlayTests.swift */,
E7970599292F83100047839F /* NotchDimensionDynamicOverlayTests.swift */,
E797059D292F83100047839F /* NotchTranslationDynamicOverlayTests.swift */,
E7970598292F83100047839F /* OverlayContainerRepresentableAdaptorTests.swift */,
E797059A292F83100047839F /* OverlayNotchIndexMapperTests.swift */,
E79705A9292F83190047839F /* ValuePublisher.swift */,
E79705AA292F83190047839F /* View+Measure.swift */,
E79705A8292F83190047839F /* ViewInspector.swift */,
E79705AB292F83190047839F /* ViewRenderer.swift */,
);
path = DynamicOverlay_ExampleTests;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
E7691568222EA78A00FDEE7F /* DynamicOverlay_Example */ = {
isa = PBXNativeTarget;
buildConfigurationList = E769157B222EA78B00FDEE7F /* Build configuration list for PBXNativeTarget "DynamicOverlay_Example" */;
buildPhases = (
E7691565222EA78A00FDEE7F /* Sources */,
E7691566222EA78A00FDEE7F /* Frameworks */,
E7691567222EA78A00FDEE7F /* Resources */,
);
buildRules = (
);
dependencies = (
);
name = DynamicOverlay_Example;
packageProductDependencies = (
E739AD93291D45F00076B2AC /* DynamicOverlay */,
);
productName = DynamicOverlay_Example;
productReference = E7691569222EA78A00FDEE7F /* DynamicOverlay_Example.app */;
productType = "com.apple.product-type.application";
};
E797058E292F817F0047839F /* DynamicOverlay_ExampleTests */ = {
isa = PBXNativeTarget;
buildConfigurationList = E7970595292F817F0047839F /* Build configuration list for PBXNativeTarget "DynamicOverlay_ExampleTests" */;
buildPhases = (
E797058B292F817F0047839F /* Sources */,
E797058C292F817F0047839F /* Frameworks */,
E797058D292F817F0047839F /* Resources */,
);
buildRules = (
);
dependencies = (
E7970594292F817F0047839F /* PBXTargetDependency */,
);
name = DynamicOverlay_ExampleTests;
productName = DynamicOverlay_ExampleTests;
productReference = E797058F292F817F0047839F /* DynamicOverlay_ExampleTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
E7691561222EA78A00FDEE7F /* Project object */ = {
isa = PBXProject;
attributes = {
LastSwiftUpdateCheck = 1410;
LastUpgradeCheck = 1220;
ORGANIZATIONNAME = Fabernovel;
TargetAttributes = {
E7691568222EA78A00FDEE7F = {
CreatedOnToolsVersion = 10.1;
};
E797058E292F817F0047839F = {
CreatedOnToolsVersion = 14.1;
LastSwiftMigration = 1410;
TestTargetID = E7691568222EA78A00FDEE7F;
};
};
};
buildConfigurationList = E7691564222EA78A00FDEE7F /* Build configuration list for PBXProject "DynamicOverlay_Example" */;
compatibilityVersion = "Xcode 9.3";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = E7691560222EA78A00FDEE7F;
productRefGroup = E769156A222EA78A00FDEE7F /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
E7691568222EA78A00FDEE7F /* DynamicOverlay_Example */,
E797058E292F817F0047839F /* DynamicOverlay_ExampleTests */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
E7691567222EA78A00FDEE7F /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
E7691577222EA78B00FDEE7F /* LaunchScreen.storyboard in Resources */,
E7691574222EA78B00FDEE7F /* Assets.xcassets in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
E797058D292F817F0047839F /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
E7691565222EA78A00FDEE7F /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
E750EE68262C69A900E79C6B /* UIKitAppDelegate.swift in Sources */,
E750EE98262D798F00E79C6B /* ActionCell.swift in Sources */,
E73A7CEC262B2AA400959344 /* MapRootView.swift in Sources */,
E73A7CE9262B2A8400959344 /* MapView.swift in Sources */,
E750EE9A262D799B00E79C6B /* FavoriteCell.swift in Sources */,
E750EE4E262B2B4800E79C6B /* OverlayView.swift in Sources */,
E750EE63262C463100E79C6B /* MapApp.swift in Sources */,
E750EE96262D772700E79C6B /* OverlayBackgroundView.swift in Sources */,
E750EE9C262D7C9000E79C6B /* BackdropView.swift in Sources */,
E750EE51262C30F600E79C6B /* SearchBar.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
E797058B292F817F0047839F /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
E79705A2292F83100047839F /* OverlayNotchIndexMapperTests.swift in Sources */,
E79705A7292F83100047839F /* NotchBindingDynamicOverlayTests.swift in Sources */,
E79705AD292F83190047839F /* ValuePublisher.swift in Sources */,
E79705A1292F83100047839F /* NotchDimensionDynamicOverlayTests.swift in Sources */,
E79705A4292F83100047839F /* DragHandleViewModifierTests.swift in Sources */,
E79705A3292F83100047839F /* DrivingScrollViewModifierTests.swift in Sources */,
E79705A0292F83100047839F /* OverlayContainerRepresentableAdaptorTests.swift in Sources */,
E79705AF292F83190047839F /* ViewRenderer.swift in Sources */,
E79705A5292F83100047839F /* NotchTranslationDynamicOverlayTests.swift in Sources */,
E79705AC292F83190047839F /* ViewInspector.swift in Sources */,
E79705A6292F83100047839F /* MagneticNotchOverlayBehaviorValueTests.swift in Sources */,
E79705AE292F83190047839F /* View+Measure.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
E7970594292F817F0047839F /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = E7691568222EA78A00FDEE7F /* DynamicOverlay_Example */;
targetProxy = E7970593292F817F0047839F /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */
E7691575222EA78B00FDEE7F /* LaunchScreen.storyboard */ = {
isa = PBXVariantGroup;
children = (
E7691576222EA78B00FDEE7F /* Base */,
);
name = LaunchScreen.storyboard;
sourceTree = "<group>";
};
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
E7691579222EA78B00FDEE7F /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_IDENTITY = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
};
name = Debug;
};
E769157A222EA78B00FDEE7F /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_IDENTITY = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
SDKROOT = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
VALIDATE_PRODUCT = YES;
};
name = Release;
};
E769157C222EA78B00FDEE7F /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Automatic;
INFOPLIST_FILE = DynamicOverlay_Example/Configuration/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 15;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = "com.fabernovel.DynamicOverlay-Example";
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 4.2;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
E769157D222EA78B00FDEE7F /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Automatic;
INFOPLIST_FILE = DynamicOverlay_Example/Configuration/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 15;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = "com.fabernovel.DynamicOverlay-Example";
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 4.2;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Release;
};
E7970596292F817F0047839F /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = C7G63Q6LZ9;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = "com.gaetanzanella.DynamicOverlay-ExampleTests";
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = NO;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/DynamicOverlay_Example.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/DynamicOverlay_Example";
};
name = Debug;
};
E7970597292F817F0047839F /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = C7G63Q6LZ9;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = "com.gaetanzanella.DynamicOverlay-ExampleTests";
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = NO;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/DynamicOverlay_Example.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/DynamicOverlay_Example";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
E7691564222EA78A00FDEE7F /* Build configuration list for PBXProject "DynamicOverlay_Example" */ = {
isa = XCConfigurationList;
buildConfigurations = (
E7691579222EA78B00FDEE7F /* Debug */,
E769157A222EA78B00FDEE7F /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
E769157B222EA78B00FDEE7F /* Build configuration list for PBXNativeTarget "DynamicOverlay_Example" */ = {
isa = XCConfigurationList;
buildConfigurations = (
E769157C222EA78B00FDEE7F /* Debug */,
E769157D222EA78B00FDEE7F /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
E7970595292F817F0047839F /* Build configuration list for PBXNativeTarget "DynamicOverlay_ExampleTests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
E7970596292F817F0047839F /* Debug */,
E7970597292F817F0047839F /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
/* Begin XCSwiftPackageProductDependency section */
E739AD93291D45F00076B2AC /* DynamicOverlay */ = {
isa = XCSwiftPackageProductDependency;
productName = DynamicOverlay;
};
/* End XCSwiftPackageProductDependency section */
};
rootObject = E7691561222EA78A00FDEE7F /* Project object */;
}
================================================
FILE: DynamicOverlay_Example/DynamicOverlay_Example.xcodeproj/project.xcworkspace/contents.xcworkspacedata
================================================
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>
================================================
FILE: DynamicOverlay_Example/DynamicOverlay_Example.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
================================================
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>
================================================
FILE: DynamicOverlay_Example/DynamicOverlay_Example.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
================================================
{
"object": {
"pins": [
{
"package": "OverlayContainer",
"repositoryURL": "https://github.com/applidium/OverlayContainer.git",
"state": {
"branch": null,
"revision": "f1c8fc38bb1ad9a810397f1d06f7026a35e0760c",
"version": "3.5.2"
}
}
]
},
"version": 1
}
================================================
FILE: DynamicOverlay_Example/DynamicOverlay_Example.xcodeproj/xcshareddata/xcschemes/DynamicOverlay_Example.xcscheme
================================================
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1400"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "E7691568222EA78A00FDEE7F"
BuildableName = "DynamicOverlay_Example.app"
BlueprintName = "DynamicOverlay_Example"
ReferencedContainer = "container:DynamicOverlay_Example.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "E797058E292F817F0047839F"
BuildableName = "DynamicOverlay_ExampleTests.xctest"
BlueprintName = "DynamicOverlay_ExampleTests"
ReferencedContainer = "container:DynamicOverlay_Example.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "E7691568222EA78A00FDEE7F"
BuildableName = "DynamicOverlay_Example.app"
BlueprintName = "DynamicOverlay_Example"
ReferencedContainer = "container:DynamicOverlay_Example.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "E7691568222EA78A00FDEE7F"
BuildableName = "DynamicOverlay_Example.app"
BlueprintName = "DynamicOverlay_Example"
ReferencedContainer = "container:DynamicOverlay_Example.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
================================================
FILE: Gemfile
================================================
source 'https://rubygems.org'
gem "cocoapods", "~> 1.11"
gem "fastlane", "~> 2.1"
plugins_path = File.join(File.dirname(__FILE__), 'fastlane', 'Pluginfile')
eval_gemfile(plugins_path) if File.exist?(plugins_path)
================================================
FILE: LICENSE
================================================
Copyright (c) 2020 Fabernovel
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
================================================
FILE: Package.resolved
================================================
{
"object": {
"pins": [
{
"package": "OverlayContainer",
"repositoryURL": "https://github.com/applidium/OverlayContainer.git",
"state": {
"branch": null,
"revision": "f1c8fc38bb1ad9a810397f1d06f7026a35e0760c",
"version": "3.5.2"
}
}
]
},
"version": 1
}
================================================
FILE: Package.swift
================================================
// swift-tools-version:5.1
import PackageDescription
let package = Package(
name: "DynamicOverlay",
platforms: [
.iOS(.v13)
],
products: [
.library(
name: "DynamicOverlay",
targets: ["DynamicOverlay"]
),
],
dependencies: [
.package(url: "https://github.com/applidium/OverlayContainer.git", from: "3.5.2")
],
targets: [
.target(
name: "DynamicOverlay",
dependencies: ["OverlayContainer"],
path: "Source"
),
.testTarget(
name: "DynamicOverlayTests",
dependencies: [
"DynamicOverlay",
]
)
],
swiftLanguageVersions: [.v5]
)
================================================
FILE: Package.xcconfig
================================================
IPHONEOS_DEPLOYMENT_TARGET = 10.0
SDKROOT =
SUPPORTED_PLATFORMS = iphoneos iphonesimulator
TARGETED_DEVICE_FAMILY = 1,2
VALID_ARCHS[sdk=iphoneos*] = arm64 armv7 armv7s
VALID_ARCHS[sdk=iphonesimulator*] = i386 x86_64
CODE_SIGN_IDENTITY =
CODE_SIGN_STYLE = Manual
INSTALL_PATH = $(LOCAL_LIBRARY_DIR)/Frameworks
SKIP_INSTALL = YES
DYLIB_COMPATIBILITY_VERSION = 1
DYLIB_CURRENT_VERSION = 1
DYLIB_INSTALL_NAME_BASE = @rpath
LD_RUNPATH_SEARCH_PATHS = $(inherited) @executable_path/../Frameworks @loader_path/Frameworks @loader_path/../Frameworks
DEFINES_MODULE = NO
================================================
FILE: README.md
================================================
# DynamicOverlay
<H4 align="center">
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.
</H4>
<p align="center">
<a href="https://developer.apple.com/"><img alt="Platform" src="https://img.shields.io/badge/platform-iOS-green.svg"/></a>
<a href="https://developer.apple.com/swift"><img alt="Swift5" src="https://img.shields.io/badge/language-Swift%205.0-orange.svg"/></a>
<a href="https://cocoapods.org/pods/DynamicOverlay"><img alt="CocoaPods" src="https://img.shields.io/cocoapods/v/DynamicOverlay.svg?style=flat"/></a>
<a href="https://github.com/Carthage/Carthage"><img alt="Carthage" src="https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat"/></a>
<a href="https://github.com/fabernovel/DynamicOverlay/actions"><img alt="Build Status" src="https://github.com/fabernovel/DynamicOverlay/workflows/CI/badge.svg?branch=main"/></a>
<a href="https://github.com/fabernovel/DynamicOverlay/blob/main/LICENSE"><img alt="License" src="https://img.shields.io/cocoapods/l/DynamicOverlay.svg?style=flat"/></a>
</p>
---
- [Requirements](#requirements)
- [Getting started](#getting-started)
- [Examples](#examples)
- [Magnetic notch overlay](#magnetic-notch-overlay)
- [Specifying the notches](#specifying-the-notches)
- [Drag gesture support](#drag-gesture-support)
- [Scroll view support](#scroll-view-support)
- [Responding to overlay update](#responding-to-overlay-update)
- [Moving the overlay](#moving-the-overlay)
- [Disabling notches](#disabling-notches)
- [Installation](#installation)
- [CocoaPods](#cocoapods)
- [Carthage](#carthage)
- [Swift Package Manager](#swift-package-manager)
- [Under the hood](#under-the-hood)
- [Release](#release)
- [Author](#author)
- [License](#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](https://developer.apple.com/documentation/swiftui/view/overlay(_:alignment:)) using a view modifier:
```swift
Color.blue.dynamicOverlay(Color.red)
```
Its behavior is defined by the `DynamicOverlayBehavior` associated to it if any.
```swift
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
- [Map App](https://github.com/faberNovel/DynamicOverlay/blob/main/DynamicOverlay_Example/DynamicOverlay_Example/View/MapRootView.swift)
| Min | Max |
| ------------------- | ------------------ |
| <img src="https://github.com/faberNovel/DynamicOverlay/blob/main/Screenshots/min.png" width=200 /> | <img src="https://github.com/faberNovel/DynamicOverlay/blob/main/Screenshots/max.png" width=200 /> |
## 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:
```swift
enum Notch: CaseIterable, Equatable {
case min, max
}
```
You specify the dimensions of each notch when you create a `MagneticNotchOverlayBehavior` instance:
```swift
@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:
```swift
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:
```swift
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:
```swift
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()`.
```swift
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:
```swift
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:
```swift
@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:
```swift
@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.
```swift
@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.
```swift
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:
```swift
@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](https://github.com/applidium/OverlayContainer). If you need more control, consider using it or open an issue.
## Installation
`DynamicOverlay` is available through [CocoaPods](https://cocoapods.org). To install it, simply add the following line to your Podfile:
### Cocoapods
```ruby
pod 'DynamicOverlay'
```
### Carthage
Add the following to your Cartfile:
```ruby
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:
```swift
.package(url: "https://github.com/fabernovel/DynamicOverlay.git")
```
## Release
- Create a release branch for the new version (release/#version#)
- Update the [CHANGELOG.md](https://github.com/faberNovel/DynamicOverlay/blob/main/CHANGELOG.md) (Be sure to spell your release version correctly)
- Push your release branch
- Run the [release workflow](https://github.com/faberNovel/DynamicOverlay/actions/workflows/release.yml) from your release branch
## Author
[@gaetanzanella](https://twitter.com/gaetanzanella), gaetan.zanella@fabernovel.com
## License
`DynamicOverlay` is available under the MIT license. See the LICENSE file for more info.
================================================
FILE: Source/DynamicOverlay.h
================================================
//
// DynamicOverlay.h
// DynamicOverlay
//
// Created by Gaétan Zanella on 05/03/2019.
// Copyright © 2019 Fabernovel. All rights reserved.
//
#import <UIKit/UIKit.h>
//! Project version number for DynamicOverlay.
FOUNDATION_EXPORT double DynamicOverlayVersionNumber;
//! Project version string for DynamicOverlay.
FOUNDATION_EXPORT const unsigned char DynamicOverlayVersionString[];
// In this header, you should import all the public headers of your framework using statements like #import <DynamicOverlay/PublicHeader.h>
================================================
FILE: Source/Info.plist
================================================
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0.0</string>
<key>CFBundleVersion</key>
<string>13</string>
</dict>
</plist>
================================================
FILE: Source/Internal/DynamicOverlayBehaviorValue.swift
================================================
//
// EmptyFile.swift
// DynamicOverlay
//
// Created by Gaétan Zanella on 05/03/2019.
// Copyright © 2019 Fabernovel. All rights reserved.
//
import SwiftUI
struct OverlayTranslation {
let height: CGFloat
let transaction: Transaction
let isDragging: Bool
let translationProgress: CGFloat
let containerFrame: CGRect
let velocity: CGPoint
let heightForNotchIndex: (Int) -> CGFloat
}
struct DynamicOverlayBehaviorValue {
let notchDimensions: [Int: NotchDimension]?
let block: ((OverlayTranslation) -> Void)?
let binding: Binding<Int>?
let disabledNotchIndexes: Set<Int>
init(notchDimensions: [Int: NotchDimension]? = nil,
block: ((OverlayTranslation) -> Void)? = nil,
binding: Binding<Int>? = nil,
disabledNotchIndexes: Set<Int> = []) {
self.notchDimensions = notchDimensions
self.block = block
self.binding = binding
self.disabledNotchIndexes = disabledNotchIndexes
}
}
extension DynamicOverlayBehaviorValue {
static var `default`: DynamicOverlayBehaviorValue {
DynamicOverlayBehaviorValue(
notchDimensions: [
0 : .fractional(0.3),
1 : .fractional(0.5),
2 : .fractional(0.7)
]
)
}
}
struct DynamicOverlayBehaviorKey: EnvironmentKey {
static var defaultValue: DynamicOverlayBehaviorValue = .default
}
extension EnvironmentValues {
var behaviorValue: DynamicOverlayBehaviorValue {
set {
self[DynamicOverlayBehaviorKey.self] = newValue
}
get {
self[DynamicOverlayBehaviorKey.self]
}
}
}
================================================
FILE: Source/Internal/DynamicOverlayNotchTransition+DynamicOverlayBehavior.swift
================================================
//
// MagneticNotchOverlayBehavior+DynamicOverlayBehavior.swift
// DynamicOverlay
//
// Created by Gaétan Zanella on 02/12/2020.
// Copyright © 2020 Fabernovel. All rights reserved.
//
import SwiftUI
extension NotchDimension {
enum ValueType: Hashable {
case absolute
case fractional
}
}
extension MagneticNotchOverlayBehavior {
// MARK: - DynamicOverlayBehavior
func buildValue() -> DynamicOverlayBehaviorValue {
DynamicOverlayBehaviorValue(
notchDimensions: Dictionary(
uniqueKeysWithValues: Notch.allCases.enumerated().map { i, notch in (i, value.dimensions(notch)) }
),
block: value.translationBlocks.isEmpty ? nil : { translation in
value.translationBlocks.forEach {
$0(
Translation(
height: translation.height,
transaction: translation.transaction,
progress: Double(min(max(translation.translationProgress, 0), 1)),
containerSize: translation.containerFrame.size,
heightForNotch: { notch in
translation.heightForNotchIndex(Notch.index(of: notch))
}
)
)
}
},
binding: value.binding?.indexBinding(),
disabledNotchIndexes: Set(value.disabledNotches.map { Notch.index(of: $0) })
)
}
}
================================================
FILE: Source/Internal/Handle/ActivatedOverlayArea.swift
================================================
//
// ActivatedOverlayArea.swift
// DynamicOverlay
//
// Created by Gaétan Zanella on 04/12/2020.
// Copyright © 2020 Fabernovel. All rights reserved.
//
import SwiftUI
struct ActivatedOverlayArea: Equatable {
private struct Spot: Equatable {
let frame: CGRect
}
private var spots: [Spot]
mutating func merge(_ handle: ActivatedOverlayArea) {
spots += handle.spots
}
var isEmpty: Bool {
spots.isEmpty
}
func contains(_ rect: CGRect) -> Bool {
spots.contains { $0.frame == rect }
}
func contains(_ point: CGPoint) -> Bool {
spots.contains { $0.frame.contains(point) }
}
func intersects(_ rect: CGRect) -> Bool {
spots.contains {
// (gz) 2022-01-29 `SwiftUI` rounds the `UIKit` view frames.
// A 0.25pt-width `SwiftUI` view can contain a 0.5pt-width `UIView`.
rect.intersection($0.frame).width >= 0.5
&& $0.frame != .zero
}
}
}
extension ActivatedOverlayArea {
static func active(_ frame: CGRect) -> ActivatedOverlayArea {
ActivatedOverlayArea(spots: [Spot(frame: frame)])
}
static func inactive() -> ActivatedOverlayArea {
ActivatedOverlayArea(spots: [Spot(frame: .zero)])
}
static var `default`: ActivatedOverlayArea {
.empty
}
static var empty: ActivatedOverlayArea {
ActivatedOverlayArea(spots: [])
}
}
================================================
FILE: Source/Internal/Handle/ActiveOverlayAreaViewModifier.swift
================================================
//
// ActiveOverlayAreaViewModifier.swift
// DynamicOverlay
//
// Created by Gaétan Zanella on 28/12/2020.
// Copyright © 2020 Fabernovel. All rights reserved.
//
import SwiftUI
struct ActiveOverlayAreaViewModifier<Key: PreferenceKey>: ViewModifier where Key.Value == ActivatedOverlayArea {
let key: Key.Type
let isActive: Bool
func body(content: Content) -> some View {
content.background(
GeometryReader { proxy in
Spacer().preference(
key: key,
value: isActive ? .active(proxy.frame(in: .overlay)) : .inactive()
)
}
)
}
}
================================================
FILE: Source/Internal/Handle/Drag/DynamicOverlayDragArea.swift
================================================
//
// DynamicOverlayDragArea.swift
// DynamicOverlay
//
// Created by Gaétan Zanella on 29/01/2022.
// Copyright © 2022 Fabernovel. All rights reserved.
//
import Foundation
import SwiftUI
struct DynamicOverlayDragArea: Equatable {
private let area: ActivatedOverlayArea
init(area: ActivatedOverlayArea) {
self.area = area
}
static var `default`: DynamicOverlayDragArea {
DynamicOverlayDragArea(area: .default)
}
var isEmpty: Bool {
area.isEmpty
}
func contains(_ rect: CGRect) -> Bool {
area.contains(rect)
}
func contains(_ point: CGPoint) -> Bool {
return area.contains(point)
}
}
struct DynamicOverlayDragAreaPreferenceKey: PreferenceKey {
typealias Value = ActivatedOverlayArea
static var defaultValue: ActivatedOverlayArea = .default
static func reduce(value: inout Value, nextValue: () -> Value) {
value.merge(nextValue())
}
}
================================================
FILE: Source/Internal/Handle/Drag/OnDragAreaChangeViewModifier.swift
================================================
//
// OnDragAreaChangeViewModifier.swift
// DynamicOverlayTests
//
// Created by Gaétan Zanella on 16/04/2021.
// Copyright © 2021 Fabernovel. All rights reserved.
//
import Foundation
import SwiftUI
private struct OnDragAreaChangeViewModifier: ViewModifier {
let handler: (DynamicOverlayDragArea) -> Void
func body(content: Content) -> some View {
content.onPreferenceChange(DynamicOverlayDragAreaPreferenceKey.self) { area in
handler(DynamicOverlayDragArea(area: area))
}
}
}
extension View {
func onDragAreaChange(handler: @escaping (DynamicOverlayDragArea) -> Void) -> some View {
modifier(OnDragAreaChangeViewModifier(handler: handler))
}
}
================================================
FILE: Source/Internal/Handle/DrivingScrollView/DynamicOverlayScrollViewProxy.swift
================================================
//
// DynamicOverlayScrollViewProxyPreferenceKey.swift
// DynamicOverlay
//
// Created by Gaétan Zanella on 11/01/2022.
// Copyright © 2022 Fabernovel. All rights reserved.
//
import SwiftUI
struct DynamicOverlayScrollViewProxy: Equatable {
private let area: ActivatedOverlayArea
init(area: ActivatedOverlayArea) {
self.area = area
}
static var `default`: DynamicOverlayScrollViewProxy {
DynamicOverlayScrollViewProxy(area: .default)
}
func findScrollView(in space: UIView) -> UIScrollView? {
space.findScrollView(in: area, coordinate: space)
}
}
struct DynamicOverlayScrollViewProxyPreferenceKey: PreferenceKey {
typealias Value = ActivatedOverlayArea
static var defaultValue: ActivatedOverlayArea = .default
static func reduce(value: inout Value, nextValue: () -> Value) {
value.merge(nextValue())
}
}
private extension UIView {
func findScrollView(in area: ActivatedOverlayArea,
coordinate: UICoordinateSpace) -> UIScrollView? {
let frame = coordinate.convert(bounds, from: self)
guard area.intersects(frame) else { return nil }
if let result = self as? UIScrollView {
return result
}
for subview in subviews {
if let result = subview.findScrollView(in: area, coordinate: coordinate) {
return result
}
}
return nil
}
}
================================================
FILE: Source/Internal/Handle/DrivingScrollView/OnDrivingScrollViewChangeViewModifier.swift
================================================
//
// OnDrivingScrollViewChangeViewModifier.swift
// DynamicOverlayTests
//
// Created by Gaétan Zanella on 16/04/2021.
// Copyright © 2021 Fabernovel. All rights reserved.
//
import SwiftUI
private struct OnDrivingScrollViewChangeViewModifier: ViewModifier {
let handler: (DynamicOverlayScrollViewProxy) -> Void
func body(content: Content) -> some View {
content.onPreferenceChange(DynamicOverlayScrollViewProxyPreferenceKey.self, perform: { value in
handler(DynamicOverlayScrollViewProxy(area: value))
})
}
}
extension View {
func onDrivingScrollViewChange(handler: @escaping (DynamicOverlayScrollViewProxy) -> Void) -> some View {
modifier(OnDrivingScrollViewChangeViewModifier(handler: handler))
}
}
================================================
FILE: Source/Internal/Handle/OverlayContainerCoordinateSpace.swift
================================================
//
// OverlayContainer+CoordinateSpace.swift
// DynamicOverlay
//
// Created by Gaétan Zanella on 28/12/2020.
// Copyright © 2020 Fabernovel. All rights reserved.
//
import SwiftUI
extension CoordinateSpace {
static var overlay: CoordinateSpace {
.named("Overlay")
}
}
extension View {
func overlayCoordinateSpace() -> some View {
modifier(OverlayCoordinateSpaceViewModifier())
}
}
private struct OverlayCoordinateSpaceViewModifier: ViewModifier {
func body(content: Content) -> some View {
content.coordinateSpace(name: "Overlay")
}
}
================================================
FILE: Source/Internal/MagneticNotchOverlayBehaviorValue.swift
================================================
//
// MagneticNotchOverlayBehaviorValue.swift
// DynamicOverlay
//
// Created by Gaétan Zanella on 02/12/2020.
// Copyright © 2020 Fabernovel. All rights reserved.
//
import SwiftUI
extension MagneticNotchOverlayBehavior {
struct Value {
let dimensions: (Notch) -> NotchDimension
let translationBlocks: [(Translation) -> Void]
let binding: Binding<Notch>?
let disabledNotches: [Notch]
init(dimensions: @escaping (Notch) -> NotchDimension,
translationBlocks: [(Translation) -> Void],
binding: Binding<Notch>?,
disabledNotches: [Notch]) {
self.dimensions = dimensions
self.translationBlocks = translationBlocks
self.binding = binding
self.disabledNotches = disabledNotches
}
init(dimensions: @escaping (Notch) -> NotchDimension) {
self.dimensions = dimensions
self.translationBlocks = []
self.binding = nil
self.disabledNotches = []
}
// MARK: - Public
func appending(_ block: @escaping (Translation) -> Void) -> Self {
Value(
dimensions: dimensions,
translationBlocks: translationBlocks + [block],
binding: binding,
disabledNotches: disabledNotches
)
}
func setting(_ binding: Binding<Notch>) -> Self {
Value(
dimensions: dimensions,
translationBlocks: translationBlocks,
binding: binding,
disabledNotches: disabledNotches
)
}
func disabling(_ isDisabled: Bool, _ notch: Notch) -> Self {
Value(
dimensions: dimensions,
translationBlocks: translationBlocks,
binding: binding,
disabledNotches: isDisabled ? disabledNotches + [notch] : disabledNotches
)
}
}
}
================================================
FILE: Source/Internal/OverlayContainer/DynamicOverlayContainerAnimationController.swift
================================================
//
// DynamicOverlayContainerAnimationController.swift
// DynamicOverlay
//
// Created by Gaétan Zanella on 28/12/2020.
// Copyright © 2020 Fabernovel. All rights reserved.
//
import UIKit
import SwiftUI
import OverlayContainer
private struct Constant {
static let defaultMass: CGFloat = 1
static let defaultDamping: CGFloat = 0.7
static let defaultRigidDamping: CGFloat = 0.9
static let defaultResponse: CGFloat = 0.3
static let minimumDamping: CGFloat = 1
static let minimumVelocityConsideration: CGFloat = 150
static let maximumVelocityConsideration: CGFloat = 3000
}
struct DynamicOverlayContainerAnimationController: OverlayAnimatedTransitioning {
private var mass: CGFloat = Constant.defaultMass
private var damping: CGFloat = Constant.defaultDamping
private var response: CGFloat = Constant.defaultResponse
// MARK: - Life Cycle
public init(style: OverlayContainerViewController.OverlayStyle) {
switch style {
case .expandableHeight, .rigid:
// (gz) 2019-06-15 We also nullify the damping value when using rigid styles
// to avoid the panel to be lifted above the bottom of the screen.
damping = Constant.defaultRigidDamping
case .flexibleHeight:
damping = Constant.defaultDamping
}
}
// MARK: - Public
public func animation(using context: OverlayContainerTransitionCoordinatorContext) -> Animation? {
guard context.isAnimated else { return nil }
return .interpolatingSpring(
mass: Double(springMass(context: context)),
stiffness: Double(springStiffness(context: context)),
damping: Double(springDamping(context: context)),
initialVelocity: Double(springVelocity(context: context))
)
}
// MARK: - OverlayAnimatedTransitioning
public func interruptibleAnimator(using context: OverlayContainerContextTransitioning) -> UIViewImplicitlyAnimating {
let timing = UISpringTimingParameters(
mass: springMass(context: context),
stiffness: springStiffness(context: context),
damping: springDamping(context: context),
initialVelocity: CGVector(dx: springVelocity(context: context), dy: springVelocity(context: context))
)
return UIViewPropertyAnimator(
duration: 0, // duration is ignored when using `UISpringTimingParameters.init(mass:stiffness:damping:initialVelocity)`
timingParameters: timing
)
}
private func springMass(context: OverlayContainerTransitionContext) -> CGFloat {
mass
}
private func springStiffness(context: OverlayContainerTransitionContext) -> CGFloat {
pow(2 * .pi / response, 2)
}
private func springDamping(context: OverlayContainerTransitionContext) -> CGFloat {
let velocity = min(
Constant.maximumVelocityConsideration,
max(abs(context.velocity.y), Constant.minimumVelocityConsideration)
)
let velocityRange = Constant.maximumVelocityConsideration - Constant.minimumVelocityConsideration
let normalizedVelocity = (velocity - Constant.minimumVelocityConsideration) / velocityRange
let normalizedDamping = normalizedVelocity * (damping - Constant.minimumDamping) + Constant.minimumDamping
return 4 * .pi * normalizedDamping / response
}
private func springVelocity(context: OverlayContainerTransitionContext) -> CGFloat {
0
}
}
================================================
FILE: Source/Internal/OverlayContainer/OverlayContainerCoordinator.swift
================================================
//
// OverlayContainerCoordinator.swift
// DynamicOverlay
//
// Created by Gaétan Zanella on 02/12/2020.
// Copyright © 2020 Fabernovel. All rights reserved.
//
import UIKit
import SwiftUI
import OverlayContainer
struct OverlayContainerLayout: Equatable {
let indexToDimension: [Int: NotchDimension]
}
// (gz) 2022-01-30 `SwiftUI` compares struct properties one by one to determine either to update the view or not.
// To avoid useless updates, we wrap the passive values inside this class.
class OverlayContainerPassiveContainer: Equatable {
var onTranslation: ((OverlayTranslation) -> Void)?
var onNotchChange: ((Int) -> Void)?
static func == (lhs: OverlayContainerPassiveContainer, rhs: OverlayContainerPassiveContainer) -> Bool {
lhs === rhs
}
}
struct OverlayContainerState: Equatable {
let dragArea: DynamicOverlayDragArea
let drivingScrollViewProxy: DynamicOverlayScrollViewProxy
let notchIndex: Int?
let disabledNotches: Set<Int>
let layout: OverlayContainerLayout
}
class OverlayContainerCoordinator {
private let background: UIViewController
private let content: UIViewController
private let indexMapper = OverlayNotchIndexMapper()
typealias State = OverlayContainerState
private var state: State
private let style: OverlayContainerViewController.OverlayStyle
private let passiveContainer: OverlayContainerPassiveContainer
private var animationController: DynamicOverlayContainerAnimationController {
DynamicOverlayContainerAnimationController(style: style)
}
// MARK: - Life Cycle
init(style: OverlayContainerViewController.OverlayStyle,
layout: OverlayContainerLayout,
passiveContainer: OverlayContainerPassiveContainer,
background: UIViewController,
content: UIViewController) {
self.state = .initial(layout)
self.passiveContainer = passiveContainer
self.background = background
self.content = content
self.style = style
}
// MARK: - Public
func move(_ container: OverlayContainerViewController, to state: State, animated: Bool) {
if container.viewControllers.isEmpty {
container.viewControllers = [background, content]
}
let changes = OverlayContainerStateDiffer().diff(
from: self.state,
to: state
)
let requiresLayoutUpdate = changes.contains(.index) || changes.contains(.layout)
if requiresLayoutUpdate && animated {
// we update the content first
container.drivingScrollView = nil // issue #21
container.view.layoutIfNeeded()
}
if changes.contains(.layout) {
container.invalidateNotchHeights()
}
if let index = state.notchIndex, changes.contains(.index) {
container.moveOverlay(toNotchAt: index, animated: animated)
}
if changes.contains(.scrollView) {
CATransaction.setCompletionBlock { [weak container] in
guard let overlay = container?.topViewController?.view else { return }
container?.drivingScrollView = state.drivingScrollViewProxy.findScrollView(in: overlay)
}
}
self.state = state
if changes.contains(.layout) && !animated {
UIView.performWithoutAnimation {
container.view.layoutIfNeeded()
}
}
}
}
extension OverlayContainerCoordinator: OverlayContainerViewControllerDelegate {
// MARK: - OverlayContainerViewControllerDelegate
func numberOfNotches(in containerViewController: OverlayContainerViewController) -> Int {
indexMapper.reload(
layout: state.layout,
availableHeight: containerViewController.availableSpace
)
return indexMapper.numberOfOverlayIndexes()
}
func overlayContainerViewController(_ containerViewController: OverlayContainerViewController,
heightForNotchAt index: Int,
availableSpace: CGFloat) -> CGFloat {
indexMapper.height(forOverlayIndex: index)
}
func overlayContainerViewController(_ containerViewController: OverlayContainerViewController,
didMoveOverlay overlayViewController: UIViewController,
toNotchAt index: Int) {
let newState = state.withNewNotch(index)
guard newState != state else { return }
passiveContainer.onNotchChange?(indexMapper.dynamicIndex(forOverlayIndex: index))
}
func overlayContainerViewController(_ containerViewController: OverlayContainerViewController,
willTranslateOverlay overlayViewController: UIViewController,
transitionCoordinator: OverlayContainerTransitionCoordinator) {
let animation = animationController.animation(using: transitionCoordinator)
let transaction = Transaction(animation: animation)
let translation = OverlayTranslation(
height: transitionCoordinator.targetTranslationHeight,
transaction: transaction,
isDragging: transitionCoordinator.isDragging,
translationProgress: transitionCoordinator.overallTranslationProgress(),
containerFrame: containerViewController.view.frame,
velocity: transitionCoordinator.velocity,
heightForNotchIndex: { transitionCoordinator.height(forNotchAt: $0) }
)
withTransaction(transaction) { [weak passiveContainer] in
passiveContainer?.onTranslation?(translation)
}
}
func overlayContainerViewController(_ containerViewController: OverlayContainerViewController,
canReachNotchAt index: Int,
forOverlay overlayViewController: UIViewController) -> Bool {
!state.disabledNotches.map { indexMapper.overlayIndex(forDynamicIndex: $0) }.contains(index)
}
func overlayContainerViewController(_ containerViewController: OverlayContainerViewController,
shouldStartDraggingOverlay overlayViewController: UIViewController,
at point: CGPoint,
in coordinateSpace: UICoordinateSpace) -> Bool {
guard let overlay = containerViewController.topViewController else { return false }
let inOverlayPoint = overlay.view.convert(point, from: coordinateSpace)
if state.dragArea.isEmpty {
return overlay.view.frame.contains(inOverlayPoint)
}
return state.dragArea.contains(inOverlayPoint)
}
func overlayContainerViewController(_ containerViewController: OverlayContainerViewController,
transitioningDelegateForOverlay overlayViewController: UIViewController) -> OverlayTransitioningDelegate? {
self
}
}
extension OverlayContainerCoordinator: OverlayTransitioningDelegate {
// MARK: - OverlayTransitioningDelegate
func animationController(for overlayViewController: UIViewController) -> OverlayAnimatedTransitioning? {
animationController
}
}
private extension OverlayContainerState {
static func initial(_ layout: OverlayContainerLayout) -> OverlayContainerState {
OverlayContainerState(
dragArea: .default,
drivingScrollViewProxy: .default,
notchIndex: nil,
disabledNotches: [],
layout: layout
)
}
func withNewNotch(_ notch: Int) -> OverlayContainerState {
OverlayContainerState(
dragArea: dragArea,
drivingScrollViewProxy: drivingScrollViewProxy,
notchIndex: notch,
disabledNotches: disabledNotches,
layout: layout
)
}
}
================================================
FILE: Source/Internal/OverlayContainer/OverlayContainerDynamicOverlayView.swift
================================================
//
// OverlayContainerView.swift
// DynamicOverlay
//
// Created by Gaétan Zanella on 02/12/2020.
// Copyright © 2020 Fabernovel. All rights reserved.
//
import SwiftUI
struct OverlayContainerDynamicOverlayView<Background: View, Content: View>: View {
@State
private var dragArea: DynamicOverlayDragArea = .default
@State
private var scrollViewProxy: DynamicOverlayScrollViewProxy = .default
@State
private var passiveContainer = OverlayContainerPassiveContainer()
@Environment(\.behaviorValue)
private var behavior: DynamicOverlayBehaviorValue
let background: Background
let content: Content
// MARK: - View
var body: some View {
SwiftUIOverlayContainerRepresentableAdaptor(
adaptor: OverlayContainerRepresentableAdaptor(
containerState: makeContainerState(),
passiveContainer: passiveContainer,
content: OverlayContentHostingView(),
background: background
)
)
.overlayContent(content.overlayCoordinateSpace())
.onUpdate {
passiveContainer.onTranslation = behavior.block
// This is tricky. `OverlayContainerPassiveContainer` is a class inside a struct,
// `passiveContainer.onNotchChange = { self.behavior.binding?.wrappedValue = $0 }`
// would create a retain cycle as `self` includes a ref to `passiveContainer`.
let behavior = behavior
passiveContainer.onNotchChange = { behavior.binding?.wrappedValue = $0 }
}
.onDragAreaChange {
dragArea = $0
}
.onDrivingScrollViewChange {
scrollViewProxy = $0
}
}
// MARK: - Private
private func makeContainerState() -> OverlayContainerState {
OverlayContainerState(
dragArea: dragArea,
drivingScrollViewProxy: scrollViewProxy,
notchIndex: behavior.binding?.wrappedValue,
disabledNotches: behavior.disabledNotchIndexes,
layout: OverlayContainerLayout(indexToDimension: behavior.notchDimensions ?? [:])
)
}
}
private extension View {
func onUpdate(_ block: () -> Void) -> some View {
block()
return self
}
}
================================================
FILE: Source/Internal/OverlayContainer/OverlayContainerRepresentableAdaptor.swift
================================================
//
// OverlayContainerRepresentableAdaptor.swift
// DynamicOverlayTests
//
// Created by Gaétan Zanella on 20/04/2021.
// Copyright © 2021 Fabernovel. All rights reserved.
//
import Foundation
import SwiftUI
import OverlayContainer
struct OverlayContainerRepresentableAdaptor<Content: View, Background: View> {
struct Context {
let coordinator: OverlayContainerCoordinator
let transaction: Transaction
}
let containerState: OverlayContainerState
let passiveContainer: OverlayContainerPassiveContainer
let content: Content
let background: Background
private let style: OverlayContainerViewController.OverlayStyle = .expandableHeight
// MARK: - UIViewControllerRepresentable
func makeCoordinator() -> OverlayContainerCoordinator {
let contentController = UIHostingController(rootView: content)
contentController.view.backgroundColor = .clear
contentController.view.setContentCompressionResistancePriority(.defaultLow, for: .vertical)
contentController.view.setContentHuggingPriority(.defaultLow, for: .vertical)
let backgroundController = UIHostingController(rootView: background)
backgroundController.view.backgroundColor = .clear
return OverlayContainerCoordinator(
style: style,
layout: containerState.layout,
passiveContainer: passiveContainer,
background: backgroundController,
content: contentController
)
}
func makeUIViewController(context: Context) -> OverlayContainerViewController {
let controller = OverlayContainerViewController(style: style)
controller.delegate = context.coordinator
return controller
}
func updateUIViewController(_ container: OverlayContainerViewController,
context: Context) {
context.coordinator.move(
container,
to: containerState,
animated: context.transaction.animation != nil
)
}
}
================================================
FILE: Source/Internal/OverlayContainer/OverlayContainerStateDiffer.swift
================================================
//
// OverlayContainerStateDiffer.swift
// DynamicOverlay
//
// Created by Gaétan Zanella on 23/07/2021.
// Copyright © 2021 Fabernovel. All rights reserved.
//
import Foundation
struct OverlayContainerStateDiffer {
struct Changes: OptionSet {
let rawValue: Int
static let layout = Changes(rawValue: 1 << 0)
static let index = Changes(rawValue: 1 << 1)
static let scrollView = Changes(rawValue: 1 << 2)
}
func diff(from previous: OverlayContainerState, to next: OverlayContainerState) -> Changes {
var changes: Changes = []
if previous.notchIndex != next.notchIndex {
changes.insert(.index)
}
// issue #21
// The scroll view depends on the content, we need to first for a potential new scroll view
// at each update
changes.insert(.scrollView)
if previous.layout != next.layout {
changes.insert(.layout)
}
return changes
}
}
================================================
FILE: Source/Internal/OverlayContainer/SwiftUIOverlayContainerRepresentableAdaptor.swift
================================================
//
// SwiftUIOverlayContainerRepresentableAdaptor.swift
// DynamicOverlayTests
//
// Created by Gaétan Zanella on 16/04/2021.
// Copyright © 2021 Fabernovel. All rights reserved.
//
import Foundation
import SwiftUI
import OverlayContainer
struct SwiftUIOverlayContainerRepresentableAdaptor<Content: View, Background: View>: UIViewControllerRepresentable {
let adaptor: OverlayContainerRepresentableAdaptor<Content, Background>
// MARK: - UIViewControllerRepresentable
func makeCoordinator() -> OverlayContainerCoordinator {
adaptor.makeCoordinator()
}
func makeUIViewController(context: Context) -> OverlayContainerViewController {
adaptor.makeUIViewController(context: map(context))
}
func updateUIViewController(_ uiViewController: OverlayContainerViewController, context: Context) {
adaptor.updateUIViewController(uiViewController, context: map(context))
}
private func map(_ context: Context) -> OverlayContainerRepresentableAdaptor<Content, Background>.Context {
OverlayContainerRepresentableAdaptor<Content, Background>.Context(
coordinator: context.coordinator,
transaction: context.transaction
)
}
}
================================================
FILE: Source/Internal/OverlayContentModifier.swift
================================================
//
// OverlayContentModifier.swift
// DynamicOverlay
//
// Created by Gaétan Zanella on 03/12/2020.
// Copyright © 2020 Fabernovel. All rights reserved.
//
import SwiftUI
struct OverlayContentModifier<Overlay: View>: ViewModifier {
let overlay: Overlay
func body(content: Content) -> some View {
content.environment(\.overlayContentKey, AnyView(overlay))
}
}
extension View {
func overlayContent<Content: View>(_ content: Content) -> ModifiedContent<Self, OverlayContentModifier<Content>> {
modifier(OverlayContentModifier(overlay: content))
}
}
/// The root view of the overlay content
struct OverlayContentHostingView: View {
/// We use an environment variable to avoid UIViewController allocations each time the content changes.
@Environment(\.overlayContentKey)
var content: AnyView
var body: some View {
content
}
}
private struct OverlayContentKey: EnvironmentKey {
static var defaultValue = AnyView(EmptyView())
}
private extension EnvironmentValues {
var overlayContentKey: AnyView {
set {
self[OverlayContentKey.self] = newValue
}
get {
self[OverlayContentKey.self]
}
}
}
================================================
FILE: Source/Internal/OverlayNotchIndexMapper.swift
================================================
//
// OverlayNotchIndexMapper.swift
// DynamicOverlay
//
// Created by Gaétan Zanella on 29/12/2020.
// Copyright © 2020 Fabernovel. All rights reserved.
//
import SwiftUI
class OverlayNotchIndexMapper {
private var overlayIndexToDynamicIndex: [Int: Int] = [:]
private var overlayIndexToHeight: [Int: CGFloat] = [:]
private var dynamicIndexToOverlayIndex: [Int: Int] = [:]
func reload(layout: OverlayContainerLayout, availableHeight: CGFloat) {
overlayIndexToDynamicIndex = [:]
overlayIndexToHeight = [:]
dynamicIndexToOverlayIndex = [:]
let sortedIndexes = layout.indexToDimension.sorted(by: {
height(for: $0.value, availableHeight: availableHeight) < height(for: $1.value, availableHeight: availableHeight)
})
sortedIndexes.enumerated().forEach{ overlayIndex, dynamicValue in
overlayIndexToHeight[overlayIndex] = height(for: dynamicValue.value, availableHeight: availableHeight)
dynamicIndexToOverlayIndex[dynamicValue.key] = overlayIndex
overlayIndexToDynamicIndex[overlayIndex] = dynamicValue.key
}
}
func numberOfOverlayIndexes() -> Int {
overlayIndexToDynamicIndex.count
}
func dynamicIndex(forOverlayIndex index: Int) -> Int {
dynamicIndexToOverlayIndex[index] ?? 0
}
func overlayIndex(forDynamicIndex index: Int) -> Int {
overlayIndexToDynamicIndex[index] ?? 0
}
func height(forOverlayIndex index: Int) -> CGFloat {
overlayIndexToHeight[index] ?? 0
}
private func height(for dimension: NotchDimension,
availableHeight: CGFloat) -> CGFloat {
switch dimension.type {
case .absolute:
return CGFloat(dimension.value)
case .fractional:
return availableHeight * CGFloat(dimension.value)
}
}
}
================================================
FILE: Source/Internal/Utils/Binding+CaseIterable.swift
================================================
//
// Binding+CaseIterable.swift
// DynamicOverlay
//
// Created by Gaétan Zanella on 02/12/2020.
// Copyright © 2020 Fabernovel. All rights reserved.
//
import SwiftUI
extension Binding where Value: Equatable, Value: CaseIterable {
func indexBinding() -> Binding<Int> {
Binding<Int>(
get: {
Value.index(of: wrappedValue)
},
set: { index in
wrappedValue = Value.value(at: index)
}
)
}
}
extension CaseIterable where Self: Equatable {
static func index(of target: AllCases.Element) -> Int {
var index = 0
for value in allCases {
if value == target {
return index
}
index += 1
}
fatalError("Cannot find a valid index for \(target)")
}
static func value(at index: Int) -> AllCases.Element {
allCases[allCases.index(allCases.startIndex, offsetBy: index)]
}
}
================================================
FILE: Source/Public/DynamicOverlayBehavior.swift
================================================
//
// DynamicOverlayBehavior.swift
// DynamicOverlay
//
// Created by Gaétan Zanella on 02/12/2020.
// Copyright © 2020 Fabernovel. All rights reserved.
//
import SwiftUI
/// A protocol that describes the overlay behavior.
public protocol DynamicOverlayBehavior {
func makeModifier() -> AddDynamicOverlayBehaviorModifier
}
================================================
FILE: Source/Public/DynamicOverlayHandle.swift
================================================
//
// DynamicOverlayHandle.swift
// DynamicOverlay
//
// Created by Gaétan Zanella on 04/12/2020.
// Copyright © 2020 Fabernovel. All rights reserved.
//
import SwiftUI
public extension View {
/// Defines the target as a draggable view.
///
/// - parameter isActive: Boolean indicating whether the target is draggable.
func draggable(_ isActive: Bool = true) -> some View {
modifier(
ActiveOverlayAreaViewModifier(
key: DynamicOverlayDragAreaPreferenceKey.self,
isActive: isActive
)
)
}
/// Defines the target as the container of a driving scroll view.
/// When specified a driving scroll view coordinates its scrolling with the overlay translation.
///
/// - parameter isActive: Boolean indicating whether the scroll view is active.
func drivingScrollView(_ isActive: Bool = true) -> some View {
modifier(
ActiveOverlayAreaViewModifier(
key: DynamicOverlayScrollViewProxyPreferenceKey.self,
isActive: isActive
)
)
}
}
================================================
FILE: Source/Public/DynamicOverlayModifier.swift
================================================
//
// DynamicOverlayModifier.swift
// DynamicOverlay
//
// Created by Gaétan Zanella on 01/12/2020.
// Copyright © 2020 Fabernovel. All rights reserved.
//
import SwiftUI
public extension View {
/// Adds a dynamic overlay above this view.
///
/// - parameter content: the content of the overlay.
///
/// - returns: A view with a dynamic overlay added above this view.
func dynamicOverlay<Content: View>(_ content: Content) -> some View {
modifier(AddDynamicOverlayModifier(overlay: content))
}
/// Sets the overlay behavior for dynamic overlays within this view.
///
/// - parameter behavior: the behavior to apply.
///
/// - returns: A view with the specified behavior set.
///
/// This modifier affects the given view, as well as that view’s descendant views. It has no effect outside the view hierarchy on which you call it.
func dynamicOverlayBehavior<Behavior: DynamicOverlayBehavior>(_ behavior: Behavior) -> some View {
modifier(behavior.makeModifier())
}
}
public struct AddDynamicOverlayModifier<Overlay: View>: ViewModifier {
let overlay: Overlay
// MARK: - ViewModifier
public func body(content: Content) -> some View {
OverlayContainerDynamicOverlayView(
background: content,
content: overlay
)
}
}
public struct AddDynamicOverlayBehaviorModifier: ViewModifier {
let value: DynamicOverlayBehaviorValue
// MARK: - ViewModifier
public func body(content: Content) -> some View {
content.environment(\.behaviorValue, value)
}
}
================================================
FILE: Source/Public/MagneticNotchOverlayBehavior.swift
================================================
//
// MagneticNotchOverlayBehavior.swift
// DynamicOverlay
//
// Created by Gaétan Zanella on 02/12/2020.
// Copyright © 2020 Fabernovel. All rights reserved.
//
import SwiftUI
/// A `DynamicOverlayBehavior` instance describing 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.
public struct MagneticNotchOverlayBehavior<Notch> where Notch: CaseIterable, Notch: Equatable {
let value: Value
/// Creates a behavior with the given notches.
///
/// - parameter notches: The notches.
///
/// - returns: A behavior with the specified notches.
public init(notches: @escaping (Notch) -> NotchDimension) {
self.value = Value(dimensions: notches)
}
init(value: Value) {
self.value = value
}
}
public extension MagneticNotchOverlayBehavior {
/// The attributes of a translation
struct Translation {
/// The current overlay height.
public let height: CGFloat
/// The transaction associated to the translation.
public let transaction: Transaction
/// The overlay translation progress (from 0.0 to 1.0).
public let progress: Double
/// The overlay container size.
public let containerSize: CGSize
let heightForNotch: (Notch) -> CGFloat
/// returns the height of the given notch.
public func height(for notch: Notch) -> CGFloat {
heightForNotch(notch)
}
}
/// Adds an action to perform when the overlay moves.
///
/// - parameter action: The action to perform when the translation changes. The action closure’s parameter contains the current translation.
///
/// - returns: A version of the behavior that triggers the action when the translation changes.
func onTranslation(_ action: @escaping (Translation) -> Void) -> Self {
MagneticNotchOverlayBehavior(value: value.appending(action))
}
}
public extension MagneticNotchOverlayBehavior {
/// Updates the current overlay notch as it changes.
///
/// - parameter binding: A binding to a notch property.
///
/// - returns: A version of the behavior that updates the current overlay notch as it changes.
func notchChange(_ binding: Binding<Notch>) -> Self {
MagneticNotchOverlayBehavior(value: value.setting(binding))
}
}
public extension MagneticNotchOverlayBehavior {
/// Disables a notch.
///
/// - parameter notch: The notch to disable.
/// - parameter isDisabled: A boolean indicating whether the notch should be disabled.
///
/// - returns: A version of the behavior that disables the specified notch.
///
/// When a notch is disabled the overlay can not be dragged to it.
/// The `notchChange` binding is still effective though.
func disable(_ notch: Notch, _ isDisabled: Bool = true) -> Self {
MagneticNotchOverlayBehavior(value: value.disabling(isDisabled, notch))
}
}
extension MagneticNotchOverlayBehavior: DynamicOverlayBehavior {
public func makeModifier() -> AddDynamicOverlayBehaviorModifier {
AddDynamicOverlayBehaviorModifier(value: buildValue())
}
}
/// A dimension of a notch.
public struct NotchDimension: Hashable {
let type: ValueType
let value: Double
}
public extension NotchDimension {
/// Creates a dimension with an absolute point value.
static func absolute(_ value: Double) -> NotchDimension {
NotchDimension(type: .absolute, value: value)
}
/// Creates a dimension that is computed as a fraction of the height of the overlay parent view.
static func fractional(_ value: Double) -> NotchDimension {
NotchDimension(type: .fractional, value: value)
}
}
================================================
FILE: Tests/DynamicOverlayTests/DragHandleViewModifierTests.swift
================================================
//
// DragHandleTests.swift
// DynamicOverlayTests
//
// Created by Gaétan Zanella on 16/04/2021.
// Copyright © 2021 Fabernovel. All rights reserved.
//
import Foundation
import XCTest
import SwiftUI
@testable import DynamicOverlay
private struct HandleView: View {
var body: some View {
Color.red
}
}
private struct ContainerView: View {
let isActive: Bool
let frame: CGRect
let handler: (DynamicOverlayDragArea) -> Void
var body: some View {
GeometryReader { _ in
HandleView()
.frame(width: frame.width, height: frame.height)
.draggable(isActive)
.offset(x: frame.origin.x, y: frame.origin.y)
}
.onDragAreaChange(handler: handler)
.overlayCoordinateSpace()
}
}
private struct MultipleHandlesView: View {
let handler: (DynamicOverlayDragArea) -> Void
var body: some View {
VStack {
Color.orange.draggable()
Color.red.draggable()
}
.overlayCoordinateSpace()
.onDragAreaChange(handler: handler)
}
}
class DragHandleViewModifierTests: XCTestCase {
func testActiveState() {
let frame = CGRect(x: 0, y: 0, width: 200, height: 200)
let activeHandle = DynamicOverlayDragArea(area: .active(frame))
let notActiveHandle = DynamicOverlayDragArea(area: .inactive())
let point = CGPoint(x: 20.0, y: 20.0)
XCTAssertTrue(activeHandle.contains(point))
XCTAssertFalse(notActiveHandle.contains(point))
}
func testMultipleFrames() {
let values: [(Bool, CGRect)] = [
(true, CGRect(x: 30, y: 30, width: 50, height: 100)),
(false, CGRect(x: 0, y: 0, width: 400, height: 400)),
(true, CGRect(x: 0, y: 0, width: 400, height: 400)),
]
values.forEach { isActive, frame in
let expectation = XCTestExpectation()
let view = ContainerView(
isActive: isActive,
frame: frame,
handler: { handler in
XCTAssertEqual(handler.contains(frame), isActive)
expectation.fulfill()
}
)
ViewRenderer(view: view).render()
wait(for: [expectation], timeout: 0.3)
}
}
}
================================================
FILE: Tests/DynamicOverlayTests/DrivingScrollViewModifierTests.swift
================================================
//
// DrivingScrollViewModifierTests.swift
// DynamicOverlayTests
//
// Created by Gaétan Zanella on 16/04/2021.
// Copyright © 2021 Fabernovel. All rights reserved.
//
import Foundation
import XCTest
import SwiftUI
@testable import DynamicOverlay
private struct ContainerView: View {
let isActive: Bool
let isActiveHandler: (DynamicOverlayScrollViewProxy) -> Void
var body: some View {
ScrollView {
Color.green
}
.overlayCoordinateSpace()
.drivingScrollView(isActive)
.onDrivingScrollViewChange(handler: isActiveHandler)
}
}
private class IdentifiedScrollView: UIScrollView {
var id = ""
}
class DrivingScrollViewModifierTests: XCTestCase {
func testScrollViewSearch() {
let container = UIView(frame: CGRect(x: 0, y: 0, width: 400, height: 400))
let layer = UIView(frame: container.bounds)
container.addSubview(layer)
let scrollViews = Array(repeating: IdentifiedScrollView(), count: 4)
for i in scrollViews.indices {
let scrollView = scrollViews[i]
scrollView.frame.size.height = container.bounds.height / 2
scrollView.frame.size.width = container.bounds.width / 2
scrollView.frame.origin.x = container.bounds.width / 2 * CGFloat(i % 2)
scrollView.frame.origin.y = i > 1 ? container.bounds.height / 2 : 0
if i.isMultiple(of: 2) {
layer.addSubview(scrollView)
} else {
container.addSubview(scrollView)
}
}
for scrollView in scrollViews {
scrollViews.forEach { $0.id = "lure" }
scrollView.id = "target"
let proxy = DynamicOverlayScrollViewProxy(
area: .active(scrollView.frame)
)
let scrollView = proxy.findScrollView(in: container) as! IdentifiedScrollView
XCTAssertEqual(scrollView.id, "target")
}
}
func testDrivingScrollView() {
[false, true].forEach { shouldBeActive in
let expectation = XCTestExpectation()
var window: UIWindow!
let view = ContainerView(
isActive: shouldBeActive,
isActiveHandler: { handle in
CATransaction.setCompletionBlock {
if shouldBeActive {
XCTAssertNotNil(handle.findScrollView(in: window))
} else {
XCTAssertNil(handle.findScrollView(in: window))
}
expectation.fulfill()
}
}
)
let renderer = ViewRenderer(view: view)
window = renderer.window
renderer.render()
wait(for: [expectation], timeout: 0.1)
}
}
func testNoneDrivingScrollView() {
let expectation = XCTestExpectation()
expectation.isInverted = true
let view = Color.red.onDrivingScrollViewChange { _ in
expectation.fulfill()
}
ViewRenderer(view: view).render()
wait(for: [expectation], timeout: 0.1)
}
}
================================================
FILE: Tests/DynamicOverlayTests/MagneticNotchOverlayBehaviorValueTests.swift
================================================
//
// MagneticNotchOverlayBehaviorValueTests.swift
// DynamicOverlayTests
//
// Created by Gaétan Zanella on 16/04/2021.
// Copyright © 2021 Fabernovel. All rights reserved.
//
import Foundation
import XCTest
import SwiftUI
@testable import DynamicOverlay
private enum Constant {
static func dimension(for notch: TestNotch) -> NotchDimension {
switch notch {
case .min:
return .fractional(0.3)
case .max:
return .fractional(0.5)
}
}
}
private enum TestNotch: CaseIterable, Equatable {
case min, max
}
private typealias Behavior = MagneticNotchOverlayBehavior<TestNotch>
class MagneticNotchOverlayBehaviorValueTests: XCTestCase {
func testDisabledIndexesOverlayValue() {
var behavior = Behavior.empty()
XCTAssertEqual(behavior.buildValue().disabledNotchIndexes, [])
behavior = behavior.disable(.min)
XCTAssertEqual(behavior.buildValue().disabledNotchIndexes, [0])
behavior = behavior.disable(.max)
XCTAssertEqual(behavior.buildValue().disabledNotchIndexes, [0, 1])
}
func testNotchChangeOverlayValue() {
var behavior = Behavior.empty()
XCTAssertTrue(behavior.buildValue().binding == nil)
behavior = behavior.notchChange(.constant(.min))
XCTAssertTrue(behavior.buildValue().binding?.wrappedValue == 0)
behavior = behavior.notchChange(.constant(.max))
XCTAssertTrue(behavior.buildValue().binding?.wrappedValue == 1)
}
func testNotchDimensionOverlayValue() {
XCTAssertEqual(
Behavior.empty().buildValue().notchDimensions,
[
0: Constant.dimension(for: .min),
1: Constant.dimension(for: .max),
]
)
}
func testBlockOverlayValue() {
var behavior = Behavior.empty()
XCTAssertTrue(behavior.buildValue().block == nil)
behavior = behavior.onTranslation { _ in }
XCTAssertTrue(behavior.buildValue().block != nil)
behavior = behavior.onTranslation { _ in }
XCTAssertTrue(behavior.buildValue().block != nil)
}
func testTranslationMapping() {
let expectation = XCTestExpectation()
let overlayTranslations = OverlayTranslation.translations()
let expectedTranslations = Behavior.Translation.translations()
XCTAssertEqual(overlayTranslations.count, expectedTranslations.count)
expectation.expectedFulfillmentCount = overlayTranslations.count
class Context {
var translation: Behavior.Translation!
}
let context = Context()
let action = { (translation: Behavior.Translation) in
XCTAssertEqual(context.translation.containerSize, translation.containerSize)
XCTAssertEqual(context.translation.height, translation.height)
XCTAssertEqual(context.translation.progress, translation.progress)
TestNotch.allCases.forEach {
XCTAssertEqual(context.translation.height(for: $0), translation.height(for: $0))
}
expectation.fulfill()
}
zip(overlayTranslations, expectedTranslations).forEach { value, translation in
context.translation = translation
Behavior.empty().onTranslation(action).buildValue().block?(value)
}
}
}
private extension MagneticNotchOverlayBehavior.Translation where Notch == TestNotch {
static func translations() -> [Self] {
[
Behavior.Translation(
height: 30.0,
transaction: Transaction(),
progress: 0.0,
containerSize: CGSize(width: 30.0, height: 30.0),
heightForNotch: {
switch $0 {
case .max:
return 400
case .min:
return 200
}
}
),
Behavior.Translation(
height: 30.0,
transaction: Transaction(),
progress: 0.0,
containerSize: CGSize(width: 30.0, height: 30.0),
heightForNotch: {
switch $0 {
case .max:
return 400
case .min:
return 200
}
}
),
Behavior.Translation(
height: 30.0,
transaction: Transaction(),
progress: 0.5,
containerSize: CGSize(width: 30.0, height: 30.0),
heightForNotch: {
switch $0 {
case .max:
return 400
case .min:
return 200
}
}
),
Behavior.Translation(
height: 10.0,
transaction: Transaction(),
progress: 1.0,
containerSize: CGSize(width: 60.0, height: 90.0),
heightForNotch: {
switch $0 {
case .max:
return 400
case .min:
return 200
}
}
)
]
}
}
private extension OverlayTranslation {
static func translations() -> [OverlayTranslation] {
[
OverlayTranslation(
height: 30.0,
transaction: Transaction(),
isDragging: false,
translationProgress: 0.0,
containerFrame: CGRect(origin: .zero, size: CGSize(width: 30.0, height: 30.0)),
velocity: .zero,
heightForNotchIndex: { i -> CGFloat in
switch i {
case 0:
return 200.0
case 1:
return 400.0
default:
fatalError()
}
}
),
OverlayTranslation(
height: 30.0,
transaction: Transaction(),
isDragging: false,
translationProgress: -1.0,
containerFrame: CGRect(origin: .zero, size: CGSize(width: 30.0, height: 30.0)),
velocity: .zero,
heightForNotchIndex: { i -> CGFloat in
switch i {
case 0:
return 200.0
case 1:
return 400.0
default:
fatalError()
}
}
),
OverlayTranslation(
height: 30.0,
transaction: Transaction(),
isDragging: false,
translationProgress: 0.5,
containerFrame: CGRect(origin: .zero, size: CGSize(width: 30.0, height: 30.0)),
velocity: .zero,
heightForNotchIndex: { i -> CGFloat in
switch i {
case 0:
return 200.0
case 1:
return 400.0
default:
fatalError()
}
}
),
OverlayTranslation(
height: 10.0,
transaction: Transaction(),
isDragging: false,
translationProgress: 1.5,
containerFrame: CGRect(origin: .zero, size: CGSize(width: 60.0, height: 90.0)),
velocity: .zero,
heightForNotchIndex: { i -> CGFloat in
switch i {
case 0:
return 200.0
case 1:
return 400.0
default:
fatalError()
}
}
)
]
}
}
private extension Behavior {
static func empty() -> Self {
Behavior { Constant.dimension(for: $0) }
}
}
================================================
FILE: Tests/DynamicOverlayTests/NotchBindingDynamicOverlayTests.swift
================================================
//
// NotchBindingDynamicOverlayTests.swift
// DynamicOverlay
//
// Created by Gaétan Zanella on 10/04/2021.
// Copyright © 2021 Fabernovel. All rights reserved.
//
import Foundation
import SwiftUI
import XCTest
import DynamicOverlay
private enum Notch: CaseIterable, Equatable {
case min
case max
}
private struct Constants {
static func height(for notch: Notch) -> CGFloat {
switch notch {
case .max:
return 300.0
case .min:
return 200.0
}
}
}
private struct NotchChangeView: View {
@ObservedObject
var target: ValuePublisher<Notch>
let onFrameChange: (CGRect) -> Void
var body: some View {
Color.red
.dynamicOverlay(Color.green.onFrameChange(onFrameChange))
.dynamicOverlayBehavior(behavior)
}
var behavior: some DynamicOverlayBehavior {
MagneticNotchOverlayBehavior<Notch> { notch in
.absolute(Double(Constants.height(for: notch)))
}
.notchChange($target.value)
}
}
class NotchBindingDynamicOverlayTests: XCTestCase {
func testInitialMaxNotch() {
expectNotchHeight(.max)
}
func testInitialMinNotch() {
expectNotchHeight(.min)
}
func testNotchChange() {
class Context {
var expectedHeight: CGFloat = 0.0
var current = Notch.min
var displayedFrame = CGRect.zero
var expectations: [Notch: XCTestExpectation] = [:]
}
let target = ValuePublisher(Notch.min)
let notches: [Notch] = [.min, .max]
let context = Context()
context.expectations = Dictionary(uniqueKeysWithValues: notches.map { ($0, XCTestExpectation()) })
let view = NotchChangeView(target: target) { rect in
context.displayedFrame = rect
context.expectations[context.current]?.fulfill()
}
let renderer = ViewRenderer(view: view)
notches.forEach { notch in
guard let expectation = context.expectations[notch] else { return }
context.current = notch
target.update(notch)
renderer.render()
wait(for: [expectation], timeout: 1.0)
let overlayFrame = renderer.window.bounds.intersection(context.displayedFrame)
XCTAssertEqual(overlayFrame.height, Constants.height(for: notch))
context.displayedFrame = .zero
}
}
private func expectNotchHeight(_ notch: Notch) {
let target = ValuePublisher(notch)
var displayedFrame: CGRect = .zero
let view = NotchChangeView(target: target) { rect in
displayedFrame = rect
}
let renderer = ViewRenderer(view: view)
renderer.render()
let overlayFrame = renderer.window.bounds.intersection(displayedFrame)
XCTAssertEqual(overlayFrame.height, Constants.height(for: notch))
}
}
================================================
FILE: Tests/DynamicOverlayTests/NotchDimensionDynamicOverlayTests.swift
================================================
//
// NotchDimensionDynamicOverlayTests.swift
// DynamicOverlayTests
//
// Created by Gaétan Zanella on 16/04/2021.
// Copyright © 2021 Fabernovel. All rights reserved.
//
import Foundation
import XCTest
import SwiftUI
import DynamicOverlay
private enum Notch: CaseIterable, Equatable {
case min
}
private struct NotchDimensionView: View {
let dimension: () -> NotchDimension
let onHeightChange: (CGFloat) -> Void
var body: some View {
Color.red
.dynamicOverlay(Color.green.onHeightChange(onHeightChange))
.dynamicOverlayBehavior(behavior)
}
var behavior: some DynamicOverlayBehavior {
MagneticNotchOverlayBehavior<Notch> { _ in
dimension()
}
}
}
class NotchDimensionDynamicOverlayTests: XCTestCase {
func testVariousDimensions() {
class Context {
var dimension: NotchDimension = .absolute(0)
var expectedHeight: CGFloat = 0.0
var expectation = XCTestExpectation()
}
let renderer = ViewRenderer(view: EmptyView())
let resultByDimension: [NotchDimension: CGFloat] = [
.absolute(-1) : 0.0,
.absolute(0) : 0.0,
.absolute(200.0) : 200.0,
.fractional(-1.0) : 0.0,
.fractional(0.0) : 0.0,
.fractional(2.0) : renderer.safeBounds.height * 2.0,
.fractional(0.5) : renderer.safeBounds.height * 0.5,
]
resultByDimension.forEach { dimension, expectedHeight in
let context = Context()
let view = NotchDimensionView(dimension: { context.dimension }) { height in
context.expectation.fulfill()
XCTAssertEqual(context.expectedHeight.rounded(.up), height.rounded(.up))
}
context.expectedHeight = expectedHeight
context.expectation = XCTestExpectation()
context.dimension = dimension
ViewRenderer(view: view).render()
wait(for: [context.expectation], timeout: 0.3)
}
}
}
================================================
FILE: Tests/DynamicOverlayTests/NotchTranslationDynamicOverlayTests.swift
================================================
//
// NotchTranslationDynamicOverlayTests.swift
// DynamicOverlay
//
// Created by Gaétan Zanella on 15/04/2021.
// Copyright © 2021 Fabernovel. All rights reserved.
//
import Foundation
import SwiftUI
import XCTest
@testable import DynamicOverlay
private enum Notch: CaseIterable, Equatable {
case min, max
}
private struct Constants {
static func insets(for layout: TranslationLayout) -> UIEdgeInsets {
switch layout {
case .compact:
return UIEdgeInsets(top: 20, left: 20, bottom: 20, right: 20)
case .full:
return .zero
}
}
static func height(for notch: Notch) -> CGFloat {
switch notch {
case .max:
return 300.0
case .min:
return 100.0
}
}
}
private typealias OverlayTranslation = MagneticNotchOverlayBehavior<Notch>.Translation
private struct TranslationView: View {
@ObservedObject
var target: ValuePublisher<Notch>
let onTranslation: (OverlayTranslation) -> Void
var body: some View {
Color.red
.dynamicOverlay(Color.green)
.dynamicOverlayBehavior(behavior)
}
private var behavior: MagneticNotchOverlayBehavior<Notch> {
MagneticNotchOverlayBehavior { notch in
.absolute(Double(Constants.height(for: notch)))
}
.notchChange($target.value)
.onTranslation(onTranslation)
.disable(.min)
}
}
private enum TranslationLayout {
case full, compact
}
private struct TranslationContainerView: View {
let layout: TranslationLayout
@ObservedObject
var target: ValuePublisher<Notch>
let onTranslation: (OverlayTranslation) -> Void
var body: some View {
TranslationView(
target: target,
onTranslation: onTranslation
)
.padding(insets)
}
private var insets: EdgeInsets {
let layoutInsets = Constants.insets(for: layout)
return EdgeInsets(
top: layoutInsets.top,
leading: layoutInsets.left,
bottom: layoutInsets.bottom,
trailing: layoutInsets.bottom
)
}
}
class NotchTranslationDynamicOverlayTests: XCTestCase {
func testInitialTranslation() {
class Context {
var expectedTranslation = OverlayTranslation.none
}
let context = Context()
let expectation = XCTestExpectation()
expectation.expectedFulfillmentCount = Notch.allCases.count
Notch.allCases.forEach { notch in
let target = ValuePublisher(notch)
let view = TranslationView(target: target) { translation in
expectation.fulfill()
self.expect(translation, toEqual: context.expectedTranslation)
}
let renderer = ViewRenderer(view: view)
context.expectedTranslation = .initial(for: notch, in: renderer.safeBounds)
renderer.render()
}
wait(for: [expectation], timeout: 1.0)
}
func testNotchMovesTranslation() {
class Context {
var expectedTranslation = OverlayTranslation.none
var expectation = XCTestExpectation()
}
let context = Context()
let target = ValuePublisher(Notch.min)
let view = TranslationView(target: target) { translation in
context.expectation.fulfill()
self.expect(translation, toEqual: context.expectedTranslation)
}
// Initial
let renderer = ViewRenderer(view: view)
context.expectedTranslation = .initial(for: .min, in: renderer.safeBounds)
renderer.render()
wait(for: [context.expectation], timeout: 0.3)
// Max not animated
context.expectation = XCTestExpectation()
context.expectedTranslation = .moved(to: .max, animated: false, in: renderer.safeBounds)
target.update(.max)
wait(for: [context.expectation], timeout: 0.3)
// Min animated
context.expectation = XCTestExpectation()
context.expectedTranslation = .moved(to: .min, animated: true, in: renderer.safeBounds)
withAnimation {
target.update(.min)
}
wait(for: [context.expectation], timeout: 0.3)
// Max animated
context.expectation = XCTestExpectation()
context.expectedTranslation = .moved(to: .max, animated: true, in: renderer.safeBounds)
withAnimation {
target.update(.max)
}
wait(for: [context.expectation], timeout: 0.3)
}
func testLayoutChangesTranslation() {
class Context {
var expected = OverlayTranslation.none
}
let layouts: [TranslationLayout] = [.compact, .full]
layouts.forEach { layout in
let context = Context()
let expectation = XCTestExpectation()
let target = ValuePublisher(Notch.min)
let view = TranslationContainerView(
layout: layout,
target: target,
onTranslation: { translation in
expectation.fulfill()
self.expect(translation, toEqual: context.expected)
}
)
let renderer = ViewRenderer(view: view)
context.expected = .layout(layout, in: renderer.safeBounds)
renderer.render()
wait(for: [expectation], timeout: 0.3)
}
}
// MARK: - Private
private func expect(_ lhs: OverlayTranslation, toEqual rhs: OverlayTranslation) {
XCTAssertEqual(lhs.containerSize, rhs.containerSize)
XCTAssertEqual(lhs.progress, rhs.progress)
XCTAssertEqual(lhs.height, rhs.height)
let lhsHasAnimation = lhs.transaction.animation != nil
let rhsHasAnimation = rhs.transaction.animation != nil
XCTAssertEqual(lhsHasAnimation, rhsHasAnimation)
Notch.allCases.forEach {
XCTAssertEqual(lhs.height(for: $0), rhs.height(for: $0))
}
}
}
private extension OverlayTranslation {
static var none: OverlayTranslation {
OverlayTranslation(height: 0, transaction: Transaction(), progress: 0, containerSize: .zero, heightForNotch: { _ in 0 })
}
static func initial(for notch: Notch, in bounds: CGRect) -> OverlayTranslation {
.notch(notch, animated: false, in: bounds)
}
static func moved(to notch: Notch, animated: Bool, in bounds: CGRect) -> OverlayTranslation {
.notch(notch, animated: animated, in: bounds)
}
static func layout(_ layout: TranslationLayout, in bounds: CGRect) -> OverlayTranslation {
OverlayTranslation(
height: Constants.height(for: .min),
transaction: Transaction(),
progress: 0.0,
containerSize: bounds.inset(by: Constants.insets(for: layout)).size,
heightForNotch: Constants.height(for:)
)
}
private static func notch(_ notch: Notch, animated: Bool, in bounds: CGRect) -> OverlayTranslation {
OverlayTranslation(
height: Constants.height(for: notch),
transaction: Transaction(animation: animated ? .default : nil),
progress: notch == .max ? 1.0 : 0.0,
containerSize: bounds.size,
heightForNotch: Constants.height(for:)
)
}
}
================================================
FILE: Tests/DynamicOverlayTests/OverlayContainerRepresentableAdaptorTests.swift
================================================
//
// OverlayContainerRepresentableAdaptorTests.swift
// DynamicOverlayTests
//
// Created by Gaétan Zanella on 20/04/2021.
// Copyright © 2021 Fabernovel. All rights reserved.
//
import Foundation
import XCTest
import SwiftUI
import OverlayContainer
@testable import DynamicOverlay
private struct AdaptorParameters {
var drivingHandle: DynamicOverlayScrollViewProxy
var handleValue: DynamicOverlayDragArea
var disabledNotches: Set<Int> = []
var indexToDimension: [Int: NotchDimension] = [:]
var onIndexChange: ((Int) -> Void)?
}
class OverlayContainerRepresentableAdaptorTests: XCTestCase {
class Context {
let container: OverlayContainerViewController
let coordinator: OverlayContainerCoordinator
var overlay: UIViewController {
container.topViewController!
}
init(container: OverlayContainerViewController,
coordinator: OverlayContainerCoordinator) {
self.container = container
self.coordinator = coordinator
}
func layout() {
container.view.frame = CGRect(origin: .zero, size: CGSize(width: 400, height: 400))
container.view.layoutIfNeeded()
}
}
func testViewControllersSetup() {
let context = makeContext(
for: AdaptorParameters(
drivingHandle: .default,
handleValue: .default,
indexToDimension: [0: .absolute(200.0)]
)
)
context.layout()
XCTAssertEqual(context.container.viewControllers.count, 2)
}
func testDefaultDraggingStart() {
let context = makeContext(
for: AdaptorParameters(
drivingHandle: .default,
handleValue: .default,
indexToDimension: [0: .absolute(200.0)]
)
)
context.layout()
let aboveOverlayPoint = CGPoint(x: 100, y: -10)
XCTAssertEqual(
context.container.delegate?.overlayContainerViewController(
context.container,
shouldStartDraggingOverlay: context.overlay,
at: aboveOverlayPoint,
in: context.overlay.view
),
false
)
let inOverlayPoint = CGPoint(x: 100, y: 100)
XCTAssertEqual(
context.container.delegate?.overlayContainerViewController(
context.container,
shouldStartDraggingOverlay: context.overlay,
at: inOverlayPoint,
in: context.overlay.view
),
true
)
}
func testDisabledDraggingStart() {
let context = makeContext(
for: AdaptorParameters(
drivingHandle: .default,
handleValue: DynamicOverlayDragArea(area: .inactive()),
indexToDimension: [0: .absolute(200.0)]
)
)
context.layout()
let aboveOverlayPoint = CGPoint(x: 100, y: -10)
XCTAssertEqual(
context.container.delegate?.overlayContainerViewController(
context.container,
shouldStartDraggingOverlay: context.overlay,
at: aboveOverlayPoint,
in: context.overlay.view
),
false
)
let inOverlayPoint = CGPoint(x: 100, y: 100)
XCTAssertEqual(
context.container.delegate?.overlayContainerViewController(
context.container,
shouldStartDraggingOverlay: context.overlay,
at: inOverlayPoint,
in: context.overlay.view
),
false
)
}
func testEnabledDraggingStart() {
let context = makeContext(
for: AdaptorParameters(
drivingHandle: .default,
handleValue: DynamicOverlayDragArea(
area: .active(CGRect(origin: .zero, size: CGSize(width: 200, height: 300)))
),
indexToDimension: [0: .absolute(200.0)]
)
)
context.layout()
let inZonePoint = CGPoint(x: 100, y: 100)
XCTAssertEqual(
context.container.delegate?.overlayContainerViewController(
context.container,
shouldStartDraggingOverlay: context.overlay,
at: inZonePoint,
in: context.overlay.view
),
true
)
let outZonePoint = CGPoint(x: 300, y: 100)
XCTAssertEqual(
context.container.delegate?.overlayContainerViewController(
context.container,
shouldStartDraggingOverlay: context.overlay,
at: outZonePoint,
in: context.overlay.view
),
false
)
}
func testOverlayMoveNotification() {
var index = 0
let context = makeContext(
for: AdaptorParameters(
drivingHandle: .default,
handleValue: DynamicOverlayDragArea(area: .default),
indexToDimension: [0: .absolute(200.0), 1: .absolute(300.0)],
onIndexChange: { index = $0 }
)
)
context.layout()
context.container.moveOverlay(toNotchAt: 1, animated: false)
context.layout()
XCTAssertEqual(index, 1)
}
func testNumberOfNotches() {
let dimensions: [[Int: NotchDimension]] = [
[0: .absolute(200)],
[0: .absolute(200), 1: .absolute(300)],
[0: .absolute(200), 1: .absolute(300), 3: .absolute(400)],
]
dimensions.forEach { layout in
let context = makeContext(
for: AdaptorParameters(
drivingHandle: .default,
handleValue: .default,
indexToDimension: layout
)
)
context.layout()
XCTAssertEqual(
context.container.delegate?.numberOfNotches(in: context.container) ?? 0,
layout.count
)
}
}
func testDisabledNotches() {
let all: [Int] = [0, 1, 2]
let indexes: [Set<Int>] = [
[],
[0],
[0, 1],
[0, 1, 2],
[2],
]
indexes.forEach { disabledIndexes in
let context = makeContext(
for: AdaptorParameters(
drivingHandle: .default,
handleValue: .default,
disabledNotches: disabledIndexes,
indexToDimension: Dictionary(uniqueKeysWithValues: all.map { ($0, .absolute(100 * Double($0))) })
)
)
context.layout()
Set(all).subtracting(disabledIndexes).forEach { index in
XCTAssertEqual(
context.container.delegate?.overlayContainerViewController(
context.container,
canReachNotchAt: index,
forOverlay: context.overlay
) ?? false,
true
)
}
disabledIndexes.forEach { index in
XCTAssertEqual(
context.container.delegate?.overlayContainerViewController(
context.container,
canReachNotchAt: index,
forOverlay: context.overlay
) ?? true,
false
)
}
}
}
private func makeContext(for parameters: AdaptorParameters) -> Context {
let holder = OverlayContainerPassiveContainer()
holder.onNotchChange = parameters.onIndexChange
let adaptor = OverlayContainerRepresentableAdaptor.init(
containerState: OverlayContainerState(
dragArea: parameters.handleValue,
drivingScrollViewProxy: parameters.drivingHandle,
notchIndex: nil,
disabledNotches: parameters.disabledNotches,
layout: OverlayContainerLayout(indexToDimension: parameters.indexToDimension)
),
passiveContainer: holder,
content: ContentView(),
background: Color.green
)
let coordinator = adaptor.makeCoordinator()
let context = OverlayContainerRepresentableAdaptor<ContentView, Color>.Context(
coordinator: coordinator,
transaction: Transaction()
)
let container = adaptor.makeUIViewController(context: context)
adaptor.updateUIViewController(container, context: context)
return Context(container: container, coordinator: coordinator)
}
}
private struct ContentView: View {
var body: some View {
List { Text("") }
}
}
================================================
FILE: Tests/DynamicOverlayTests/OverlayNotchIndexMapperTests.swift
================================================
//
// OverlayNotchIndexMapperTests.swift
// DynamicOverlay
//
// Created by Gaétan Zanella on 29/12/2020.
// Copyright © 2020 Fabernovel. All rights reserved.
//
import XCTest
@testable import DynamicOverlay
class OverlayNotchIndexMapperTests: XCTestCase {
private var mapper: OverlayNotchIndexMapper!
override func setUp() {
self.mapper = OverlayNotchIndexMapper()
}
func testAlreadyOrderedIndexesMapping() {
let layout = OverlayContainerLayout(
indexToDimension: [
0: NotchDimension(type: .fractional, value: 0.1),
1: NotchDimension(type: .fractional, value: 0.2),
2: NotchDimension(type: .fractional, value: 0.3),
]
)
mapper.reload(
layout: layout,
availableHeight: 200.0
)
XCTAssert(mapper.numberOfOverlayIndexes() == 3)
XCTAssert(mapper.dynamicIndex(forOverlayIndex: 0) == 0)
XCTAssert(mapper.dynamicIndex(forOverlayIndex: 1) == 1)
XCTAssert(mapper.dynamicIndex(forOverlayIndex: 2) == 2)
XCTAssert(mapper.overlayIndex(forDynamicIndex: 0) == 0)
XCTAssert(mapper.overlayIndex(forDynamicIndex: 1) == 1)
XCTAssert(mapper.overlayIndex(forDynamicIndex: 2) == 2)
}
func testFractionalNotchDimensionReordering() {
let layout = OverlayContainerLayout(
indexToDimension: [
0: NotchDimension(type: .absolute, value: 100),
1: NotchDimension(type: .fractional, value: 0.1),
]
)
mapper.reload(
layout: layout,
availableHeight: 200.0
)
XCTAssert(mapper.numberOfOverlayIndexes() == 2)
XCTAssert(mapper.dynamicIndex(forOverlayIndex: 0) == 1)
XCTAssert(mapper.dynamicIndex(forOverlayIndex: 1) == 0)
XCTAssert(mapper.overlayIndex(forDynamicIndex: 1) == 0)
XCTAssert(mapper.overlayIndex(forDynamicIndex: 0) == 1)
}
func testNotchFractionalHeights() {
let layout = OverlayContainerLayout(
indexToDimension: [
0: NotchDimension(type: .fractional, value: 0.1),
1: NotchDimension(type: .fractional, value: 0.5),
2: NotchDimension(type: .fractional, value: 1),
]
)
mapper.reload(
layout: layout,
availableHeight: 200.0
)
XCTAssert(mapper.numberOfOverlayIndexes() == 3)
XCTAssert(mapper.height(forOverlayIndex: 0) == 20.0)
XCTAssert(mapper.height(forOverlayIndex: 1) == 100.0)
XCTAssert(mapper.height(forOverlayIndex: 2) == 200.0)
}
}
================================================
FILE: Tests/DynamicOverlayTests/Utils/ValuePublisher.swift
================================================
//
// ValuePublisher.swift
// DynamicOverlay
//
// Created by Gaétan Zanella on 15/04/2021.
// Copyright © 2021 Fabernovel. All rights reserved.
//
import Foundation
class ValuePublisher<V>: ObservableObject {
@Published
var value: V
init(_ value: V) {
self.value = value
}
func update(_ value: V) {
self.value = value
}
}
================================================
FILE: Tests/DynamicOverlayTests/Utils/View+Measure.swift
================================================
//
// View+OnHeightCHange.swift
// DynamicOverlay
//
// Created by Gaétan Zanella on 15/04/2021.
// Copyright © 2021 Fabernovel. All rights reserved.
//
import Foundation
import SwiftUI
extension View {
func onHeightChange(_ block: @escaping (CGFloat) -> Void) -> some View {
onFrameChange { block($0.height) }
}
func onFrameChange(in coordinateSpace: CoordinateSpace = .global,
_ block: @escaping (CGRect) -> Void) -> some View {
modifier(OnFrameChangeViewModifier(coordinateSpace: coordinateSpace, block: block))
}
}
private struct OnFrameChangeViewModifier: ViewModifier {
let coordinateSpace: CoordinateSpace
let block: (CGRect) -> Void
func body(content: Content) -> some View {
content.background(
GeometryReader { proxy -> Color in
let frame = proxy.frame(in: coordinateSpace)
block(frame)
return Color.clear
}
)
}
}
================================================
FILE: Tests/DynamicOverlayTests/Utils/ViewInspector.swift
================================================
//
// ViewInspector.swift
// DynamicOverlayTests
//
// Created by Gaétan Zanella on 16/04/2021.
// Copyright © 2021 Fabernovel. All rights reserved.
//
import UIKit
struct ViewInspector {
let view: UIView
func search<V: UIView>(_ type: V.Type) -> V {
guard let result = view.search(type) else {
fatalError("\(type) does not exist")
}
return result
}
}
private extension UIView {
func search<V: UIView>(_ type: V.Type) -> V? {
if let result = self as? V {
return result
}
for subview in subviews {
if let target = subview.search(type) {
return target
}
}
return nil
}
}
================================================
FILE: Tests/DynamicOverlayTests/Utils/ViewRenderer.swift
================================================
//
// ViewRenderer.swift
// DynamicOverlay
//
// Created by Gaétan Zanella on 12/04/2021.
// Copyright © 2021 Fabernovel. All rights reserved.
//
import Foundation
import SwiftUI
class ViewRenderer<V: View> {
var size: CGSize {
window.frame.size
}
var safeAreaInsets: UIEdgeInsets {
window.safeAreaInsets
}
var bounds: CGRect {
window.bounds
}
var safeBounds: CGRect {
bounds.inset(by: safeAreaInsets)
}
var window: UIWindow {
UIApplication.shared.windows.first!
}
private let hostController: UIHostingController<V>
init(view: V) {
self.hostController = UIHostingController(rootView: view)
}
func render() {
if window.rootViewController !== hostController {
window.rootViewController = hostController
}
CATransaction.flush()
}
}
================================================
FILE: fastlane/Fastfile
================================================
default_platform(:ios)
platform :ios do
desc "Run all unit tests"
lane :tests do
scan(
scheme: "DynamicOverlay_Example",
project: "DynamicOverlay_Example/DynamicOverlay_Example.xcodeproj",
clean: true
)
end
desc "Pod linting"
lane :pod_lint do
pod_lib_lint(allow_warnings: true)
end
desc "Carthage linting"
lane :carthage_lint do
# FIX lint
# carthage(command: "build", no_skip_current: true, platform: "iOS")
end
desc "SPM linting"
lane :spm_lint do
output = "Package.xcodeproj"
Dir.chdir("..") do
sh("swift package generate-xcodeproj --output #{output} --xcconfig-overrides #{ENV["XCCONFIG"]}")
end
xcodebuild(
project: output,
scheme: "#{ENV["SCHEME"]}-Package"
)
Dir.chdir("..") do
sh("rm Package.resolved")
sh("rm -rf Package.xcodeproj")
end
end
desc "Release a new version"
lane :release do |options|
target_version = options[:version]
raise "The version is missed. Use `fastlane release version:{version_number}`.`" if target_version.nil?
ensure_git_branch(branch: "(release/*)|(hotfix/*)")
ensure_git_status_clean
podspec = ENV["PODSPEC"]
version_bump_podspec(path: podspec, version_number: target_version)
git_add
git_commit(
path: ["DynamicOverlay.podspec"],
message: "Bump to #{target_version}"
)
ensure_git_status_clean
add_git_tag(tag: target_version)
changelog = read_changelog(
changelog_path: "CHANGELOG.md",
section_identifier: "[#{target_version}]"
)
# Push
push_to_git_remote
push_git_tags(tag: target_version)
UI.success "Pushed 🎉"
# Release cocoapods
pod_push
UI.success "Released 🎉"
# Release Github
set_github_release(
repository_name: "faberNovel/DynamicOverlay",
api_token: ENV["GITHUB_TOKEN"],
name: "v#{target_version}",
tag_name: "#{target_version}",
description: changelog,
)
# Make PR
create_pull_request(
api_token: ENV["GITHUB_TOKEN"],
repo: "faberNovel/DynamicOverlay",
title: "Release #{target_version}",
base: "main",
body: changelog
)
end
end
================================================
FILE: fastlane/Pluginfile
================================================
# Autogenerated by fastlane
#
# Ensure this file is checked in to source control!
gem 'fastlane-plugin-changelog'
gitextract_s05uf70j/
├── .github/
│ └── workflows/
│ ├── ci.yml
│ └── release.yml
├── .gitignore
├── CHANGELOG.md
├── DynamicOverlay.podspec
├── DynamicOverlay_Example/
│ ├── DynamicOverlay_Example/
│ │ ├── Classes/
│ │ │ └── MapApp.swift
│ │ ├── Configuration/
│ │ │ └── Info.plist
│ │ ├── Resources/
│ │ │ ├── Assets.xcassets/
│ │ │ │ ├── AppIcon.appiconset/
│ │ │ │ │ └── Contents.json
│ │ │ │ └── Contents.json
│ │ │ └── Base.lproj/
│ │ │ └── LaunchScreen.storyboard
│ │ ├── UIKit/
│ │ │ └── UIKitAppDelegate.swift
│ │ └── View/
│ │ ├── ActionCell.swift
│ │ ├── BackdropView.swift
│ │ ├── FavoriteCell.swift
│ │ ├── MapRootView.swift
│ │ ├── MapView.swift
│ │ ├── OverlayBackgroundView.swift
│ │ ├── OverlayView.swift
│ │ └── SearchBar.swift
│ └── DynamicOverlay_Example.xcodeproj/
│ ├── project.pbxproj
│ ├── project.xcworkspace/
│ │ ├── contents.xcworkspacedata
│ │ └── xcshareddata/
│ │ ├── IDEWorkspaceChecks.plist
│ │ └── swiftpm/
│ │ └── Package.resolved
│ └── xcshareddata/
│ └── xcschemes/
│ └── DynamicOverlay_Example.xcscheme
├── Gemfile
├── LICENSE
├── Package.resolved
├── Package.swift
├── Package.xcconfig
├── README.md
├── Source/
│ ├── DynamicOverlay.h
│ ├── Info.plist
│ ├── Internal/
│ │ ├── DynamicOverlayBehaviorValue.swift
│ │ ├── DynamicOverlayNotchTransition+DynamicOverlayBehavior.swift
│ │ ├── Handle/
│ │ │ ├── ActivatedOverlayArea.swift
│ │ │ ├── ActiveOverlayAreaViewModifier.swift
│ │ │ ├── Drag/
│ │ │ │ ├── DynamicOverlayDragArea.swift
│ │ │ │ └── OnDragAreaChangeViewModifier.swift
│ │ │ ├── DrivingScrollView/
│ │ │ │ ├── DynamicOverlayScrollViewProxy.swift
│ │ │ │ └── OnDrivingScrollViewChangeViewModifier.swift
│ │ │ └── OverlayContainerCoordinateSpace.swift
│ │ ├── MagneticNotchOverlayBehaviorValue.swift
│ │ ├── OverlayContainer/
│ │ │ ├── DynamicOverlayContainerAnimationController.swift
│ │ │ ├── OverlayContainerCoordinator.swift
│ │ │ ├── OverlayContainerDynamicOverlayView.swift
│ │ │ ├── OverlayContainerRepresentableAdaptor.swift
│ │ │ ├── OverlayContainerStateDiffer.swift
│ │ │ └── SwiftUIOverlayContainerRepresentableAdaptor.swift
│ │ ├── OverlayContentModifier.swift
│ │ ├── OverlayNotchIndexMapper.swift
│ │ └── Utils/
│ │ └── Binding+CaseIterable.swift
│ └── Public/
│ ├── DynamicOverlayBehavior.swift
│ ├── DynamicOverlayHandle.swift
│ ├── DynamicOverlayModifier.swift
│ └── MagneticNotchOverlayBehavior.swift
├── Tests/
│ └── DynamicOverlayTests/
│ ├── DragHandleViewModifierTests.swift
│ ├── DrivingScrollViewModifierTests.swift
│ ├── MagneticNotchOverlayBehaviorValueTests.swift
│ ├── NotchBindingDynamicOverlayTests.swift
│ ├── NotchDimensionDynamicOverlayTests.swift
│ ├── NotchTranslationDynamicOverlayTests.swift
│ ├── OverlayContainerRepresentableAdaptorTests.swift
│ ├── OverlayNotchIndexMapperTests.swift
│ └── Utils/
│ ├── ValuePublisher.swift
│ ├── View+Measure.swift
│ ├── ViewInspector.swift
│ └── ViewRenderer.swift
└── fastlane/
├── Fastfile
└── Pluginfile
Condensed preview — 69 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (163K chars).
[
{
"path": ".github/workflows/ci.yml",
"chars": 473,
"preview": "name: CI\n\non: [push]\n\njobs:\n build:\n runs-on: macOS-latest\n\n steps:\n - uses: actions/checkout@v2\n - nam"
},
{
"path": ".github/workflows/release.yml",
"chars": 908,
"preview": "\nname: Release\n\non:\n workflow_dispatch:\n inputs:\n name:\n description: 'Version name'\n "
},
{
"path": ".gitignore",
"chars": 774,
"preview": "# OS X\n.DS_Store\n\n# Xcode\n\n## Build generated\nDerivedData/\n\n## Various settings\n*.pbxuser\n!default.pbxuser\n*.mode1v3\n!de"
},
{
"path": "CHANGELOG.md",
"chars": 244,
"preview": "## [1.0.2]\n\n### Fixed\n\n- Jumping animation issue. #42\n\n## [1.0.0]\n\n### Fixed\n\n- Overlay with multiple scroll views #16\n\n"
},
{
"path": "DynamicOverlay.podspec",
"chars": 1209,
"preview": "#\n# Be sure to run `pod lib lint DynamicOverlay.podspec' to ensure this is a\n# valid spec before submitting.\n#\n# Any lin"
},
{
"path": "DynamicOverlay_Example/DynamicOverlay_Example/Classes/MapApp.swift",
"chars": 426,
"preview": "//\n// MapApp.swift\n// DynamicOverlay_Example\n//\n// Created by Gaétan Zanella on 05/03/2019.\n// Copyright © 2019 Fabe"
},
{
"path": "DynamicOverlay_Example/DynamicOverlay_Example/Configuration/Info.plist",
"chars": 1407,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
},
{
"path": "DynamicOverlay_Example/DynamicOverlay_Example/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json",
"chars": 1590,
"preview": "{\n \"images\" : [\n {\n \"idiom\" : \"iphone\",\n \"size\" : \"20x20\",\n \"scale\" : \"2x\"\n },\n {\n \"idiom\""
},
{
"path": "DynamicOverlay_Example/DynamicOverlay_Example/Resources/Assets.xcassets/Contents.json",
"chars": 62,
"preview": "{\n \"info\" : {\n \"version\" : 1,\n \"author\" : \"xcode\"\n }\n}"
},
{
"path": "DynamicOverlay_Example/DynamicOverlay_Example/Resources/Base.lproj/LaunchScreen.storyboard",
"chars": 1784,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<document type=\"com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB\" version=\"3"
},
{
"path": "DynamicOverlay_Example/DynamicOverlay_Example/UIKit/UIKitAppDelegate.swift",
"chars": 525,
"preview": "//\n// UIKitAppDelegate.swift\n// DynamicOverlay_Example\n//\n// Created by Gaétan Zanella on 18/04/2021.\n// Copyright ©"
},
{
"path": "DynamicOverlay_Example/DynamicOverlay_Example/View/ActionCell.swift",
"chars": 301,
"preview": "//\n// ActionCell.swift\n// DynamicOverlay_Example\n//\n// Created by Gaétan Zanella on 19/04/2021.\n// Copyright © 2021 "
},
{
"path": "DynamicOverlay_Example/DynamicOverlay_Example/View/BackdropView.swift",
"chars": 289,
"preview": "//\n// Backdropview.swift\n// DynamicOverlay_Example\n//\n// Created by Gaétan Zanella on 19/04/2021.\n// Copyright © 202"
},
{
"path": "DynamicOverlay_Example/DynamicOverlay_Example/View/FavoriteCell.swift",
"chars": 577,
"preview": "//\n// FavoriteCell.swift\n// DynamicOverlay_Example\n//\n// Created by Gaétan Zanella on 19/04/2021.\n// Copyright © 202"
},
{
"path": "DynamicOverlay_Example/DynamicOverlay_Example/View/MapRootView.swift",
"chars": 1732,
"preview": "//\n// MapRootView.swift\n// DynamicOverlay_Example\n//\n// Created by Gaétan Zanella on 17/04/2021.\n// Copyright © 2021"
},
{
"path": "DynamicOverlay_Example/DynamicOverlay_Example/View/MapView.swift",
"chars": 504,
"preview": "//\n// MapView.swift\n// DynamicOverlay_Example\n//\n// Created by Gaétan Zanella on 17/04/2021.\n// Copyright © 2021 Fab"
},
{
"path": "DynamicOverlay_Example/DynamicOverlay_Example/View/OverlayBackgroundView.swift",
"chars": 993,
"preview": "//\n// OverlayBackgroundView.swift\n// DynamicOverlay_Example\n//\n// Created by Gaétan Zanella on 19/04/2021.\n// Copyri"
},
{
"path": "DynamicOverlay_Example/DynamicOverlay_Example/View/OverlayView.swift",
"chars": 1453,
"preview": "//\n// OverlayView.swift\n// DynamicOverlay_Example\n//\n// Created by Gaétan Zanella on 17/04/2021.\n// Copyright © 2021"
},
{
"path": "DynamicOverlay_Example/DynamicOverlay_Example/View/SearchBar.swift",
"chars": 1689,
"preview": "//\n// SearchBar.swift\n// DynamicOverlay_Example\n//\n// Created by Gaétan Zanella on 18/04/2021.\n// Copyright © 2021 F"
},
{
"path": "DynamicOverlay_Example/DynamicOverlay_Example.xcodeproj/project.pbxproj",
"chars": 30330,
"preview": "// !$*UTF8*$!\n{\n\tarchiveVersion = 1;\n\tclasses = {\n\t};\n\tobjectVersion = 52;\n\tobjects = {\n\n/* Begin PBXBuildFile section *"
},
{
"path": "DynamicOverlay_Example/DynamicOverlay_Example.xcodeproj/project.xcworkspace/contents.xcworkspacedata",
"chars": 135,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Workspace\n version = \"1.0\">\n <FileRef\n location = \"self:\">\n </FileRef"
},
{
"path": "DynamicOverlay_Example/DynamicOverlay_Example.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist",
"chars": 238,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
},
{
"path": "DynamicOverlay_Example/DynamicOverlay_Example.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved",
"chars": 341,
"preview": "{\n \"object\": {\n \"pins\": [\n {\n \"package\": \"OverlayContainer\",\n \"repositoryURL\": \"https://github.co"
},
{
"path": "DynamicOverlay_Example/DynamicOverlay_Example.xcodeproj/xcshareddata/xcschemes/DynamicOverlay_Example.xcscheme",
"chars": 3450,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Scheme\n LastUpgradeVersion = \"1400\"\n version = \"1.3\">\n <BuildAction\n "
},
{
"path": "Gemfile",
"chars": 214,
"preview": "source 'https://rubygems.org'\n\ngem \"cocoapods\", \"~> 1.11\"\ngem \"fastlane\", \"~> 2.1\"\nplugins_path = File.join(File.dirname"
},
{
"path": "LICENSE",
"chars": 1054,
"preview": "Copyright (c) 2020 Fabernovel\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this soft"
},
{
"path": "Package.resolved",
"chars": 341,
"preview": "{\n \"object\": {\n \"pins\": [\n {\n \"package\": \"OverlayContainer\",\n \"repositoryURL\": \"https://github.co"
},
{
"path": "Package.swift",
"chars": 737,
"preview": "// swift-tools-version:5.1\nimport PackageDescription\n\nlet package = Package(\n name: \"DynamicOverlay\",\n platforms: "
},
{
"path": "Package.xcconfig",
"chars": 562,
"preview": "IPHONEOS_DEPLOYMENT_TARGET = 10.0\n\nSDKROOT =\nSUPPORTED_PLATFORMS = iphoneos iphonesimulator\nTARGETED_DEVICE_FAMILY = 1,2"
},
{
"path": "README.md",
"chars": 9324,
"preview": "# DynamicOverlay\n\n<H4 align=\"center\">\nDynamicOverlay is a SwiftUI library. It makes easier to develop overlay based inte"
},
{
"path": "Source/DynamicOverlay.h",
"chars": 535,
"preview": "//\n// DynamicOverlay.h\n// DynamicOverlay\n//\n// Created by Gaétan Zanella on 05/03/2019.\n// Copyright © 2019 Fabernov"
},
{
"path": "Source/Info.plist",
"chars": 704,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
},
{
"path": "Source/Internal/DynamicOverlayBehaviorValue.swift",
"chars": 1673,
"preview": "//\n// EmptyFile.swift\n// DynamicOverlay\n//\n// Created by Gaétan Zanella on 05/03/2019.\n// Copyright © 2019 Fabernove"
},
{
"path": "Source/Internal/DynamicOverlayNotchTransition+DynamicOverlayBehavior.swift",
"chars": 1557,
"preview": "//\n// MagneticNotchOverlayBehavior+DynamicOverlayBehavior.swift\n// DynamicOverlay\n//\n// Created by Gaétan Zanella on "
},
{
"path": "Source/Internal/Handle/ActivatedOverlayArea.swift",
"chars": 1443,
"preview": "//\n// ActivatedOverlayArea.swift\n// DynamicOverlay\n//\n// Created by Gaétan Zanella on 04/12/2020.\n// Copyright © 202"
},
{
"path": "Source/Internal/Handle/ActiveOverlayAreaViewModifier.swift",
"chars": 661,
"preview": "//\n// ActiveOverlayAreaViewModifier.swift\n// DynamicOverlay\n//\n// Created by Gaétan Zanella on 28/12/2020.\n// Copyri"
},
{
"path": "Source/Internal/Handle/Drag/DynamicOverlayDragArea.swift",
"chars": 958,
"preview": "//\n// DynamicOverlayDragArea.swift\n// DynamicOverlay\n//\n// Created by Gaétan Zanella on 29/01/2022.\n// Copyright © 2"
},
{
"path": "Source/Internal/Handle/Drag/OnDragAreaChangeViewModifier.swift",
"chars": 712,
"preview": "//\n// OnDragAreaChangeViewModifier.swift\n// DynamicOverlayTests\n//\n// Created by Gaétan Zanella on 16/04/2021.\n// Co"
},
{
"path": "Source/Internal/Handle/DrivingScrollView/DynamicOverlayScrollViewProxy.swift",
"chars": 1455,
"preview": "//\n// DynamicOverlayScrollViewProxyPreferenceKey.swift\n// DynamicOverlay\n//\n// Created by Gaétan Zanella on 11/01/202"
},
{
"path": "Source/Internal/Handle/DrivingScrollView/OnDrivingScrollViewChangeViewModifier.swift",
"chars": 770,
"preview": "//\n// OnDrivingScrollViewChangeViewModifier.swift\n// DynamicOverlayTests\n//\n// Created by Gaétan Zanella on 16/04/202"
},
{
"path": "Source/Internal/Handle/OverlayContainerCoordinateSpace.swift",
"chars": 595,
"preview": "//\n// OverlayContainer+CoordinateSpace.swift\n// DynamicOverlay\n//\n// Created by Gaétan Zanella on 28/12/2020.\n// Cop"
},
{
"path": "Source/Internal/MagneticNotchOverlayBehaviorValue.swift",
"chars": 1997,
"preview": "//\n// MagneticNotchOverlayBehaviorValue.swift\n// DynamicOverlay\n//\n// Created by Gaétan Zanella on 02/12/2020.\n// Co"
},
{
"path": "Source/Internal/OverlayContainer/DynamicOverlayContainerAnimationController.swift",
"chars": 3527,
"preview": "//\n// DynamicOverlayContainerAnimationController.swift\n// DynamicOverlay\n//\n// Created by Gaétan Zanella on 28/12/202"
},
{
"path": "Source/Internal/OverlayContainer/OverlayContainerCoordinator.swift",
"chars": 7995,
"preview": "//\n// OverlayContainerCoordinator.swift\n// DynamicOverlay\n//\n// Created by Gaétan Zanella on 02/12/2020.\n// Copyrigh"
},
{
"path": "Source/Internal/OverlayContainer/OverlayContainerDynamicOverlayView.swift",
"chars": 2285,
"preview": "//\n// OverlayContainerView.swift\n// DynamicOverlay\n//\n// Created by Gaétan Zanella on 02/12/2020.\n// Copyright © 202"
},
{
"path": "Source/Internal/OverlayContainer/OverlayContainerRepresentableAdaptor.swift",
"chars": 2036,
"preview": "//\n// OverlayContainerRepresentableAdaptor.swift\n// DynamicOverlayTests\n//\n// Created by Gaétan Zanella on 20/04/2021"
},
{
"path": "Source/Internal/OverlayContainer/OverlayContainerStateDiffer.swift",
"chars": 986,
"preview": "//\n// OverlayContainerStateDiffer.swift\n// DynamicOverlay\n//\n// Created by Gaétan Zanella on 23/07/2021.\n// Copyrigh"
},
{
"path": "Source/Internal/OverlayContainer/SwiftUIOverlayContainerRepresentableAdaptor.swift",
"chars": 1224,
"preview": "//\n// SwiftUIOverlayContainerRepresentableAdaptor.swift\n// DynamicOverlayTests\n//\n// Created by Gaétan Zanella on 16/"
},
{
"path": "Source/Internal/OverlayContentModifier.swift",
"chars": 1230,
"preview": "//\n// OverlayContentModifier.swift\n// DynamicOverlay\n//\n// Created by Gaétan Zanella on 03/12/2020.\n// Copyright © 2"
},
{
"path": "Source/Internal/OverlayNotchIndexMapper.swift",
"chars": 1884,
"preview": "//\n// OverlayNotchIndexMapper.swift\n// DynamicOverlay\n//\n// Created by Gaétan Zanella on 29/12/2020.\n// Copyright © "
},
{
"path": "Source/Internal/Utils/Binding+CaseIterable.swift",
"chars": 977,
"preview": "//\n// Binding+CaseIterable.swift\n// DynamicOverlay\n//\n// Created by Gaétan Zanella on 02/12/2020.\n// Copyright © 202"
},
{
"path": "Source/Public/DynamicOverlayBehavior.swift",
"chars": 334,
"preview": "//\n// DynamicOverlayBehavior.swift\n// DynamicOverlay\n//\n// Created by Gaétan Zanella on 02/12/2020.\n// Copyright © 2"
},
{
"path": "Source/Public/DynamicOverlayHandle.swift",
"chars": 1115,
"preview": "//\n// DynamicOverlayHandle.swift\n// DynamicOverlay\n//\n// Created by Gaétan Zanella on 04/12/2020.\n// Copyright © 202"
},
{
"path": "Source/Public/DynamicOverlayModifier.swift",
"chars": 1613,
"preview": "//\n// DynamicOverlayModifier.swift\n// DynamicOverlay\n//\n// Created by Gaétan Zanella on 01/12/2020.\n// Copyright © 2"
},
{
"path": "Source/Public/MagneticNotchOverlayBehavior.swift",
"chars": 3830,
"preview": "//\n// MagneticNotchOverlayBehavior.swift\n// DynamicOverlay\n//\n// Created by Gaétan Zanella on 02/12/2020.\n// Copyrig"
},
{
"path": "Tests/DynamicOverlayTests/DragHandleViewModifierTests.swift",
"chars": 2331,
"preview": "//\n// DragHandleTests.swift\n// DynamicOverlayTests\n//\n// Created by Gaétan Zanella on 16/04/2021.\n// Copyright © 202"
},
{
"path": "Tests/DynamicOverlayTests/DrivingScrollViewModifierTests.swift",
"chars": 3194,
"preview": "//\n// DrivingScrollViewModifierTests.swift\n// DynamicOverlayTests\n//\n// Created by Gaétan Zanella on 16/04/2021.\n// "
},
{
"path": "Tests/DynamicOverlayTests/MagneticNotchOverlayBehaviorValueTests.swift",
"chars": 8191,
"preview": "//\n// MagneticNotchOverlayBehaviorValueTests.swift\n// DynamicOverlayTests\n//\n// Created by Gaétan Zanella on 16/04/20"
},
{
"path": "Tests/DynamicOverlayTests/NotchBindingDynamicOverlayTests.swift",
"chars": 2926,
"preview": "//\n// NotchBindingDynamicOverlayTests.swift\n// DynamicOverlay\n//\n// Created by Gaétan Zanella on 10/04/2021.\n// Copy"
},
{
"path": "Tests/DynamicOverlayTests/NotchDimensionDynamicOverlayTests.swift",
"chars": 2064,
"preview": "//\n// NotchDimensionDynamicOverlayTests.swift\n// DynamicOverlayTests\n//\n// Created by Gaétan Zanella on 16/04/2021.\n/"
},
{
"path": "Tests/DynamicOverlayTests/NotchTranslationDynamicOverlayTests.swift",
"chars": 7352,
"preview": "//\n// NotchTranslationDynamicOverlayTests.swift\n// DynamicOverlay\n//\n// Created by Gaétan Zanella on 15/04/2021.\n// "
},
{
"path": "Tests/DynamicOverlayTests/OverlayContainerRepresentableAdaptorTests.swift",
"chars": 8902,
"preview": "//\n// OverlayContainerRepresentableAdaptorTests.swift\n// DynamicOverlayTests\n//\n// Created by Gaétan Zanella on 20/04"
},
{
"path": "Tests/DynamicOverlayTests/OverlayNotchIndexMapperTests.swift",
"chars": 2665,
"preview": "//\n// OverlayNotchIndexMapperTests.swift\n// DynamicOverlay\n//\n// Created by Gaétan Zanella on 29/12/2020.\n// Copyrig"
},
{
"path": "Tests/DynamicOverlayTests/Utils/ValuePublisher.swift",
"chars": 372,
"preview": "//\n// ValuePublisher.swift\n// DynamicOverlay\n//\n// Created by Gaétan Zanella on 15/04/2021.\n// Copyright © 2021 Fabe"
},
{
"path": "Tests/DynamicOverlayTests/Utils/View+Measure.swift",
"chars": 994,
"preview": "//\n// View+OnHeightCHange.swift\n// DynamicOverlay\n//\n// Created by Gaétan Zanella on 15/04/2021.\n// Copyright © 2021"
},
{
"path": "Tests/DynamicOverlayTests/Utils/ViewInspector.swift",
"chars": 727,
"preview": "//\n// ViewInspector.swift\n// DynamicOverlayTests\n//\n// Created by Gaétan Zanella on 16/04/2021.\n// Copyright © 2021 "
},
{
"path": "Tests/DynamicOverlayTests/Utils/ViewRenderer.swift",
"chars": 887,
"preview": "//\n// ViewRenderer.swift\n// DynamicOverlay\n//\n// Created by Gaétan Zanella on 12/04/2021.\n// Copyright © 2021 Fabern"
},
{
"path": "fastlane/Fastfile",
"chars": 2521,
"preview": "\ndefault_platform(:ios)\n\nplatform :ios do\n desc \"Run all unit tests\"\n lane :tests do\n scan(\n sch"
},
{
"path": "fastlane/Pluginfile",
"chars": 115,
"preview": "# Autogenerated by fastlane\n#\n# Ensure this file is checked in to source control!\n\ngem 'fastlane-plugin-changelog'\n"
}
]
About this extraction
This page contains the full source code of the faberNovel/DynamicOverlay GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 69 files (145.5 KB), approximately 38.3k tokens. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.