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 ================================================ CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType APPL CFBundleShortVersionString 1.0 CFBundleVersion 1 LSRequiresIPhoneOS UILaunchStoryboardName LaunchScreen UIRequiredDeviceCapabilities armv7 UISupportedInterfaceOrientations UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UISupportedInterfaceOrientations~ipad UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight ================================================ 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 ================================================ ================================================ 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 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 = ""; }; E73A7CE8262B2A8400959344 /* MapView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapView.swift; sourceTree = ""; }; E73A7CEB262B2AA400959344 /* MapRootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapRootView.swift; sourceTree = ""; }; 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 = ""; }; E750EE4D262B2B4800E79C6B /* OverlayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverlayView.swift; sourceTree = ""; }; E750EE50262C30F600E79C6B /* SearchBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchBar.swift; sourceTree = ""; }; E750EE67262C69A900E79C6B /* UIKitAppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIKitAppDelegate.swift; sourceTree = ""; }; E750EE95262D772700E79C6B /* OverlayBackgroundView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverlayBackgroundView.swift; sourceTree = ""; }; E750EE97262D798F00E79C6B /* ActionCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionCell.swift; sourceTree = ""; }; E750EE99262D799B00E79C6B /* FavoriteCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoriteCell.swift; sourceTree = ""; }; E750EE9B262D7C9000E79C6B /* BackdropView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackdropView.swift; sourceTree = ""; }; 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 = ""; }; E7691576222EA78B00FDEE7F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; E7691578222EA78B00FDEE7F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 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 = ""; }; E7970599292F83100047839F /* NotchDimensionDynamicOverlayTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = NotchDimensionDynamicOverlayTests.swift; path = ../../Tests/DynamicOverlayTests/NotchDimensionDynamicOverlayTests.swift; sourceTree = ""; }; E797059A292F83100047839F /* OverlayNotchIndexMapperTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = OverlayNotchIndexMapperTests.swift; path = ../../Tests/DynamicOverlayTests/OverlayNotchIndexMapperTests.swift; sourceTree = ""; }; E797059B292F83100047839F /* DrivingScrollViewModifierTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = DrivingScrollViewModifierTests.swift; path = ../../Tests/DynamicOverlayTests/DrivingScrollViewModifierTests.swift; sourceTree = ""; }; E797059C292F83100047839F /* DragHandleViewModifierTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = DragHandleViewModifierTests.swift; path = ../../Tests/DynamicOverlayTests/DragHandleViewModifierTests.swift; sourceTree = ""; }; E797059D292F83100047839F /* NotchTranslationDynamicOverlayTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = NotchTranslationDynamicOverlayTests.swift; path = ../../Tests/DynamicOverlayTests/NotchTranslationDynamicOverlayTests.swift; sourceTree = ""; }; E797059E292F83100047839F /* MagneticNotchOverlayBehaviorValueTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = MagneticNotchOverlayBehaviorValueTests.swift; path = ../../Tests/DynamicOverlayTests/MagneticNotchOverlayBehaviorValueTests.swift; sourceTree = ""; }; E797059F292F83100047839F /* NotchBindingDynamicOverlayTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = NotchBindingDynamicOverlayTests.swift; path = ../../Tests/DynamicOverlayTests/NotchBindingDynamicOverlayTests.swift; sourceTree = ""; }; E79705A8292F83190047839F /* ViewInspector.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ViewInspector.swift; path = ../../Tests/DynamicOverlayTests/Utils/ViewInspector.swift; sourceTree = ""; }; E79705A9292F83190047839F /* ValuePublisher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ValuePublisher.swift; path = ../../Tests/DynamicOverlayTests/Utils/ValuePublisher.swift; sourceTree = ""; }; E79705AA292F83190047839F /* View+Measure.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "View+Measure.swift"; path = "../../Tests/DynamicOverlayTests/Utils/View+Measure.swift"; sourceTree = ""; }; E79705AB292F83190047839F /* ViewRenderer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ViewRenderer.swift; path = ../../Tests/DynamicOverlayTests/Utils/ViewRenderer.swift; sourceTree = ""; }; /* 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 = ""; }; 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 = ""; }; E741EE712576B10D0073FF6B /* Frameworks */ = { isa = PBXGroup; children = ( E741EE722576B10D0073FF6B /* DynamicOverlay.framework */, ); name = Frameworks; sourceTree = ""; }; E750EE66262C699C00E79C6B /* UIKit */ = { isa = PBXGroup; children = ( E750EE67262C69A900E79C6B /* UIKitAppDelegate.swift */, ); path = UIKit; sourceTree = ""; }; E7691560222EA78A00FDEE7F = { isa = PBXGroup; children = ( E739AD91291D45B10076B2AC /* Packages */, E769156B222EA78A00FDEE7F /* DynamicOverlay_Example */, E7970590292F817F0047839F /* DynamicOverlay_ExampleTests */, E769156A222EA78A00FDEE7F /* Products */, E741EE712576B10D0073FF6B /* Frameworks */, ); sourceTree = ""; }; E769156A222EA78A00FDEE7F /* Products */ = { isa = PBXGroup; children = ( E7691569222EA78A00FDEE7F /* DynamicOverlay_Example.app */, E797058F292F817F0047839F /* DynamicOverlay_ExampleTests.xctest */, ); name = Products; sourceTree = ""; }; E769156B222EA78A00FDEE7F /* DynamicOverlay_Example */ = { isa = PBXGroup; children = ( E742E3C422302B4B002A2BED /* MapApp.swift */, E750EE66262C699C00E79C6B /* UIKit */, E73A7CEA262B2A8B00959344 /* View */, E7691582222EA7C100FDEE7F /* Resources */, E7691581222EA7B400FDEE7F /* Configuration */, ); path = DynamicOverlay_Example; sourceTree = ""; }; E7691581222EA7B400FDEE7F /* Configuration */ = { isa = PBXGroup; children = ( E7691578222EA78B00FDEE7F /* Info.plist */, ); path = Configuration; sourceTree = ""; }; E7691582222EA7C100FDEE7F /* Resources */ = { isa = PBXGroup; children = ( E7691575222EA78B00FDEE7F /* LaunchScreen.storyboard */, E7691573222EA78B00FDEE7F /* Assets.xcassets */, ); path = Resources; sourceTree = ""; }; 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 = ""; }; /* 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 = ""; }; /* 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 ================================================ ================================================ FILE: DynamicOverlay_Example/DynamicOverlay_Example.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist ================================================ IDEDidComputeMac32BitWarning ================================================ 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 ================================================ ================================================ 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

DynamicOverlay is a SwiftUI library. It makes easier to develop overlay based interfaces, such as the one presented in the Apple Maps, Stocks or Shortcuts apps.

Platform Swift5 CocoaPods Carthage Build Status License

--- - [Requirements](#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 | | ------------------- | ------------------ | | | | ## 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 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 { ... } } ``` 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 { ... } .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 { ... } .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 { ... } .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 { ... } .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 //! 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 ================================================ FILE: Source/Info.plist ================================================ CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType FMWK CFBundleShortVersionString 1.0.0 CFBundleVersion 13 ================================================ 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? let disabledNotchIndexes: Set init(notchDimensions: [Int: NotchDimension]? = nil, block: ((OverlayTranslation) -> Void)? = nil, binding: Binding? = nil, disabledNotchIndexes: Set = []) { 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: 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? let disabledNotches: [Notch] init(dimensions: @escaping (Notch) -> NotchDimension, translationBlocks: [(Translation) -> Void], binding: Binding?, 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) -> 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 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: 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 { 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: UIViewControllerRepresentable { let adaptor: OverlayContainerRepresentableAdaptor // 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.Context { OverlayContainerRepresentableAdaptor.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: ViewModifier { let overlay: Overlay func body(content: Content) -> some View { content.environment(\.overlayContentKey, AnyView(overlay)) } } extension View { func overlayContent(_ content: Content) -> ModifiedContent> { 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 { Binding( 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: 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: Behavior) -> some View { modifier(behavior.makeModifier()) } } public struct AddDynamicOverlayModifier: 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 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) -> 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 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 let onFrameChange: (CGRect) -> Void var body: some View { Color.red .dynamicOverlay(Color.green.onFrameChange(onFrameChange)) .dynamicOverlayBehavior(behavior) } var behavior: some DynamicOverlayBehavior { MagneticNotchOverlayBehavior { 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 { _ 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.Translation private struct TranslationView: View { @ObservedObject var target: ValuePublisher let onTranslation: (OverlayTranslation) -> Void var body: some View { Color.red .dynamicOverlay(Color.green) .dynamicOverlayBehavior(behavior) } private var behavior: MagneticNotchOverlayBehavior { 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 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 = [] 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] = [ [], [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.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: 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(_ type: V.Type) -> V { guard let result = view.search(type) else { fatalError("\(type) does not exist") } return result } } private extension UIView { func search(_ 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 { 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 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'