├── Screenshots ├── max.png └── min.png ├── fastlane ├── .env.default ├── Pluginfile └── Fastfile ├── DynamicOverlay_Example ├── DynamicOverlay_Example │ ├── Resources │ │ ├── Assets.xcassets │ │ │ ├── Contents.json │ │ │ └── AppIcon.appiconset │ │ │ │ └── Contents.json │ │ └── Base.lproj │ │ │ └── LaunchScreen.storyboard │ ├── View │ │ ├── BackdropView.swift │ │ ├── ActionCell.swift │ │ ├── MapView.swift │ │ ├── FavoriteCell.swift │ │ ├── OverlayBackgroundView.swift │ │ ├── OverlayView.swift │ │ ├── SearchBar.swift │ │ └── MapRootView.swift │ ├── Classes │ │ └── MapApp.swift │ ├── UIKit │ │ └── UIKitAppDelegate.swift │ └── Configuration │ │ └── Info.plist └── DynamicOverlay_Example.xcodeproj │ ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── swiftpm │ │ └── Package.resolved │ ├── xcshareddata │ └── xcschemes │ │ └── DynamicOverlay_Example.xcscheme │ └── project.pbxproj ├── Gemfile ├── CHANGELOG.md ├── Package.resolved ├── Source ├── Public │ ├── DynamicOverlayBehavior.swift │ ├── DynamicOverlayHandle.swift │ ├── DynamicOverlayModifier.swift │ └── MagneticNotchOverlayBehavior.swift ├── DynamicOverlay.h ├── Internal │ ├── Handle │ │ ├── OverlayContainerCoordinateSpace.swift │ │ ├── ActiveOverlayAreaViewModifier.swift │ │ ├── Drag │ │ │ ├── OnDragAreaChangeViewModifier.swift │ │ │ └── DynamicOverlayDragArea.swift │ │ ├── DrivingScrollView │ │ │ ├── OnDrivingScrollViewChangeViewModifier.swift │ │ │ └── DynamicOverlayScrollViewProxy.swift │ │ └── ActivatedOverlayArea.swift │ ├── OverlayContainer │ │ ├── OverlayContainerStateDiffer.swift │ │ ├── SwiftUIOverlayContainerRepresentableAdaptor.swift │ │ ├── OverlayContainerRepresentableAdaptor.swift │ │ ├── OverlayContainerDynamicOverlayView.swift │ │ ├── DynamicOverlayContainerAnimationController.swift │ │ └── OverlayContainerCoordinator.swift │ ├── Utils │ │ └── Binding+CaseIterable.swift │ ├── OverlayContentModifier.swift │ ├── DynamicOverlayNotchTransition+DynamicOverlayBehavior.swift │ ├── DynamicOverlayBehaviorValue.swift │ ├── OverlayNotchIndexMapper.swift │ └── MagneticNotchOverlayBehaviorValue.swift └── Info.plist ├── Tests └── DynamicOverlayTests │ ├── Utils │ ├── ValuePublisher.swift │ ├── ViewInspector.swift │ ├── ViewRenderer.swift │ └── View+Measure.swift │ ├── NotchDimensionDynamicOverlayTests.swift │ ├── DragHandleViewModifierTests.swift │ ├── OverlayNotchIndexMapperTests.swift │ ├── NotchBindingDynamicOverlayTests.swift │ ├── DrivingScrollViewModifierTests.swift │ ├── NotchTranslationDynamicOverlayTests.swift │ ├── MagneticNotchOverlayBehaviorValueTests.swift │ └── OverlayContainerRepresentableAdaptorTests.swift ├── .github └── workflows │ ├── ci.yml │ └── release.yml ├── Package.xcconfig ├── Package.swift ├── .gitignore ├── LICENSE ├── DynamicOverlay.podspec ├── Gemfile.lock └── README.md /Screenshots/max.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/faberNovel/DynamicOverlay/HEAD/Screenshots/max.png -------------------------------------------------------------------------------- /Screenshots/min.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/faberNovel/DynamicOverlay/HEAD/Screenshots/min.png -------------------------------------------------------------------------------- /fastlane/.env.default: -------------------------------------------------------------------------------- 1 | SCHEME = "DynamicOverlay" 2 | PODSPEC = "DynamicOverlay.podspec" 3 | XCCONFIG = "Package.xcconfig" 4 | -------------------------------------------------------------------------------- /fastlane/Pluginfile: -------------------------------------------------------------------------------- 1 | # Autogenerated by fastlane 2 | # 3 | # Ensure this file is checked in to source control! 4 | 5 | gem 'fastlane-plugin-changelog' 6 | -------------------------------------------------------------------------------- /DynamicOverlay_Example/DynamicOverlay_Example/Resources/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem "cocoapods", "~> 1.11" 4 | gem "fastlane", "~> 2.1" 5 | plugins_path = File.join(File.dirname(__FILE__), 'fastlane', 'Pluginfile') 6 | eval_gemfile(plugins_path) if File.exist?(plugins_path) 7 | -------------------------------------------------------------------------------- /DynamicOverlay_Example/DynamicOverlay_Example.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [1.0.2] 2 | 3 | ### Fixed 4 | 5 | - Jumping animation issue. #42 6 | 7 | ## [1.0.0] 8 | 9 | ### Fixed 10 | 11 | - Overlay with multiple scroll views #16 12 | 13 | ## [1.0.0-beta.10] 14 | 15 | ### Fixed 16 | 17 | - Crash with drivingScrollView a List and a condition #21 18 | - Bump OverlayContainer to `3.5.2` 19 | -------------------------------------------------------------------------------- /DynamicOverlay_Example/DynamicOverlay_Example.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "OverlayContainer", 6 | "repositoryURL": "https://github.com/applidium/OverlayContainer.git", 7 | "state": { 8 | "branch": null, 9 | "revision": "f1c8fc38bb1ad9a810397f1d06f7026a35e0760c", 10 | "version": "3.5.2" 11 | } 12 | } 13 | ] 14 | }, 15 | "version": 1 16 | } 17 | -------------------------------------------------------------------------------- /DynamicOverlay_Example/DynamicOverlay_Example/View/BackdropView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Backdropview.swift 3 | // DynamicOverlay_Example 4 | // 5 | // Created by Gaétan Zanella on 19/04/2021. 6 | // Copyright © 2021 Fabernovel. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import SwiftUI 11 | 12 | struct BackdropView: View { 13 | 14 | var body: some View { 15 | Color.black.opacity(0.3) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Source/Public/DynamicOverlayBehavior.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DynamicOverlayBehavior.swift 3 | // DynamicOverlay 4 | // 5 | // Created by Gaétan Zanella on 02/12/2020. 6 | // Copyright © 2020 Fabernovel. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | /// A protocol that describes the overlay behavior. 12 | public protocol DynamicOverlayBehavior { 13 | 14 | func makeModifier() -> AddDynamicOverlayBehaviorModifier 15 | } 16 | -------------------------------------------------------------------------------- /DynamicOverlay_Example/DynamicOverlay_Example/View/ActionCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ActionCell.swift 3 | // DynamicOverlay_Example 4 | // 5 | // Created by Gaétan Zanella on 19/04/2021. 6 | // Copyright © 2021 Fabernovel. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import SwiftUI 11 | 12 | struct ActionCell: View { 13 | 14 | var body: some View { 15 | Label("New Guide…", systemImage: "plus") 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /DynamicOverlay_Example/DynamicOverlay_Example.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "OverlayContainer", 6 | "repositoryURL": "https://github.com/applidium/OverlayContainer.git", 7 | "state": { 8 | "branch": null, 9 | "revision": "f1c8fc38bb1ad9a810397f1d06f7026a35e0760c", 10 | "version": "3.5.2" 11 | } 12 | } 13 | ] 14 | }, 15 | "version": 1 16 | } 17 | -------------------------------------------------------------------------------- /Tests/DynamicOverlayTests/Utils/ValuePublisher.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ValuePublisher.swift 3 | // DynamicOverlay 4 | // 5 | // Created by Gaétan Zanella on 15/04/2021. 6 | // Copyright © 2021 Fabernovel. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class ValuePublisher: ObservableObject { 12 | 13 | @Published 14 | var value: V 15 | 16 | init(_ value: V) { 17 | self.value = value 18 | } 19 | 20 | func update(_ value: V) { 21 | self.value = value 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /DynamicOverlay_Example/DynamicOverlay_Example/Classes/MapApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MapApp.swift 3 | // DynamicOverlay_Example 4 | // 5 | // Created by Gaétan Zanella on 05/03/2019. 6 | // Copyright © 2019 Fabernovel. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import SwiftUI 11 | import DynamicOverlay 12 | 13 | @main 14 | struct MapApp: App { 15 | 16 | @UIApplicationDelegateAdaptor(UIKitAppDelegate.self) 17 | private var delegate: UIKitAppDelegate 18 | 19 | var body: some Scene { 20 | WindowGroup { 21 | MapRootView() 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | runs-on: macOS-latest 8 | 9 | steps: 10 | - uses: actions/checkout@v2 11 | - name: Bundle install 12 | working-directory: ./ 13 | run: bundle install 14 | 15 | - name: Unit tests 16 | run: bundle exec fastlane tests 17 | 18 | - name: SPM lint 19 | run: bundle exec fastlane spm_lint 20 | 21 | - name: Carthage lint 22 | run: bundle exec fastlane carthage_lint 23 | 24 | - name: Pod lint 25 | run: bundle exec fastlane pod_lint 26 | -------------------------------------------------------------------------------- /Source/DynamicOverlay.h: -------------------------------------------------------------------------------- 1 | // 2 | // DynamicOverlay.h 3 | // DynamicOverlay 4 | // 5 | // Created by Gaétan Zanella on 05/03/2019. 6 | // Copyright © 2019 Fabernovel. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | //! Project version number for DynamicOverlay. 12 | FOUNDATION_EXPORT double DynamicOverlayVersionNumber; 13 | 14 | //! Project version string for DynamicOverlay. 15 | FOUNDATION_EXPORT const unsigned char DynamicOverlayVersionString[]; 16 | 17 | // In this header, you should import all the public headers of your framework using statements like #import 18 | 19 | 20 | -------------------------------------------------------------------------------- /Package.xcconfig: -------------------------------------------------------------------------------- 1 | IPHONEOS_DEPLOYMENT_TARGET = 10.0 2 | 3 | SDKROOT = 4 | SUPPORTED_PLATFORMS = iphoneos iphonesimulator 5 | TARGETED_DEVICE_FAMILY = 1,2 6 | VALID_ARCHS[sdk=iphoneos*] = arm64 armv7 armv7s 7 | VALID_ARCHS[sdk=iphonesimulator*] = i386 x86_64 8 | 9 | CODE_SIGN_IDENTITY = 10 | CODE_SIGN_STYLE = Manual 11 | INSTALL_PATH = $(LOCAL_LIBRARY_DIR)/Frameworks 12 | SKIP_INSTALL = YES 13 | DYLIB_COMPATIBILITY_VERSION = 1 14 | DYLIB_CURRENT_VERSION = 1 15 | DYLIB_INSTALL_NAME_BASE = @rpath 16 | LD_RUNPATH_SEARCH_PATHS = $(inherited) @executable_path/../Frameworks @loader_path/Frameworks @loader_path/../Frameworks 17 | DEFINES_MODULE = NO 18 | -------------------------------------------------------------------------------- /DynamicOverlay_Example/DynamicOverlay_Example/UIKit/UIKitAppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIKitAppDelegate.swift 3 | // DynamicOverlay_Example 4 | // 5 | // Created by Gaétan Zanella on 18/04/2021. 6 | // Copyright © 2021 Fabernovel. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | 12 | class UIKitAppDelegate: NSObject, UIApplicationDelegate { 13 | 14 | func application(_ application: UIApplication, 15 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { 16 | UITableView.appearance().backgroundColor = .systemBackground 17 | return true 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /DynamicOverlay_Example/DynamicOverlay_Example/View/MapView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MapView.swift 3 | // DynamicOverlay_Example 4 | // 5 | // Created by Gaétan Zanella on 17/04/2021. 6 | // Copyright © 2021 Fabernovel. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import MapKit 11 | import SwiftUI 12 | 13 | struct MapView: View { 14 | 15 | var body: some View { 16 | MapViewAdaptor().ignoresSafeArea() 17 | } 18 | } 19 | 20 | private struct MapViewAdaptor: UIViewRepresentable { 21 | 22 | func makeUIView(context: Context) -> MKMapView { 23 | MKMapView() 24 | } 25 | 26 | func updateUIView(_ uiView: MKMapView, context: Context) {} 27 | } 28 | -------------------------------------------------------------------------------- /DynamicOverlay_Example/DynamicOverlay_Example/View/FavoriteCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FavoriteCell.swift 3 | // DynamicOverlay_Example 4 | // 5 | // Created by Gaétan Zanella on 19/04/2021. 6 | // Copyright © 2021 Fabernovel. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import SwiftUI 11 | 12 | struct FavoriteCell: View { 13 | 14 | let imageName: String 15 | let title: String 16 | 17 | var body: some View { 18 | VStack { 19 | Circle() 20 | .foregroundColor(Color(.secondarySystemFill)) 21 | .frame(width: 70, height: 70) 22 | .overlay(Image(systemName: imageName).font(.title2).foregroundColor(.blue)) 23 | Text(title) 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Source/Internal/Handle/OverlayContainerCoordinateSpace.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OverlayContainer+CoordinateSpace.swift 3 | // DynamicOverlay 4 | // 5 | // Created by Gaétan Zanella on 28/12/2020. 6 | // Copyright © 2020 Fabernovel. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | extension CoordinateSpace { 12 | 13 | static var overlay: CoordinateSpace { 14 | .named("Overlay") 15 | } 16 | } 17 | 18 | extension View { 19 | 20 | func overlayCoordinateSpace() -> some View { 21 | modifier(OverlayCoordinateSpaceViewModifier()) 22 | } 23 | } 24 | 25 | private struct OverlayCoordinateSpaceViewModifier: ViewModifier { 26 | 27 | func body(content: Content) -> some View { 28 | content.coordinateSpace(name: "Overlay") 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Source/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0.0 19 | CFBundleVersion 20 | 13 21 | 22 | 23 | -------------------------------------------------------------------------------- /Source/Internal/Handle/ActiveOverlayAreaViewModifier.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ActiveOverlayAreaViewModifier.swift 3 | // DynamicOverlay 4 | // 5 | // Created by Gaétan Zanella on 28/12/2020. 6 | // Copyright © 2020 Fabernovel. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct ActiveOverlayAreaViewModifier: ViewModifier where Key.Value == ActivatedOverlayArea { 12 | 13 | let key: Key.Type 14 | let isActive: Bool 15 | 16 | func body(content: Content) -> some View { 17 | content.background( 18 | GeometryReader { proxy in 19 | Spacer().preference( 20 | key: key, 21 | value: isActive ? .active(proxy.frame(in: .overlay)) : .inactive() 22 | ) 23 | } 24 | ) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.1 2 | import PackageDescription 3 | 4 | let package = Package( 5 | name: "DynamicOverlay", 6 | platforms: [ 7 | .iOS(.v13) 8 | ], 9 | products: [ 10 | .library( 11 | name: "DynamicOverlay", 12 | targets: ["DynamicOverlay"] 13 | ), 14 | ], 15 | dependencies: [ 16 | .package(url: "https://github.com/applidium/OverlayContainer.git", from: "3.5.2") 17 | ], 18 | targets: [ 19 | .target( 20 | name: "DynamicOverlay", 21 | dependencies: ["OverlayContainer"], 22 | path: "Source" 23 | ), 24 | .testTarget( 25 | name: "DynamicOverlayTests", 26 | dependencies: [ 27 | "DynamicOverlay", 28 | ] 29 | ) 30 | ], 31 | swiftLanguageVersions: [.v5] 32 | ) 33 | -------------------------------------------------------------------------------- /Source/Internal/Handle/Drag/OnDragAreaChangeViewModifier.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OnDragAreaChangeViewModifier.swift 3 | // DynamicOverlayTests 4 | // 5 | // Created by Gaétan Zanella on 16/04/2021. 6 | // Copyright © 2021 Fabernovel. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import SwiftUI 11 | 12 | private struct OnDragAreaChangeViewModifier: ViewModifier { 13 | 14 | let handler: (DynamicOverlayDragArea) -> Void 15 | 16 | func body(content: Content) -> some View { 17 | content.onPreferenceChange(DynamicOverlayDragAreaPreferenceKey.self) { area in 18 | handler(DynamicOverlayDragArea(area: area)) 19 | } 20 | } 21 | } 22 | 23 | extension View { 24 | 25 | func onDragAreaChange(handler: @escaping (DynamicOverlayDragArea) -> Void) -> some View { 26 | modifier(OnDragAreaChangeViewModifier(handler: handler)) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Tests/DynamicOverlayTests/Utils/ViewInspector.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewInspector.swift 3 | // DynamicOverlayTests 4 | // 5 | // Created by Gaétan Zanella on 16/04/2021. 6 | // Copyright © 2021 Fabernovel. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | struct ViewInspector { 12 | 13 | let view: UIView 14 | 15 | func search(_ type: V.Type) -> V { 16 | guard let result = view.search(type) else { 17 | fatalError("\(type) does not exist") 18 | } 19 | return result 20 | } 21 | } 22 | 23 | private extension UIView { 24 | 25 | func search(_ type: V.Type) -> V? { 26 | if let result = self as? V { 27 | return result 28 | } 29 | for subview in subviews { 30 | if let target = subview.search(type) { 31 | return target 32 | } 33 | } 34 | return nil 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Source/Internal/Handle/DrivingScrollView/OnDrivingScrollViewChangeViewModifier.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OnDrivingScrollViewChangeViewModifier.swift 3 | // DynamicOverlayTests 4 | // 5 | // Created by Gaétan Zanella on 16/04/2021. 6 | // Copyright © 2021 Fabernovel. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | private struct OnDrivingScrollViewChangeViewModifier: ViewModifier { 12 | 13 | let handler: (DynamicOverlayScrollViewProxy) -> Void 14 | 15 | func body(content: Content) -> some View { 16 | content.onPreferenceChange(DynamicOverlayScrollViewProxyPreferenceKey.self, perform: { value in 17 | handler(DynamicOverlayScrollViewProxy(area: value)) 18 | }) 19 | } 20 | } 21 | 22 | extension View { 23 | 24 | func onDrivingScrollViewChange(handler: @escaping (DynamicOverlayScrollViewProxy) -> Void) -> some View { 25 | modifier(OnDrivingScrollViewChangeViewModifier(handler: handler)) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OS X 2 | .DS_Store 3 | 4 | # Xcode 5 | 6 | ## Build generated 7 | DerivedData/ 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata/ 19 | 20 | *.xccheckout 21 | profile 22 | ## Other 23 | *.moved-aside 24 | *.xccheckout 25 | DerivedData 26 | *.xcscmblueprint 27 | 28 | ## Obj-C/Swift specific 29 | *.hmap 30 | *.ipa 31 | *.dSYM.zip 32 | *.dSYM 33 | !Carthage/Build/**/*.dSYM 34 | 35 | # Bundler 36 | .bundle 37 | 38 | # Swift Package Manager 39 | .build 40 | .swiftpm 41 | 42 | Example/Pods/ 43 | Pods/ 44 | 45 | .idea/ 46 | 47 | # fastlane specific 48 | fastlane/report.xml 49 | 50 | # deliver temporary files 51 | fastlane/Preview.html 52 | 53 | # snapshot generated screenshots 54 | fastlane/screenshots/**/*.png 55 | fastlane/screenshots/screenshots.html 56 | 57 | # scan temporary files 58 | fastlane/test_output 59 | test_output 60 | fastlane/.env 61 | pre-change.yml 62 | .build 63 | fastlane/README.md 64 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | 2 | name: Release 3 | 4 | on: 5 | workflow_dispatch: 6 | inputs: 7 | name: 8 | description: 'Version name' 9 | required: true 10 | 11 | jobs: 12 | build: 13 | runs-on: macOS-latest 14 | steps: 15 | - uses: maxim-lobanov/setup-xcode@v1 16 | with: 17 | xcode-version: latest-stable 18 | 19 | - name: Checkout 20 | uses: actions/checkout@v2 21 | 22 | - name: Bundle install 23 | working-directory: ./ 24 | run: bundle install 25 | 26 | - name: Release 27 | env: 28 | LC_ALL: en_US.UTF-8 29 | LANG: en_US.UTF-8 30 | GITHUB_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN_CI }} 31 | COCOAPODS_TRUNK_TOKEN: ${{ secrets.COCOAPODS_GZ_TOKEN }} 32 | GIT_COMMITTER_NAME: Bot Fabernovel 33 | GIT_AUTHOR_NAME: Bot Fabernovel 34 | GIT_COMMITTER_EMAIL: ci@fabernovel.com 35 | GIT_AUTHOR_EMAIL: ci@fabernovel.com 36 | run: bundle exec fastlane release version:${{ github.event.inputs.name }} bypass_confirmations:true 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 Fabernovel 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /Tests/DynamicOverlayTests/Utils/ViewRenderer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewRenderer.swift 3 | // DynamicOverlay 4 | // 5 | // Created by Gaétan Zanella on 12/04/2021. 6 | // Copyright © 2021 Fabernovel. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import SwiftUI 11 | 12 | class ViewRenderer { 13 | 14 | var size: CGSize { 15 | window.frame.size 16 | } 17 | 18 | var safeAreaInsets: UIEdgeInsets { 19 | window.safeAreaInsets 20 | } 21 | 22 | var bounds: CGRect { 23 | window.bounds 24 | } 25 | 26 | var safeBounds: CGRect { 27 | bounds.inset(by: safeAreaInsets) 28 | } 29 | 30 | var window: UIWindow { 31 | UIApplication.shared.windows.first! 32 | } 33 | 34 | private let hostController: UIHostingController 35 | 36 | init(view: V) { 37 | self.hostController = UIHostingController(rootView: view) 38 | } 39 | 40 | func render() { 41 | if window.rootViewController !== hostController { 42 | window.rootViewController = hostController 43 | } 44 | CATransaction.flush() 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Source/Internal/OverlayContainer/OverlayContainerStateDiffer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OverlayContainerStateDiffer.swift 3 | // DynamicOverlay 4 | // 5 | // Created by Gaétan Zanella on 23/07/2021. 6 | // Copyright © 2021 Fabernovel. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct OverlayContainerStateDiffer { 12 | 13 | struct Changes: OptionSet { 14 | let rawValue: Int 15 | 16 | static let layout = Changes(rawValue: 1 << 0) 17 | static let index = Changes(rawValue: 1 << 1) 18 | static let scrollView = Changes(rawValue: 1 << 2) 19 | } 20 | 21 | func diff(from previous: OverlayContainerState, to next: OverlayContainerState) -> Changes { 22 | var changes: Changes = [] 23 | if previous.notchIndex != next.notchIndex { 24 | changes.insert(.index) 25 | } 26 | // issue #21 27 | // The scroll view depends on the content, we need to first for a potential new scroll view 28 | // at each update 29 | changes.insert(.scrollView) 30 | if previous.layout != next.layout { 31 | changes.insert(.layout) 32 | } 33 | return changes 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Source/Internal/Utils/Binding+CaseIterable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Binding+CaseIterable.swift 3 | // DynamicOverlay 4 | // 5 | // Created by Gaétan Zanella on 02/12/2020. 6 | // Copyright © 2020 Fabernovel. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | extension Binding where Value: Equatable, Value: CaseIterable { 12 | 13 | func indexBinding() -> Binding { 14 | Binding( 15 | get: { 16 | Value.index(of: wrappedValue) 17 | }, 18 | set: { index in 19 | wrappedValue = Value.value(at: index) 20 | } 21 | ) 22 | } 23 | } 24 | 25 | extension CaseIterable where Self: Equatable { 26 | 27 | static func index(of target: AllCases.Element) -> Int { 28 | var index = 0 29 | for value in allCases { 30 | if value == target { 31 | return index 32 | } 33 | index += 1 34 | } 35 | fatalError("Cannot find a valid index for \(target)") 36 | } 37 | 38 | static func value(at index: Int) -> AllCases.Element { 39 | allCases[allCases.index(allCases.startIndex, offsetBy: index)] 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Tests/DynamicOverlayTests/Utils/View+Measure.swift: -------------------------------------------------------------------------------- 1 | // 2 | // View+OnHeightCHange.swift 3 | // DynamicOverlay 4 | // 5 | // Created by Gaétan Zanella on 15/04/2021. 6 | // Copyright © 2021 Fabernovel. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import SwiftUI 11 | 12 | extension View { 13 | 14 | func onHeightChange(_ block: @escaping (CGFloat) -> Void) -> some View { 15 | onFrameChange { block($0.height) } 16 | } 17 | 18 | func onFrameChange(in coordinateSpace: CoordinateSpace = .global, 19 | _ block: @escaping (CGRect) -> Void) -> some View { 20 | modifier(OnFrameChangeViewModifier(coordinateSpace: coordinateSpace, block: block)) 21 | } 22 | } 23 | 24 | private struct OnFrameChangeViewModifier: ViewModifier { 25 | 26 | let coordinateSpace: CoordinateSpace 27 | let block: (CGRect) -> Void 28 | 29 | func body(content: Content) -> some View { 30 | content.background( 31 | GeometryReader { proxy -> Color in 32 | let frame = proxy.frame(in: coordinateSpace) 33 | block(frame) 34 | return Color.clear 35 | } 36 | ) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Source/Internal/Handle/Drag/DynamicOverlayDragArea.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DynamicOverlayDragArea.swift 3 | // DynamicOverlay 4 | // 5 | // Created by Gaétan Zanella on 29/01/2022. 6 | // Copyright © 2022 Fabernovel. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import SwiftUI 11 | 12 | struct DynamicOverlayDragArea: Equatable { 13 | 14 | private let area: ActivatedOverlayArea 15 | 16 | init(area: ActivatedOverlayArea) { 17 | self.area = area 18 | } 19 | 20 | static var `default`: DynamicOverlayDragArea { 21 | DynamicOverlayDragArea(area: .default) 22 | } 23 | 24 | var isEmpty: Bool { 25 | area.isEmpty 26 | } 27 | 28 | func contains(_ rect: CGRect) -> Bool { 29 | area.contains(rect) 30 | } 31 | 32 | func contains(_ point: CGPoint) -> Bool { 33 | return area.contains(point) 34 | } 35 | } 36 | 37 | struct DynamicOverlayDragAreaPreferenceKey: PreferenceKey { 38 | 39 | typealias Value = ActivatedOverlayArea 40 | 41 | static var defaultValue: ActivatedOverlayArea = .default 42 | 43 | static func reduce(value: inout Value, nextValue: () -> Value) { 44 | value.merge(nextValue()) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /DynamicOverlay_Example/DynamicOverlay_Example/View/OverlayBackgroundView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OverlayBackgroundView.swift 3 | // DynamicOverlay_Example 4 | // 5 | // Created by Gaétan Zanella on 19/04/2021. 6 | // Copyright © 2021 Fabernovel. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct OverlayBackgroundView: View { 12 | 13 | var body: some View { 14 | Color(.systemBackground) 15 | .cornerRadius(8.0, corners: [.topLeft, .topRight]) 16 | .shadow(color: Color.black.opacity(0.3), radius: 8.0) 17 | } 18 | } 19 | 20 | private extension View { 21 | 22 | func cornerRadius(_ radius: CGFloat, corners: UIRectCorner) -> some View { 23 | clipShape(RoundedCorner(radius: radius, corners: corners)) 24 | } 25 | } 26 | 27 | private struct RoundedCorner: Shape { 28 | 29 | var radius: CGFloat = 0.0 30 | var corners: UIRectCorner = .allCorners 31 | 32 | func path(in rect: CGRect) -> Path { 33 | Path( 34 | UIBezierPath( 35 | roundedRect: rect, 36 | byRoundingCorners: corners, 37 | cornerRadii: CGSize(width: radius, height: radius) 38 | ) 39 | .cgPath 40 | ) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Source/Public/DynamicOverlayHandle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DynamicOverlayHandle.swift 3 | // DynamicOverlay 4 | // 5 | // Created by Gaétan Zanella on 04/12/2020. 6 | // Copyright © 2020 Fabernovel. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | public extension View { 12 | 13 | /// Defines the target as a draggable view. 14 | /// 15 | /// - parameter isActive: Boolean indicating whether the target is draggable. 16 | func draggable(_ isActive: Bool = true) -> some View { 17 | modifier( 18 | ActiveOverlayAreaViewModifier( 19 | key: DynamicOverlayDragAreaPreferenceKey.self, 20 | isActive: isActive 21 | ) 22 | ) 23 | } 24 | 25 | /// Defines the target as the container of a driving scroll view. 26 | /// When specified a driving scroll view coordinates its scrolling with the overlay translation. 27 | /// 28 | /// - parameter isActive: Boolean indicating whether the scroll view is active. 29 | func drivingScrollView(_ isActive: Bool = true) -> some View { 30 | modifier( 31 | ActiveOverlayAreaViewModifier( 32 | key: DynamicOverlayScrollViewProxyPreferenceKey.self, 33 | isActive: isActive 34 | ) 35 | ) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /DynamicOverlay.podspec: -------------------------------------------------------------------------------- 1 | # 2 | # Be sure to run `pod lib lint DynamicOverlay.podspec' to ensure this is a 3 | # valid spec before submitting. 4 | # 5 | # Any lines starting with a # are optional, but their use is encouraged 6 | # To learn more about a Podspec see https://guides.cocoapods.org/syntax/podspec.html 7 | # 8 | 9 | Pod::Spec.new do |s| 10 | s.name = 'DynamicOverlay' 11 | s.version = '1.0.2' 12 | s.summary = 'OverlayContainer is a SwiftUI library which makes it easier to develop overlay based interfaces.' 13 | s.swift_version = "5.0" 14 | 15 | s.description = <<-DESC 16 | 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. 17 | DESC 18 | 19 | s.homepage = 'https://github.com/fabernovel/DynamicOverlay' 20 | s.license = { :type => 'MIT', :file => 'LICENSE' } 21 | s.author = { 'gaetanzanella' => 'gaetan.zanella@fabernovel.com' } 22 | s.source = { :git => 'https://github.com/fabernovel/DynamicOverlay.git', :tag => s.version.to_s } 23 | s.dependency 'OverlayContainer', '~> 3.5' 24 | 25 | s.ios.deployment_target = '13.0' 26 | s.source_files = 'Source/**/*.swift' 27 | end 28 | -------------------------------------------------------------------------------- /Source/Internal/OverlayContainer/SwiftUIOverlayContainerRepresentableAdaptor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftUIOverlayContainerRepresentableAdaptor.swift 3 | // DynamicOverlayTests 4 | // 5 | // Created by Gaétan Zanella on 16/04/2021. 6 | // Copyright © 2021 Fabernovel. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import SwiftUI 11 | import OverlayContainer 12 | 13 | struct SwiftUIOverlayContainerRepresentableAdaptor: UIViewControllerRepresentable { 14 | 15 | let adaptor: OverlayContainerRepresentableAdaptor 16 | 17 | // MARK: - UIViewControllerRepresentable 18 | 19 | func makeCoordinator() -> OverlayContainerCoordinator { 20 | adaptor.makeCoordinator() 21 | } 22 | 23 | func makeUIViewController(context: Context) -> OverlayContainerViewController { 24 | adaptor.makeUIViewController(context: map(context)) 25 | } 26 | 27 | func updateUIViewController(_ uiViewController: OverlayContainerViewController, context: Context) { 28 | adaptor.updateUIViewController(uiViewController, context: map(context)) 29 | } 30 | 31 | private func map(_ context: Context) -> OverlayContainerRepresentableAdaptor.Context { 32 | OverlayContainerRepresentableAdaptor.Context( 33 | coordinator: context.coordinator, 34 | transaction: context.transaction 35 | ) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Source/Internal/OverlayContentModifier.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OverlayContentModifier.swift 3 | // DynamicOverlay 4 | // 5 | // Created by Gaétan Zanella on 03/12/2020. 6 | // Copyright © 2020 Fabernovel. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct OverlayContentModifier: ViewModifier { 12 | 13 | let overlay: Overlay 14 | 15 | func body(content: Content) -> some View { 16 | content.environment(\.overlayContentKey, AnyView(overlay)) 17 | } 18 | } 19 | 20 | extension View { 21 | 22 | func overlayContent(_ content: Content) -> ModifiedContent> { 23 | modifier(OverlayContentModifier(overlay: content)) 24 | } 25 | } 26 | 27 | /// The root view of the overlay content 28 | struct OverlayContentHostingView: View { 29 | 30 | /// We use an environment variable to avoid UIViewController allocations each time the content changes. 31 | @Environment(\.overlayContentKey) 32 | var content: AnyView 33 | 34 | var body: some View { 35 | content 36 | } 37 | } 38 | 39 | private struct OverlayContentKey: EnvironmentKey { 40 | 41 | static var defaultValue = AnyView(EmptyView()) 42 | } 43 | 44 | private extension EnvironmentValues { 45 | 46 | var overlayContentKey: AnyView { 47 | set { 48 | self[OverlayContentKey.self] = newValue 49 | } 50 | get { 51 | self[OverlayContentKey.self] 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /DynamicOverlay_Example/DynamicOverlay_Example/Configuration/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIRequiredDeviceCapabilities 26 | 27 | armv7 28 | 29 | UISupportedInterfaceOrientations 30 | 31 | UIInterfaceOrientationPortrait 32 | UIInterfaceOrientationLandscapeLeft 33 | UIInterfaceOrientationLandscapeRight 34 | 35 | UISupportedInterfaceOrientations~ipad 36 | 37 | UIInterfaceOrientationPortrait 38 | UIInterfaceOrientationPortraitUpsideDown 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /Source/Internal/Handle/ActivatedOverlayArea.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ActivatedOverlayArea.swift 3 | // DynamicOverlay 4 | // 5 | // Created by Gaétan Zanella on 04/12/2020. 6 | // Copyright © 2020 Fabernovel. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct ActivatedOverlayArea: Equatable { 12 | 13 | private struct Spot: Equatable { 14 | let frame: CGRect 15 | } 16 | 17 | private var spots: [Spot] 18 | 19 | mutating func merge(_ handle: ActivatedOverlayArea) { 20 | spots += handle.spots 21 | } 22 | 23 | var isEmpty: Bool { 24 | spots.isEmpty 25 | } 26 | 27 | func contains(_ rect: CGRect) -> Bool { 28 | spots.contains { $0.frame == rect } 29 | } 30 | 31 | func contains(_ point: CGPoint) -> Bool { 32 | spots.contains { $0.frame.contains(point) } 33 | } 34 | 35 | func intersects(_ rect: CGRect) -> Bool { 36 | spots.contains { 37 | // (gz) 2022-01-29 `SwiftUI` rounds the `UIKit` view frames. 38 | // A 0.25pt-width `SwiftUI` view can contain a 0.5pt-width `UIView`. 39 | rect.intersection($0.frame).width >= 0.5 40 | && $0.frame != .zero 41 | } 42 | } 43 | } 44 | 45 | extension ActivatedOverlayArea { 46 | 47 | static func active(_ frame: CGRect) -> ActivatedOverlayArea { 48 | ActivatedOverlayArea(spots: [Spot(frame: frame)]) 49 | } 50 | 51 | static func inactive() -> ActivatedOverlayArea { 52 | ActivatedOverlayArea(spots: [Spot(frame: .zero)]) 53 | } 54 | 55 | static var `default`: ActivatedOverlayArea { 56 | .empty 57 | } 58 | 59 | static var empty: ActivatedOverlayArea { 60 | ActivatedOverlayArea(spots: []) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Source/Internal/Handle/DrivingScrollView/DynamicOverlayScrollViewProxy.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DynamicOverlayScrollViewProxyPreferenceKey.swift 3 | // DynamicOverlay 4 | // 5 | // Created by Gaétan Zanella on 11/01/2022. 6 | // Copyright © 2022 Fabernovel. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct DynamicOverlayScrollViewProxy: Equatable { 12 | 13 | private let area: ActivatedOverlayArea 14 | 15 | init(area: ActivatedOverlayArea) { 16 | self.area = area 17 | } 18 | 19 | static var `default`: DynamicOverlayScrollViewProxy { 20 | DynamicOverlayScrollViewProxy(area: .default) 21 | } 22 | 23 | func findScrollView(in space: UIView) -> UIScrollView? { 24 | space.findScrollView(in: area, coordinate: space) 25 | } 26 | } 27 | 28 | 29 | struct DynamicOverlayScrollViewProxyPreferenceKey: PreferenceKey { 30 | 31 | typealias Value = ActivatedOverlayArea 32 | 33 | static var defaultValue: ActivatedOverlayArea = .default 34 | 35 | static func reduce(value: inout Value, nextValue: () -> Value) { 36 | value.merge(nextValue()) 37 | } 38 | } 39 | 40 | private extension UIView { 41 | 42 | func findScrollView(in area: ActivatedOverlayArea, 43 | coordinate: UICoordinateSpace) -> UIScrollView? { 44 | let frame = coordinate.convert(bounds, from: self) 45 | guard area.intersects(frame) else { return nil } 46 | if let result = self as? UIScrollView { 47 | return result 48 | } 49 | for subview in subviews { 50 | if let result = subview.findScrollView(in: area, coordinate: coordinate) { 51 | return result 52 | } 53 | } 54 | return nil 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /DynamicOverlay_Example/DynamicOverlay_Example/View/OverlayView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OverlayView.swift 3 | // DynamicOverlay_Example 4 | // 5 | // Created by Gaétan Zanella on 17/04/2021. 6 | // Copyright © 2021 Fabernovel. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import SwiftUI 11 | import DynamicOverlay 12 | 13 | struct OverlayView: View { 14 | 15 | enum Event { 16 | case didBeginEditing 17 | case didEndEditing 18 | } 19 | 20 | let eventHandler: (Event) -> Void 21 | 22 | // MARK: - View 23 | 24 | var body: some View { 25 | VStack(spacing: 0.0) { 26 | header.draggable() 27 | list 28 | } 29 | .background(OverlayBackgroundView()) 30 | } 31 | 32 | // MARK: - Private 33 | 34 | private var list: some View { 35 | List { 36 | Section(header: Text("Favorites")) { 37 | ScrollView(.horizontal) { 38 | HStack { 39 | FavoriteCell(imageName: "house.fill", title: "House") 40 | FavoriteCell(imageName: "briefcase.fill", title: "Work") 41 | FavoriteCell(imageName: "plus", title: "Add") 42 | } 43 | } 44 | } 45 | Section(header: Text("My Guides")) { 46 | ActionCell() 47 | } 48 | } 49 | .listStyle(GroupedListStyle()) 50 | } 51 | 52 | private var header: some View { 53 | SearchBar { event in 54 | switch event { 55 | case .didBeginEditing: 56 | eventHandler(.didBeginEditing) 57 | case .didCancel: 58 | eventHandler(.didEndEditing) 59 | } 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Source/Internal/DynamicOverlayNotchTransition+DynamicOverlayBehavior.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MagneticNotchOverlayBehavior+DynamicOverlayBehavior.swift 3 | // DynamicOverlay 4 | // 5 | // Created by Gaétan Zanella on 02/12/2020. 6 | // Copyright © 2020 Fabernovel. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | extension NotchDimension { 12 | 13 | enum ValueType: Hashable { 14 | case absolute 15 | case fractional 16 | } 17 | } 18 | 19 | extension MagneticNotchOverlayBehavior { 20 | 21 | // MARK: - DynamicOverlayBehavior 22 | 23 | func buildValue() -> DynamicOverlayBehaviorValue { 24 | DynamicOverlayBehaviorValue( 25 | notchDimensions: Dictionary( 26 | uniqueKeysWithValues: Notch.allCases.enumerated().map { i, notch in (i, value.dimensions(notch)) } 27 | ), 28 | block: value.translationBlocks.isEmpty ? nil : { translation in 29 | value.translationBlocks.forEach { 30 | $0( 31 | Translation( 32 | height: translation.height, 33 | transaction: translation.transaction, 34 | progress: Double(min(max(translation.translationProgress, 0), 1)), 35 | containerSize: translation.containerFrame.size, 36 | heightForNotch: { notch in 37 | translation.heightForNotchIndex(Notch.index(of: notch)) 38 | } 39 | ) 40 | ) 41 | } 42 | }, 43 | binding: value.binding?.indexBinding(), 44 | disabledNotchIndexes: Set(value.disabledNotches.map { Notch.index(of: $0) }) 45 | ) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Source/Public/DynamicOverlayModifier.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DynamicOverlayModifier.swift 3 | // DynamicOverlay 4 | // 5 | // Created by Gaétan Zanella on 01/12/2020. 6 | // Copyright © 2020 Fabernovel. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | public extension View { 12 | 13 | /// Adds a dynamic overlay above this view. 14 | /// 15 | /// - parameter content: the content of the overlay. 16 | /// 17 | /// - returns: A view with a dynamic overlay added above this view. 18 | func dynamicOverlay(_ content: Content) -> some View { 19 | modifier(AddDynamicOverlayModifier(overlay: content)) 20 | } 21 | 22 | /// Sets the overlay behavior for dynamic overlays within this view. 23 | /// 24 | /// - parameter behavior: the behavior to apply. 25 | /// 26 | /// - returns: A view with the specified behavior set. 27 | /// 28 | /// 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. 29 | func dynamicOverlayBehavior(_ behavior: Behavior) -> some View { 30 | modifier(behavior.makeModifier()) 31 | } 32 | } 33 | 34 | public struct AddDynamicOverlayModifier: ViewModifier { 35 | 36 | let overlay: Overlay 37 | 38 | // MARK: - ViewModifier 39 | 40 | public func body(content: Content) -> some View { 41 | OverlayContainerDynamicOverlayView( 42 | background: content, 43 | content: overlay 44 | ) 45 | } 46 | } 47 | 48 | public struct AddDynamicOverlayBehaviorModifier: ViewModifier { 49 | 50 | let value: DynamicOverlayBehaviorValue 51 | 52 | // MARK: - ViewModifier 53 | 54 | public func body(content: Content) -> some View { 55 | content.environment(\.behaviorValue, value) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /DynamicOverlay_Example/DynamicOverlay_Example/Resources/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /Source/Internal/DynamicOverlayBehaviorValue.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EmptyFile.swift 3 | // DynamicOverlay 4 | // 5 | // Created by Gaétan Zanella on 05/03/2019. 6 | // Copyright © 2019 Fabernovel. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct OverlayTranslation { 12 | let height: CGFloat 13 | let transaction: Transaction 14 | let isDragging: Bool 15 | let translationProgress: CGFloat 16 | let containerFrame: CGRect 17 | let velocity: CGPoint 18 | let heightForNotchIndex: (Int) -> CGFloat 19 | } 20 | 21 | struct DynamicOverlayBehaviorValue { 22 | 23 | let notchDimensions: [Int: NotchDimension]? 24 | let block: ((OverlayTranslation) -> Void)? 25 | let binding: Binding? 26 | let disabledNotchIndexes: Set 27 | 28 | init(notchDimensions: [Int: NotchDimension]? = nil, 29 | block: ((OverlayTranslation) -> Void)? = nil, 30 | binding: Binding? = nil, 31 | disabledNotchIndexes: Set = []) { 32 | self.notchDimensions = notchDimensions 33 | self.block = block 34 | self.binding = binding 35 | self.disabledNotchIndexes = disabledNotchIndexes 36 | } 37 | } 38 | 39 | extension DynamicOverlayBehaviorValue { 40 | 41 | static var `default`: DynamicOverlayBehaviorValue { 42 | DynamicOverlayBehaviorValue( 43 | notchDimensions: [ 44 | 0 : .fractional(0.3), 45 | 1 : .fractional(0.5), 46 | 2 : .fractional(0.7) 47 | ] 48 | ) 49 | } 50 | } 51 | 52 | struct DynamicOverlayBehaviorKey: EnvironmentKey { 53 | 54 | static var defaultValue: DynamicOverlayBehaviorValue = .default 55 | } 56 | 57 | extension EnvironmentValues { 58 | 59 | var behaviorValue: DynamicOverlayBehaviorValue { 60 | set { 61 | self[DynamicOverlayBehaviorKey.self] = newValue 62 | } 63 | get { 64 | self[DynamicOverlayBehaviorKey.self] 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /DynamicOverlay_Example/DynamicOverlay_Example/View/SearchBar.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SearchBar.swift 3 | // DynamicOverlay_Example 4 | // 5 | // Created by Gaétan Zanella on 18/04/2021. 6 | // Copyright © 2021 Fabernovel. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import SwiftUI 11 | import UIKit 12 | 13 | struct SearchBar: View { 14 | 15 | enum Event { 16 | case didBeginEditing 17 | case didCancel 18 | } 19 | 20 | let eventHandler: (Event) -> Void 21 | 22 | var body: some View { 23 | SearchBarAdaptor( 24 | didBeginEditing: { eventHandler(.didBeginEditing) }, 25 | didCancel: { eventHandler(.didCancel) } 26 | ) 27 | } 28 | } 29 | 30 | private class SearchBarCoordinator: NSObject, UISearchBarDelegate { 31 | 32 | var didBeginEditing: (() -> Void)? 33 | var didCancel: (() -> Void)? 34 | 35 | // MARK: - UISearchBarDelegate 36 | 37 | func searchBarTextDidBeginEditing(_ searchBar: UISearchBar) { 38 | didBeginEditing?() 39 | } 40 | 41 | func searchBarCancelButtonClicked(_ searchBar: UISearchBar) { 42 | searchBar.endEditing(true) 43 | didCancel?() 44 | } 45 | } 46 | 47 | private struct SearchBarAdaptor: UIViewRepresentable { 48 | 49 | let didBeginEditing: () -> Void 50 | let didCancel: () -> Void 51 | 52 | func makeCoordinator() -> SearchBarCoordinator { 53 | let coordinator = SearchBarCoordinator() 54 | coordinator.didBeginEditing = didBeginEditing 55 | coordinator.didCancel = didCancel 56 | return coordinator 57 | } 58 | 59 | func makeUIView(context: Context) -> UISearchBar { 60 | let searchBar = UISearchBar() 61 | searchBar.searchBarStyle = .minimal 62 | searchBar.showsCancelButton = true 63 | searchBar.placeholder = "Search for a place or address" 64 | searchBar.delegate = context.coordinator 65 | return searchBar 66 | } 67 | 68 | func updateUIView(_ uiView: UISearchBar, context: Context) {} 69 | } 70 | -------------------------------------------------------------------------------- /DynamicOverlay_Example/DynamicOverlay_Example/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /DynamicOverlay_Example/DynamicOverlay_Example/View/MapRootView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MapRootView.swift 3 | // DynamicOverlay_Example 4 | // 5 | // Created by Gaétan Zanella on 17/04/2021. 6 | // Copyright © 2021 Fabernovel. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import SwiftUI 11 | import DynamicOverlay 12 | 13 | enum Notch: CaseIterable, Equatable { 14 | case min, max 15 | } 16 | 17 | struct MapRootView: View { 18 | 19 | struct State { 20 | var notch: Notch = .min 21 | var isEditing = false 22 | var progress = 0.0 23 | } 24 | 25 | @SwiftUI.State 26 | private var state = State() 27 | 28 | // MARK: - View 29 | 30 | var body: some View { 31 | background 32 | .dynamicOverlay(overlay) 33 | .dynamicOverlayBehavior(behavior) 34 | .ignoresSafeArea() 35 | } 36 | 37 | // MARK: - Private 38 | 39 | private var behavior: some DynamicOverlayBehavior { 40 | MagneticNotchOverlayBehavior { notch in 41 | switch notch { 42 | case .max: 43 | return .fractional(0.8) 44 | case .min: 45 | return .fractional(0.3) 46 | } 47 | } 48 | .disable(.min, state.isEditing) 49 | .notchChange($state.notch) 50 | .onTranslation { translation in 51 | state.progress = translation.progress 52 | } 53 | } 54 | 55 | private var background: some View { 56 | ZStack { 57 | MapView() 58 | BackdropView().opacity(state.progress) 59 | } 60 | .ignoresSafeArea() 61 | } 62 | 63 | private var overlay: some View { 64 | OverlayView { event in 65 | switch event { 66 | case .didBeginEditing: 67 | state.isEditing = true 68 | withAnimation { state.notch = .max } 69 | case .didEndEditing: 70 | state.isEditing = false 71 | withAnimation { state.notch = .min } 72 | } 73 | } 74 | .drivingScrollView() 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /Source/Internal/OverlayNotchIndexMapper.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OverlayNotchIndexMapper.swift 3 | // DynamicOverlay 4 | // 5 | // Created by Gaétan Zanella on 29/12/2020. 6 | // Copyright © 2020 Fabernovel. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | class OverlayNotchIndexMapper { 12 | 13 | private var overlayIndexToDynamicIndex: [Int: Int] = [:] 14 | private var overlayIndexToHeight: [Int: CGFloat] = [:] 15 | private var dynamicIndexToOverlayIndex: [Int: Int] = [:] 16 | 17 | func reload(layout: OverlayContainerLayout, availableHeight: CGFloat) { 18 | overlayIndexToDynamicIndex = [:] 19 | overlayIndexToHeight = [:] 20 | dynamicIndexToOverlayIndex = [:] 21 | let sortedIndexes = layout.indexToDimension.sorted(by: { 22 | height(for: $0.value, availableHeight: availableHeight) < height(for: $1.value, availableHeight: availableHeight) 23 | }) 24 | sortedIndexes.enumerated().forEach{ overlayIndex, dynamicValue in 25 | overlayIndexToHeight[overlayIndex] = height(for: dynamicValue.value, availableHeight: availableHeight) 26 | dynamicIndexToOverlayIndex[dynamicValue.key] = overlayIndex 27 | overlayIndexToDynamicIndex[overlayIndex] = dynamicValue.key 28 | } 29 | } 30 | 31 | func numberOfOverlayIndexes() -> Int { 32 | overlayIndexToDynamicIndex.count 33 | } 34 | 35 | func dynamicIndex(forOverlayIndex index: Int) -> Int { 36 | dynamicIndexToOverlayIndex[index] ?? 0 37 | } 38 | 39 | func overlayIndex(forDynamicIndex index: Int) -> Int { 40 | overlayIndexToDynamicIndex[index] ?? 0 41 | } 42 | 43 | func height(forOverlayIndex index: Int) -> CGFloat { 44 | overlayIndexToHeight[index] ?? 0 45 | } 46 | 47 | private func height(for dimension: NotchDimension, 48 | availableHeight: CGFloat) -> CGFloat { 49 | switch dimension.type { 50 | case .absolute: 51 | return CGFloat(dimension.value) 52 | case .fractional: 53 | return availableHeight * CGFloat(dimension.value) 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Source/Internal/MagneticNotchOverlayBehaviorValue.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MagneticNotchOverlayBehaviorValue.swift 3 | // DynamicOverlay 4 | // 5 | // Created by Gaétan Zanella on 02/12/2020. 6 | // Copyright © 2020 Fabernovel. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | extension MagneticNotchOverlayBehavior { 12 | 13 | struct Value { 14 | 15 | let dimensions: (Notch) -> NotchDimension 16 | let translationBlocks: [(Translation) -> Void] 17 | let binding: Binding? 18 | let disabledNotches: [Notch] 19 | 20 | init(dimensions: @escaping (Notch) -> NotchDimension, 21 | translationBlocks: [(Translation) -> Void], 22 | binding: Binding?, 23 | disabledNotches: [Notch]) { 24 | self.dimensions = dimensions 25 | self.translationBlocks = translationBlocks 26 | self.binding = binding 27 | self.disabledNotches = disabledNotches 28 | } 29 | 30 | init(dimensions: @escaping (Notch) -> NotchDimension) { 31 | self.dimensions = dimensions 32 | self.translationBlocks = [] 33 | self.binding = nil 34 | self.disabledNotches = [] 35 | } 36 | 37 | // MARK: - Public 38 | 39 | func appending(_ block: @escaping (Translation) -> Void) -> Self { 40 | Value( 41 | dimensions: dimensions, 42 | translationBlocks: translationBlocks + [block], 43 | binding: binding, 44 | disabledNotches: disabledNotches 45 | ) 46 | } 47 | 48 | func setting(_ binding: Binding) -> Self { 49 | Value( 50 | dimensions: dimensions, 51 | translationBlocks: translationBlocks, 52 | binding: binding, 53 | disabledNotches: disabledNotches 54 | ) 55 | } 56 | 57 | func disabling(_ isDisabled: Bool, _ notch: Notch) -> Self { 58 | Value( 59 | dimensions: dimensions, 60 | translationBlocks: translationBlocks, 61 | binding: binding, 62 | disabledNotches: isDisabled ? disabledNotches + [notch] : disabledNotches 63 | ) 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Source/Internal/OverlayContainer/OverlayContainerRepresentableAdaptor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OverlayContainerRepresentableAdaptor.swift 3 | // DynamicOverlayTests 4 | // 5 | // Created by Gaétan Zanella on 20/04/2021. 6 | // Copyright © 2021 Fabernovel. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import SwiftUI 11 | import OverlayContainer 12 | 13 | struct OverlayContainerRepresentableAdaptor { 14 | 15 | struct Context { 16 | let coordinator: OverlayContainerCoordinator 17 | let transaction: Transaction 18 | } 19 | 20 | let containerState: OverlayContainerState 21 | let passiveContainer: OverlayContainerPassiveContainer 22 | let content: Content 23 | let background: Background 24 | 25 | private let style: OverlayContainerViewController.OverlayStyle = .expandableHeight 26 | 27 | // MARK: - UIViewControllerRepresentable 28 | 29 | func makeCoordinator() -> OverlayContainerCoordinator { 30 | let contentController = UIHostingController(rootView: content) 31 | contentController.view.backgroundColor = .clear 32 | contentController.view.setContentCompressionResistancePriority(.defaultLow, for: .vertical) 33 | contentController.view.setContentHuggingPriority(.defaultLow, for: .vertical) 34 | let backgroundController = UIHostingController(rootView: background) 35 | backgroundController.view.backgroundColor = .clear 36 | return OverlayContainerCoordinator( 37 | style: style, 38 | layout: containerState.layout, 39 | passiveContainer: passiveContainer, 40 | background: backgroundController, 41 | content: contentController 42 | ) 43 | } 44 | 45 | func makeUIViewController(context: Context) -> OverlayContainerViewController { 46 | let controller = OverlayContainerViewController(style: style) 47 | controller.delegate = context.coordinator 48 | return controller 49 | } 50 | 51 | func updateUIViewController(_ container: OverlayContainerViewController, 52 | context: Context) { 53 | context.coordinator.move( 54 | container, 55 | to: containerState, 56 | animated: context.transaction.animation != nil 57 | ) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Tests/DynamicOverlayTests/NotchDimensionDynamicOverlayTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NotchDimensionDynamicOverlayTests.swift 3 | // DynamicOverlayTests 4 | // 5 | // Created by Gaétan Zanella on 16/04/2021. 6 | // Copyright © 2021 Fabernovel. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import XCTest 11 | import SwiftUI 12 | import DynamicOverlay 13 | 14 | private enum Notch: CaseIterable, Equatable { 15 | case min 16 | } 17 | 18 | private struct NotchDimensionView: View { 19 | 20 | let dimension: () -> NotchDimension 21 | let onHeightChange: (CGFloat) -> Void 22 | 23 | var body: some View { 24 | Color.red 25 | .dynamicOverlay(Color.green.onHeightChange(onHeightChange)) 26 | .dynamicOverlayBehavior(behavior) 27 | } 28 | 29 | var behavior: some DynamicOverlayBehavior { 30 | MagneticNotchOverlayBehavior { _ in 31 | dimension() 32 | } 33 | } 34 | } 35 | 36 | class NotchDimensionDynamicOverlayTests: XCTestCase { 37 | 38 | func testVariousDimensions() { 39 | class Context { 40 | var dimension: NotchDimension = .absolute(0) 41 | var expectedHeight: CGFloat = 0.0 42 | var expectation = XCTestExpectation() 43 | } 44 | let renderer = ViewRenderer(view: EmptyView()) 45 | let resultByDimension: [NotchDimension: CGFloat] = [ 46 | .absolute(-1) : 0.0, 47 | .absolute(0) : 0.0, 48 | .absolute(200.0) : 200.0, 49 | .fractional(-1.0) : 0.0, 50 | .fractional(0.0) : 0.0, 51 | .fractional(2.0) : renderer.safeBounds.height * 2.0, 52 | .fractional(0.5) : renderer.safeBounds.height * 0.5, 53 | ] 54 | resultByDimension.forEach { dimension, expectedHeight in 55 | let context = Context() 56 | let view = NotchDimensionView(dimension: { context.dimension }) { height in 57 | context.expectation.fulfill() 58 | XCTAssertEqual(context.expectedHeight.rounded(.up), height.rounded(.up)) 59 | } 60 | context.expectedHeight = expectedHeight 61 | context.expectation = XCTestExpectation() 62 | context.dimension = dimension 63 | ViewRenderer(view: view).render() 64 | wait(for: [context.expectation], timeout: 0.3) 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /Source/Internal/OverlayContainer/OverlayContainerDynamicOverlayView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OverlayContainerView.swift 3 | // DynamicOverlay 4 | // 5 | // Created by Gaétan Zanella on 02/12/2020. 6 | // Copyright © 2020 Fabernovel. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct OverlayContainerDynamicOverlayView: View { 12 | 13 | @State 14 | private var dragArea: DynamicOverlayDragArea = .default 15 | 16 | @State 17 | private var scrollViewProxy: DynamicOverlayScrollViewProxy = .default 18 | 19 | @State 20 | private var passiveContainer = OverlayContainerPassiveContainer() 21 | 22 | @Environment(\.behaviorValue) 23 | private var behavior: DynamicOverlayBehaviorValue 24 | 25 | let background: Background 26 | let content: Content 27 | 28 | // MARK: - View 29 | 30 | var body: some View { 31 | SwiftUIOverlayContainerRepresentableAdaptor( 32 | adaptor: OverlayContainerRepresentableAdaptor( 33 | containerState: makeContainerState(), 34 | passiveContainer: passiveContainer, 35 | content: OverlayContentHostingView(), 36 | background: background 37 | ) 38 | ) 39 | .overlayContent(content.overlayCoordinateSpace()) 40 | .onUpdate { 41 | passiveContainer.onTranslation = behavior.block 42 | // This is tricky. `OverlayContainerPassiveContainer` is a class inside a struct, 43 | // `passiveContainer.onNotchChange = { self.behavior.binding?.wrappedValue = $0 }` 44 | // would create a retain cycle as `self` includes a ref to `passiveContainer`. 45 | let behavior = behavior 46 | passiveContainer.onNotchChange = { behavior.binding?.wrappedValue = $0 } 47 | } 48 | .onDragAreaChange { 49 | dragArea = $0 50 | } 51 | .onDrivingScrollViewChange { 52 | scrollViewProxy = $0 53 | } 54 | } 55 | 56 | // MARK: - Private 57 | 58 | private func makeContainerState() -> OverlayContainerState { 59 | OverlayContainerState( 60 | dragArea: dragArea, 61 | drivingScrollViewProxy: scrollViewProxy, 62 | notchIndex: behavior.binding?.wrappedValue, 63 | disabledNotches: behavior.disabledNotchIndexes, 64 | layout: OverlayContainerLayout(indexToDimension: behavior.notchDimensions ?? [:]) 65 | ) 66 | } 67 | } 68 | 69 | private extension View { 70 | 71 | func onUpdate(_ block: () -> Void) -> some View { 72 | block() 73 | return self 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /Tests/DynamicOverlayTests/DragHandleViewModifierTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DragHandleTests.swift 3 | // DynamicOverlayTests 4 | // 5 | // Created by Gaétan Zanella on 16/04/2021. 6 | // Copyright © 2021 Fabernovel. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import XCTest 11 | import SwiftUI 12 | @testable import DynamicOverlay 13 | 14 | private struct HandleView: View { 15 | 16 | var body: some View { 17 | Color.red 18 | } 19 | } 20 | 21 | private struct ContainerView: View { 22 | 23 | let isActive: Bool 24 | let frame: CGRect 25 | let handler: (DynamicOverlayDragArea) -> Void 26 | 27 | var body: some View { 28 | GeometryReader { _ in 29 | HandleView() 30 | .frame(width: frame.width, height: frame.height) 31 | .draggable(isActive) 32 | .offset(x: frame.origin.x, y: frame.origin.y) 33 | } 34 | .onDragAreaChange(handler: handler) 35 | .overlayCoordinateSpace() 36 | } 37 | } 38 | 39 | private struct MultipleHandlesView: View { 40 | 41 | let handler: (DynamicOverlayDragArea) -> Void 42 | 43 | var body: some View { 44 | VStack { 45 | Color.orange.draggable() 46 | Color.red.draggable() 47 | } 48 | .overlayCoordinateSpace() 49 | .onDragAreaChange(handler: handler) 50 | } 51 | } 52 | 53 | class DragHandleViewModifierTests: XCTestCase { 54 | 55 | func testActiveState() { 56 | let frame = CGRect(x: 0, y: 0, width: 200, height: 200) 57 | let activeHandle = DynamicOverlayDragArea(area: .active(frame)) 58 | let notActiveHandle = DynamicOverlayDragArea(area: .inactive()) 59 | let point = CGPoint(x: 20.0, y: 20.0) 60 | XCTAssertTrue(activeHandle.contains(point)) 61 | XCTAssertFalse(notActiveHandle.contains(point)) 62 | } 63 | 64 | func testMultipleFrames() { 65 | let values: [(Bool, CGRect)] = [ 66 | (true, CGRect(x: 30, y: 30, width: 50, height: 100)), 67 | (false, CGRect(x: 0, y: 0, width: 400, height: 400)), 68 | (true, CGRect(x: 0, y: 0, width: 400, height: 400)), 69 | ] 70 | values.forEach { isActive, frame in 71 | let expectation = XCTestExpectation() 72 | let view = ContainerView( 73 | isActive: isActive, 74 | frame: frame, 75 | handler: { handler in 76 | XCTAssertEqual(handler.contains(frame), isActive) 77 | expectation.fulfill() 78 | } 79 | ) 80 | ViewRenderer(view: view).render() 81 | wait(for: [expectation], timeout: 0.3) 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /fastlane/Fastfile: -------------------------------------------------------------------------------- 1 | 2 | default_platform(:ios) 3 | 4 | platform :ios do 5 | desc "Run all unit tests" 6 | lane :tests do 7 | scan( 8 | scheme: "DynamicOverlay_Example", 9 | project: "DynamicOverlay_Example/DynamicOverlay_Example.xcodeproj", 10 | clean: true 11 | ) 12 | end 13 | 14 | desc "Pod linting" 15 | lane :pod_lint do 16 | pod_lib_lint(allow_warnings: true) 17 | end 18 | 19 | desc "Carthage linting" 20 | lane :carthage_lint do 21 | # FIX lint 22 | # carthage(command: "build", no_skip_current: true, platform: "iOS") 23 | end 24 | 25 | desc "SPM linting" 26 | lane :spm_lint do 27 | output = "Package.xcodeproj" 28 | Dir.chdir("..") do 29 | sh("swift package generate-xcodeproj --output #{output} --xcconfig-overrides #{ENV["XCCONFIG"]}") 30 | end 31 | xcodebuild( 32 | project: output, 33 | scheme: "#{ENV["SCHEME"]}-Package" 34 | ) 35 | Dir.chdir("..") do 36 | sh("rm Package.resolved") 37 | sh("rm -rf Package.xcodeproj") 38 | end 39 | end 40 | 41 | desc "Release a new version" 42 | lane :release do |options| 43 | target_version = options[:version] 44 | raise "The version is missed. Use `fastlane release version:{version_number}`.`" if target_version.nil? 45 | 46 | ensure_git_branch(branch: "(release/*)|(hotfix/*)") 47 | ensure_git_status_clean 48 | 49 | podspec = ENV["PODSPEC"] 50 | version_bump_podspec(path: podspec, version_number: target_version) 51 | git_add 52 | git_commit( 53 | path: ["DynamicOverlay.podspec"], 54 | message: "Bump to #{target_version}" 55 | ) 56 | ensure_git_status_clean 57 | add_git_tag(tag: target_version) 58 | 59 | changelog = read_changelog( 60 | changelog_path: "CHANGELOG.md", 61 | section_identifier: "[#{target_version}]" 62 | ) 63 | 64 | # Push 65 | push_to_git_remote 66 | push_git_tags(tag: target_version) 67 | UI.success "Pushed 🎉" 68 | 69 | # Release cocoapods 70 | pod_push 71 | UI.success "Released 🎉" 72 | 73 | # Release Github 74 | 75 | set_github_release( 76 | repository_name: "faberNovel/DynamicOverlay", 77 | api_token: ENV["GITHUB_TOKEN"], 78 | name: "v#{target_version}", 79 | tag_name: "#{target_version}", 80 | description: changelog, 81 | ) 82 | 83 | # Make PR 84 | create_pull_request( 85 | api_token: ENV["GITHUB_TOKEN"], 86 | repo: "faberNovel/DynamicOverlay", 87 | title: "Release #{target_version}", 88 | base: "main", 89 | body: changelog 90 | ) 91 | end 92 | end 93 | -------------------------------------------------------------------------------- /Tests/DynamicOverlayTests/OverlayNotchIndexMapperTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OverlayNotchIndexMapperTests.swift 3 | // DynamicOverlay 4 | // 5 | // Created by Gaétan Zanella on 29/12/2020. 6 | // Copyright © 2020 Fabernovel. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import DynamicOverlay 11 | 12 | class OverlayNotchIndexMapperTests: XCTestCase { 13 | 14 | private var mapper: OverlayNotchIndexMapper! 15 | 16 | override func setUp() { 17 | self.mapper = OverlayNotchIndexMapper() 18 | } 19 | 20 | func testAlreadyOrderedIndexesMapping() { 21 | let layout = OverlayContainerLayout( 22 | indexToDimension: [ 23 | 0: NotchDimension(type: .fractional, value: 0.1), 24 | 1: NotchDimension(type: .fractional, value: 0.2), 25 | 2: NotchDimension(type: .fractional, value: 0.3), 26 | ] 27 | ) 28 | mapper.reload( 29 | layout: layout, 30 | availableHeight: 200.0 31 | ) 32 | XCTAssert(mapper.numberOfOverlayIndexes() == 3) 33 | XCTAssert(mapper.dynamicIndex(forOverlayIndex: 0) == 0) 34 | XCTAssert(mapper.dynamicIndex(forOverlayIndex: 1) == 1) 35 | XCTAssert(mapper.dynamicIndex(forOverlayIndex: 2) == 2) 36 | XCTAssert(mapper.overlayIndex(forDynamicIndex: 0) == 0) 37 | XCTAssert(mapper.overlayIndex(forDynamicIndex: 1) == 1) 38 | XCTAssert(mapper.overlayIndex(forDynamicIndex: 2) == 2) 39 | } 40 | 41 | func testFractionalNotchDimensionReordering() { 42 | let layout = OverlayContainerLayout( 43 | indexToDimension: [ 44 | 0: NotchDimension(type: .absolute, value: 100), 45 | 1: NotchDimension(type: .fractional, value: 0.1), 46 | ] 47 | ) 48 | mapper.reload( 49 | layout: layout, 50 | availableHeight: 200.0 51 | ) 52 | XCTAssert(mapper.numberOfOverlayIndexes() == 2) 53 | XCTAssert(mapper.dynamicIndex(forOverlayIndex: 0) == 1) 54 | XCTAssert(mapper.dynamicIndex(forOverlayIndex: 1) == 0) 55 | XCTAssert(mapper.overlayIndex(forDynamicIndex: 1) == 0) 56 | XCTAssert(mapper.overlayIndex(forDynamicIndex: 0) == 1) 57 | } 58 | 59 | func testNotchFractionalHeights() { 60 | let layout = OverlayContainerLayout( 61 | indexToDimension: [ 62 | 0: NotchDimension(type: .fractional, value: 0.1), 63 | 1: NotchDimension(type: .fractional, value: 0.5), 64 | 2: NotchDimension(type: .fractional, value: 1), 65 | ] 66 | ) 67 | mapper.reload( 68 | layout: layout, 69 | availableHeight: 200.0 70 | ) 71 | XCTAssert(mapper.numberOfOverlayIndexes() == 3) 72 | XCTAssert(mapper.height(forOverlayIndex: 0) == 20.0) 73 | XCTAssert(mapper.height(forOverlayIndex: 1) == 100.0) 74 | XCTAssert(mapper.height(forOverlayIndex: 2) == 200.0) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /Tests/DynamicOverlayTests/NotchBindingDynamicOverlayTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NotchBindingDynamicOverlayTests.swift 3 | // DynamicOverlay 4 | // 5 | // Created by Gaétan Zanella on 10/04/2021. 6 | // Copyright © 2021 Fabernovel. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import SwiftUI 11 | import XCTest 12 | import DynamicOverlay 13 | 14 | private enum Notch: CaseIterable, Equatable { 15 | case min 16 | case max 17 | } 18 | 19 | private struct Constants { 20 | 21 | static func height(for notch: Notch) -> CGFloat { 22 | switch notch { 23 | case .max: 24 | return 300.0 25 | case .min: 26 | return 200.0 27 | } 28 | } 29 | } 30 | 31 | private struct NotchChangeView: View { 32 | 33 | @ObservedObject 34 | var target: ValuePublisher 35 | let onFrameChange: (CGRect) -> Void 36 | 37 | var body: some View { 38 | Color.red 39 | .dynamicOverlay(Color.green.onFrameChange(onFrameChange)) 40 | .dynamicOverlayBehavior(behavior) 41 | } 42 | 43 | var behavior: some DynamicOverlayBehavior { 44 | MagneticNotchOverlayBehavior { notch in 45 | .absolute(Double(Constants.height(for: notch))) 46 | } 47 | .notchChange($target.value) 48 | } 49 | } 50 | 51 | class NotchBindingDynamicOverlayTests: XCTestCase { 52 | 53 | func testInitialMaxNotch() { 54 | expectNotchHeight(.max) 55 | } 56 | 57 | func testInitialMinNotch() { 58 | expectNotchHeight(.min) 59 | } 60 | 61 | func testNotchChange() { 62 | class Context { 63 | var expectedHeight: CGFloat = 0.0 64 | var current = Notch.min 65 | var displayedFrame = CGRect.zero 66 | var expectations: [Notch: XCTestExpectation] = [:] 67 | } 68 | let target = ValuePublisher(Notch.min) 69 | let notches: [Notch] = [.min, .max] 70 | let context = Context() 71 | context.expectations = Dictionary(uniqueKeysWithValues: notches.map { ($0, XCTestExpectation()) }) 72 | let view = NotchChangeView(target: target) { rect in 73 | context.displayedFrame = rect 74 | context.expectations[context.current]?.fulfill() 75 | } 76 | let renderer = ViewRenderer(view: view) 77 | notches.forEach { notch in 78 | guard let expectation = context.expectations[notch] else { return } 79 | context.current = notch 80 | target.update(notch) 81 | renderer.render() 82 | wait(for: [expectation], timeout: 1.0) 83 | let overlayFrame = renderer.window.bounds.intersection(context.displayedFrame) 84 | XCTAssertEqual(overlayFrame.height, Constants.height(for: notch)) 85 | context.displayedFrame = .zero 86 | } 87 | } 88 | 89 | private func expectNotchHeight(_ notch: Notch) { 90 | let target = ValuePublisher(notch) 91 | var displayedFrame: CGRect = .zero 92 | let view = NotchChangeView(target: target) { rect in 93 | displayedFrame = rect 94 | } 95 | let renderer = ViewRenderer(view: view) 96 | renderer.render() 97 | let overlayFrame = renderer.window.bounds.intersection(displayedFrame) 98 | XCTAssertEqual(overlayFrame.height, Constants.height(for: notch)) 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /Tests/DynamicOverlayTests/DrivingScrollViewModifierTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DrivingScrollViewModifierTests.swift 3 | // DynamicOverlayTests 4 | // 5 | // Created by Gaétan Zanella on 16/04/2021. 6 | // Copyright © 2021 Fabernovel. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import XCTest 11 | import SwiftUI 12 | @testable import DynamicOverlay 13 | 14 | private struct ContainerView: View { 15 | 16 | let isActive: Bool 17 | let isActiveHandler: (DynamicOverlayScrollViewProxy) -> Void 18 | 19 | var body: some View { 20 | ScrollView { 21 | Color.green 22 | } 23 | .overlayCoordinateSpace() 24 | .drivingScrollView(isActive) 25 | .onDrivingScrollViewChange(handler: isActiveHandler) 26 | } 27 | } 28 | 29 | private class IdentifiedScrollView: UIScrollView { 30 | var id = "" 31 | } 32 | 33 | class DrivingScrollViewModifierTests: XCTestCase { 34 | 35 | func testScrollViewSearch() { 36 | let container = UIView(frame: CGRect(x: 0, y: 0, width: 400, height: 400)) 37 | let layer = UIView(frame: container.bounds) 38 | container.addSubview(layer) 39 | let scrollViews = Array(repeating: IdentifiedScrollView(), count: 4) 40 | for i in scrollViews.indices { 41 | let scrollView = scrollViews[i] 42 | scrollView.frame.size.height = container.bounds.height / 2 43 | scrollView.frame.size.width = container.bounds.width / 2 44 | scrollView.frame.origin.x = container.bounds.width / 2 * CGFloat(i % 2) 45 | scrollView.frame.origin.y = i > 1 ? container.bounds.height / 2 : 0 46 | if i.isMultiple(of: 2) { 47 | layer.addSubview(scrollView) 48 | } else { 49 | container.addSubview(scrollView) 50 | } 51 | } 52 | for scrollView in scrollViews { 53 | scrollViews.forEach { $0.id = "lure" } 54 | scrollView.id = "target" 55 | let proxy = DynamicOverlayScrollViewProxy( 56 | area: .active(scrollView.frame) 57 | ) 58 | let scrollView = proxy.findScrollView(in: container) as! IdentifiedScrollView 59 | XCTAssertEqual(scrollView.id, "target") 60 | } 61 | } 62 | 63 | func testDrivingScrollView() { 64 | [false, true].forEach { shouldBeActive in 65 | let expectation = XCTestExpectation() 66 | var window: UIWindow! 67 | let view = ContainerView( 68 | isActive: shouldBeActive, 69 | isActiveHandler: { handle in 70 | CATransaction.setCompletionBlock { 71 | if shouldBeActive { 72 | XCTAssertNotNil(handle.findScrollView(in: window)) 73 | } else { 74 | XCTAssertNil(handle.findScrollView(in: window)) 75 | } 76 | expectation.fulfill() 77 | } 78 | } 79 | ) 80 | let renderer = ViewRenderer(view: view) 81 | window = renderer.window 82 | renderer.render() 83 | wait(for: [expectation], timeout: 0.1) 84 | } 85 | } 86 | 87 | func testNoneDrivingScrollView() { 88 | let expectation = XCTestExpectation() 89 | expectation.isInverted = true 90 | let view = Color.red.onDrivingScrollViewChange { _ in 91 | expectation.fulfill() 92 | } 93 | ViewRenderer(view: view).render() 94 | wait(for: [expectation], timeout: 0.1) 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /DynamicOverlay_Example/DynamicOverlay_Example.xcodeproj/xcshareddata/xcschemes/DynamicOverlay_Example.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 53 | 55 | 61 | 62 | 63 | 64 | 70 | 72 | 78 | 79 | 80 | 81 | 83 | 84 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /Source/Internal/OverlayContainer/DynamicOverlayContainerAnimationController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DynamicOverlayContainerAnimationController.swift 3 | // DynamicOverlay 4 | // 5 | // Created by Gaétan Zanella on 28/12/2020. 6 | // Copyright © 2020 Fabernovel. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import SwiftUI 11 | import OverlayContainer 12 | 13 | private struct Constant { 14 | static let defaultMass: CGFloat = 1 15 | static let defaultDamping: CGFloat = 0.7 16 | static let defaultRigidDamping: CGFloat = 0.9 17 | static let defaultResponse: CGFloat = 0.3 18 | static let minimumDamping: CGFloat = 1 19 | static let minimumVelocityConsideration: CGFloat = 150 20 | static let maximumVelocityConsideration: CGFloat = 3000 21 | } 22 | 23 | struct DynamicOverlayContainerAnimationController: OverlayAnimatedTransitioning { 24 | 25 | private var mass: CGFloat = Constant.defaultMass 26 | private var damping: CGFloat = Constant.defaultDamping 27 | private var response: CGFloat = Constant.defaultResponse 28 | 29 | // MARK: - Life Cycle 30 | 31 | public init(style: OverlayContainerViewController.OverlayStyle) { 32 | switch style { 33 | case .expandableHeight, .rigid: 34 | // (gz) 2019-06-15 We also nullify the damping value when using rigid styles 35 | // to avoid the panel to be lifted above the bottom of the screen. 36 | damping = Constant.defaultRigidDamping 37 | case .flexibleHeight: 38 | damping = Constant.defaultDamping 39 | } 40 | } 41 | 42 | // MARK: - Public 43 | 44 | public func animation(using context: OverlayContainerTransitionCoordinatorContext) -> Animation? { 45 | guard context.isAnimated else { return nil } 46 | return .interpolatingSpring( 47 | mass: Double(springMass(context: context)), 48 | stiffness: Double(springStiffness(context: context)), 49 | damping: Double(springDamping(context: context)), 50 | initialVelocity: Double(springVelocity(context: context)) 51 | ) 52 | } 53 | 54 | // MARK: - OverlayAnimatedTransitioning 55 | 56 | public func interruptibleAnimator(using context: OverlayContainerContextTransitioning) -> UIViewImplicitlyAnimating { 57 | let timing = UISpringTimingParameters( 58 | mass: springMass(context: context), 59 | stiffness: springStiffness(context: context), 60 | damping: springDamping(context: context), 61 | initialVelocity: CGVector(dx: springVelocity(context: context), dy: springVelocity(context: context)) 62 | ) 63 | return UIViewPropertyAnimator( 64 | duration: 0, // duration is ignored when using `UISpringTimingParameters.init(mass:stiffness:damping:initialVelocity)` 65 | timingParameters: timing 66 | ) 67 | } 68 | 69 | private func springMass(context: OverlayContainerTransitionContext) -> CGFloat { 70 | mass 71 | } 72 | 73 | private func springStiffness(context: OverlayContainerTransitionContext) -> CGFloat { 74 | pow(2 * .pi / response, 2) 75 | } 76 | 77 | private func springDamping(context: OverlayContainerTransitionContext) -> CGFloat { 78 | let velocity = min( 79 | Constant.maximumVelocityConsideration, 80 | max(abs(context.velocity.y), Constant.minimumVelocityConsideration) 81 | ) 82 | let velocityRange = Constant.maximumVelocityConsideration - Constant.minimumVelocityConsideration 83 | let normalizedVelocity = (velocity - Constant.minimumVelocityConsideration) / velocityRange 84 | let normalizedDamping = normalizedVelocity * (damping - Constant.minimumDamping) + Constant.minimumDamping 85 | return 4 * .pi * normalizedDamping / response 86 | } 87 | 88 | private func springVelocity(context: OverlayContainerTransitionContext) -> CGFloat { 89 | 0 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /Source/Public/MagneticNotchOverlayBehavior.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MagneticNotchOverlayBehavior.swift 3 | // DynamicOverlay 4 | // 5 | // Created by Gaétan Zanella on 02/12/2020. 6 | // Copyright © 2020 Fabernovel. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | /// A `DynamicOverlayBehavior` instance describing an overlay that can be dragged up and down alongside predefined notches. 12 | /// Whenever a drag gesture ends, the overlay motion will continue until it reaches one of its notches. 13 | public struct MagneticNotchOverlayBehavior where Notch: CaseIterable, Notch: Equatable { 14 | 15 | let value: Value 16 | 17 | /// Creates a behavior with the given notches. 18 | /// 19 | /// - parameter notches: The notches. 20 | /// 21 | /// - returns: A behavior with the specified notches. 22 | public init(notches: @escaping (Notch) -> NotchDimension) { 23 | self.value = Value(dimensions: notches) 24 | } 25 | 26 | init(value: Value) { 27 | self.value = value 28 | } 29 | } 30 | 31 | public extension MagneticNotchOverlayBehavior { 32 | 33 | /// The attributes of a translation 34 | struct Translation { 35 | /// The current overlay height. 36 | public let height: CGFloat 37 | /// The transaction associated to the translation. 38 | public let transaction: Transaction 39 | /// The overlay translation progress (from 0.0 to 1.0). 40 | public let progress: Double 41 | /// The overlay container size. 42 | public let containerSize: CGSize 43 | 44 | let heightForNotch: (Notch) -> CGFloat 45 | 46 | /// returns the height of the given notch. 47 | public func height(for notch: Notch) -> CGFloat { 48 | heightForNotch(notch) 49 | } 50 | } 51 | 52 | /// Adds an action to perform when the overlay moves. 53 | /// 54 | /// - parameter action: The action to perform when the translation changes. The action closure’s parameter contains the current translation. 55 | /// 56 | /// - returns: A version of the behavior that triggers the action when the translation changes. 57 | func onTranslation(_ action: @escaping (Translation) -> Void) -> Self { 58 | MagneticNotchOverlayBehavior(value: value.appending(action)) 59 | } 60 | } 61 | 62 | public extension MagneticNotchOverlayBehavior { 63 | 64 | /// Updates the current overlay notch as it changes. 65 | /// 66 | /// - parameter binding: A binding to a notch property. 67 | /// 68 | /// - returns: A version of the behavior that updates the current overlay notch as it changes. 69 | func notchChange(_ binding: Binding) -> Self { 70 | MagneticNotchOverlayBehavior(value: value.setting(binding)) 71 | } 72 | } 73 | 74 | public extension MagneticNotchOverlayBehavior { 75 | 76 | /// Disables a notch. 77 | /// 78 | /// - parameter notch: The notch to disable. 79 | /// - parameter isDisabled: A boolean indicating whether the notch should be disabled. 80 | /// 81 | /// - returns: A version of the behavior that disables the specified notch. 82 | /// 83 | /// When a notch is disabled the overlay can not be dragged to it. 84 | /// The `notchChange` binding is still effective though. 85 | func disable(_ notch: Notch, _ isDisabled: Bool = true) -> Self { 86 | MagneticNotchOverlayBehavior(value: value.disabling(isDisabled, notch)) 87 | } 88 | } 89 | 90 | extension MagneticNotchOverlayBehavior: DynamicOverlayBehavior { 91 | 92 | public func makeModifier() -> AddDynamicOverlayBehaviorModifier { 93 | AddDynamicOverlayBehaviorModifier(value: buildValue()) 94 | } 95 | } 96 | 97 | /// A dimension of a notch. 98 | public struct NotchDimension: Hashable { 99 | 100 | let type: ValueType 101 | let value: Double 102 | } 103 | 104 | public extension NotchDimension { 105 | 106 | /// Creates a dimension with an absolute point value. 107 | static func absolute(_ value: Double) -> NotchDimension { 108 | NotchDimension(type: .absolute, value: value) 109 | } 110 | 111 | /// Creates a dimension that is computed as a fraction of the height of the overlay parent view. 112 | static func fractional(_ value: Double) -> NotchDimension { 113 | NotchDimension(type: .fractional, value: value) 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /Tests/DynamicOverlayTests/NotchTranslationDynamicOverlayTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NotchTranslationDynamicOverlayTests.swift 3 | // DynamicOverlay 4 | // 5 | // Created by Gaétan Zanella on 15/04/2021. 6 | // Copyright © 2021 Fabernovel. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import SwiftUI 11 | import XCTest 12 | @testable import DynamicOverlay 13 | 14 | private enum Notch: CaseIterable, Equatable { 15 | case min, max 16 | } 17 | 18 | private struct Constants { 19 | 20 | static func insets(for layout: TranslationLayout) -> UIEdgeInsets { 21 | switch layout { 22 | case .compact: 23 | return UIEdgeInsets(top: 20, left: 20, bottom: 20, right: 20) 24 | case .full: 25 | return .zero 26 | } 27 | } 28 | 29 | static func height(for notch: Notch) -> CGFloat { 30 | switch notch { 31 | case .max: 32 | return 300.0 33 | case .min: 34 | return 100.0 35 | } 36 | } 37 | } 38 | 39 | private typealias OverlayTranslation = MagneticNotchOverlayBehavior.Translation 40 | 41 | private struct TranslationView: View { 42 | 43 | @ObservedObject 44 | var target: ValuePublisher 45 | let onTranslation: (OverlayTranslation) -> Void 46 | 47 | var body: some View { 48 | Color.red 49 | .dynamicOverlay(Color.green) 50 | .dynamicOverlayBehavior(behavior) 51 | } 52 | 53 | private var behavior: MagneticNotchOverlayBehavior { 54 | MagneticNotchOverlayBehavior { notch in 55 | .absolute(Double(Constants.height(for: notch))) 56 | } 57 | .notchChange($target.value) 58 | .onTranslation(onTranslation) 59 | .disable(.min) 60 | } 61 | } 62 | 63 | private enum TranslationLayout { 64 | case full, compact 65 | } 66 | 67 | private struct TranslationContainerView: View { 68 | 69 | let layout: TranslationLayout 70 | @ObservedObject 71 | var target: ValuePublisher 72 | let onTranslation: (OverlayTranslation) -> Void 73 | 74 | var body: some View { 75 | TranslationView( 76 | target: target, 77 | onTranslation: onTranslation 78 | ) 79 | .padding(insets) 80 | } 81 | 82 | private var insets: EdgeInsets { 83 | let layoutInsets = Constants.insets(for: layout) 84 | return EdgeInsets( 85 | top: layoutInsets.top, 86 | leading: layoutInsets.left, 87 | bottom: layoutInsets.bottom, 88 | trailing: layoutInsets.bottom 89 | ) 90 | } 91 | } 92 | 93 | class NotchTranslationDynamicOverlayTests: XCTestCase { 94 | 95 | func testInitialTranslation() { 96 | class Context { 97 | var expectedTranslation = OverlayTranslation.none 98 | } 99 | let context = Context() 100 | let expectation = XCTestExpectation() 101 | expectation.expectedFulfillmentCount = Notch.allCases.count 102 | Notch.allCases.forEach { notch in 103 | let target = ValuePublisher(notch) 104 | let view = TranslationView(target: target) { translation in 105 | expectation.fulfill() 106 | self.expect(translation, toEqual: context.expectedTranslation) 107 | } 108 | let renderer = ViewRenderer(view: view) 109 | context.expectedTranslation = .initial(for: notch, in: renderer.safeBounds) 110 | renderer.render() 111 | } 112 | wait(for: [expectation], timeout: 1.0) 113 | } 114 | 115 | func testNotchMovesTranslation() { 116 | class Context { 117 | var expectedTranslation = OverlayTranslation.none 118 | var expectation = XCTestExpectation() 119 | } 120 | let context = Context() 121 | let target = ValuePublisher(Notch.min) 122 | let view = TranslationView(target: target) { translation in 123 | context.expectation.fulfill() 124 | self.expect(translation, toEqual: context.expectedTranslation) 125 | } 126 | // Initial 127 | let renderer = ViewRenderer(view: view) 128 | context.expectedTranslation = .initial(for: .min, in: renderer.safeBounds) 129 | renderer.render() 130 | wait(for: [context.expectation], timeout: 0.3) 131 | // Max not animated 132 | context.expectation = XCTestExpectation() 133 | context.expectedTranslation = .moved(to: .max, animated: false, in: renderer.safeBounds) 134 | target.update(.max) 135 | wait(for: [context.expectation], timeout: 0.3) 136 | // Min animated 137 | context.expectation = XCTestExpectation() 138 | context.expectedTranslation = .moved(to: .min, animated: true, in: renderer.safeBounds) 139 | withAnimation { 140 | target.update(.min) 141 | } 142 | wait(for: [context.expectation], timeout: 0.3) 143 | // Max animated 144 | context.expectation = XCTestExpectation() 145 | context.expectedTranslation = .moved(to: .max, animated: true, in: renderer.safeBounds) 146 | withAnimation { 147 | target.update(.max) 148 | } 149 | wait(for: [context.expectation], timeout: 0.3) 150 | } 151 | 152 | func testLayoutChangesTranslation() { 153 | class Context { 154 | var expected = OverlayTranslation.none 155 | } 156 | let layouts: [TranslationLayout] = [.compact, .full] 157 | layouts.forEach { layout in 158 | let context = Context() 159 | let expectation = XCTestExpectation() 160 | let target = ValuePublisher(Notch.min) 161 | let view = TranslationContainerView( 162 | layout: layout, 163 | target: target, 164 | onTranslation: { translation in 165 | expectation.fulfill() 166 | self.expect(translation, toEqual: context.expected) 167 | } 168 | ) 169 | let renderer = ViewRenderer(view: view) 170 | context.expected = .layout(layout, in: renderer.safeBounds) 171 | renderer.render() 172 | wait(for: [expectation], timeout: 0.3) 173 | } 174 | } 175 | 176 | // MARK: - Private 177 | 178 | private func expect(_ lhs: OverlayTranslation, toEqual rhs: OverlayTranslation) { 179 | XCTAssertEqual(lhs.containerSize, rhs.containerSize) 180 | XCTAssertEqual(lhs.progress, rhs.progress) 181 | XCTAssertEqual(lhs.height, rhs.height) 182 | let lhsHasAnimation = lhs.transaction.animation != nil 183 | let rhsHasAnimation = rhs.transaction.animation != nil 184 | XCTAssertEqual(lhsHasAnimation, rhsHasAnimation) 185 | Notch.allCases.forEach { 186 | XCTAssertEqual(lhs.height(for: $0), rhs.height(for: $0)) 187 | } 188 | } 189 | } 190 | 191 | private extension OverlayTranslation { 192 | 193 | static var none: OverlayTranslation { 194 | OverlayTranslation(height: 0, transaction: Transaction(), progress: 0, containerSize: .zero, heightForNotch: { _ in 0 }) 195 | } 196 | 197 | static func initial(for notch: Notch, in bounds: CGRect) -> OverlayTranslation { 198 | .notch(notch, animated: false, in: bounds) 199 | } 200 | 201 | static func moved(to notch: Notch, animated: Bool, in bounds: CGRect) -> OverlayTranslation { 202 | .notch(notch, animated: animated, in: bounds) 203 | } 204 | 205 | static func layout(_ layout: TranslationLayout, in bounds: CGRect) -> OverlayTranslation { 206 | OverlayTranslation( 207 | height: Constants.height(for: .min), 208 | transaction: Transaction(), 209 | progress: 0.0, 210 | containerSize: bounds.inset(by: Constants.insets(for: layout)).size, 211 | heightForNotch: Constants.height(for:) 212 | ) 213 | } 214 | 215 | private static func notch(_ notch: Notch, animated: Bool, in bounds: CGRect) -> OverlayTranslation { 216 | OverlayTranslation( 217 | height: Constants.height(for: notch), 218 | transaction: Transaction(animation: animated ? .default : nil), 219 | progress: notch == .max ? 1.0 : 0.0, 220 | containerSize: bounds.size, 221 | heightForNotch: Constants.height(for:) 222 | ) 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /Source/Internal/OverlayContainer/OverlayContainerCoordinator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OverlayContainerCoordinator.swift 3 | // DynamicOverlay 4 | // 5 | // Created by Gaétan Zanella on 02/12/2020. 6 | // Copyright © 2020 Fabernovel. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import SwiftUI 11 | import OverlayContainer 12 | 13 | struct OverlayContainerLayout: Equatable { 14 | let indexToDimension: [Int: NotchDimension] 15 | } 16 | 17 | // (gz) 2022-01-30 `SwiftUI` compares struct properties one by one to determine either to update the view or not. 18 | // To avoid useless updates, we wrap the passive values inside this class. 19 | class OverlayContainerPassiveContainer: Equatable { 20 | 21 | var onTranslation: ((OverlayTranslation) -> Void)? 22 | var onNotchChange: ((Int) -> Void)? 23 | 24 | static func == (lhs: OverlayContainerPassiveContainer, rhs: OverlayContainerPassiveContainer) -> Bool { 25 | lhs === rhs 26 | } 27 | } 28 | 29 | struct OverlayContainerState: Equatable { 30 | let dragArea: DynamicOverlayDragArea 31 | let drivingScrollViewProxy: DynamicOverlayScrollViewProxy 32 | let notchIndex: Int? 33 | let disabledNotches: Set 34 | let layout: OverlayContainerLayout 35 | } 36 | 37 | class OverlayContainerCoordinator { 38 | 39 | private let background: UIViewController 40 | private let content: UIViewController 41 | 42 | private let indexMapper = OverlayNotchIndexMapper() 43 | 44 | typealias State = OverlayContainerState 45 | 46 | private var state: State 47 | private let style: OverlayContainerViewController.OverlayStyle 48 | private let passiveContainer: OverlayContainerPassiveContainer 49 | 50 | private var animationController: DynamicOverlayContainerAnimationController { 51 | DynamicOverlayContainerAnimationController(style: style) 52 | } 53 | 54 | // MARK: - Life Cycle 55 | 56 | init(style: OverlayContainerViewController.OverlayStyle, 57 | layout: OverlayContainerLayout, 58 | passiveContainer: OverlayContainerPassiveContainer, 59 | background: UIViewController, 60 | content: UIViewController) { 61 | self.state = .initial(layout) 62 | self.passiveContainer = passiveContainer 63 | self.background = background 64 | self.content = content 65 | self.style = style 66 | } 67 | 68 | // MARK: - Public 69 | 70 | func move(_ container: OverlayContainerViewController, to state: State, animated: Bool) { 71 | if container.viewControllers.isEmpty { 72 | container.viewControllers = [background, content] 73 | } 74 | let changes = OverlayContainerStateDiffer().diff( 75 | from: self.state, 76 | to: state 77 | ) 78 | let requiresLayoutUpdate = changes.contains(.index) || changes.contains(.layout) 79 | if requiresLayoutUpdate && animated { 80 | // we update the content first 81 | container.drivingScrollView = nil // issue #21 82 | container.view.layoutIfNeeded() 83 | } 84 | if changes.contains(.layout) { 85 | container.invalidateNotchHeights() 86 | } 87 | if let index = state.notchIndex, changes.contains(.index) { 88 | container.moveOverlay(toNotchAt: index, animated: animated) 89 | } 90 | if changes.contains(.scrollView) { 91 | CATransaction.setCompletionBlock { [weak container] in 92 | guard let overlay = container?.topViewController?.view else { return } 93 | container?.drivingScrollView = state.drivingScrollViewProxy.findScrollView(in: overlay) 94 | } 95 | } 96 | self.state = state 97 | if changes.contains(.layout) && !animated { 98 | UIView.performWithoutAnimation { 99 | container.view.layoutIfNeeded() 100 | } 101 | } 102 | } 103 | } 104 | 105 | extension OverlayContainerCoordinator: OverlayContainerViewControllerDelegate { 106 | 107 | // MARK: - OverlayContainerViewControllerDelegate 108 | 109 | func numberOfNotches(in containerViewController: OverlayContainerViewController) -> Int { 110 | indexMapper.reload( 111 | layout: state.layout, 112 | availableHeight: containerViewController.availableSpace 113 | ) 114 | return indexMapper.numberOfOverlayIndexes() 115 | } 116 | 117 | func overlayContainerViewController(_ containerViewController: OverlayContainerViewController, 118 | heightForNotchAt index: Int, 119 | availableSpace: CGFloat) -> CGFloat { 120 | indexMapper.height(forOverlayIndex: index) 121 | } 122 | 123 | func overlayContainerViewController(_ containerViewController: OverlayContainerViewController, 124 | didMoveOverlay overlayViewController: UIViewController, 125 | toNotchAt index: Int) { 126 | let newState = state.withNewNotch(index) 127 | guard newState != state else { return } 128 | passiveContainer.onNotchChange?(indexMapper.dynamicIndex(forOverlayIndex: index)) 129 | } 130 | 131 | func overlayContainerViewController(_ containerViewController: OverlayContainerViewController, 132 | willTranslateOverlay overlayViewController: UIViewController, 133 | transitionCoordinator: OverlayContainerTransitionCoordinator) { 134 | let animation = animationController.animation(using: transitionCoordinator) 135 | let transaction = Transaction(animation: animation) 136 | let translation = OverlayTranslation( 137 | height: transitionCoordinator.targetTranslationHeight, 138 | transaction: transaction, 139 | isDragging: transitionCoordinator.isDragging, 140 | translationProgress: transitionCoordinator.overallTranslationProgress(), 141 | containerFrame: containerViewController.view.frame, 142 | velocity: transitionCoordinator.velocity, 143 | heightForNotchIndex: { transitionCoordinator.height(forNotchAt: $0) } 144 | ) 145 | withTransaction(transaction) { [weak passiveContainer] in 146 | passiveContainer?.onTranslation?(translation) 147 | } 148 | } 149 | 150 | func overlayContainerViewController(_ containerViewController: OverlayContainerViewController, 151 | canReachNotchAt index: Int, 152 | forOverlay overlayViewController: UIViewController) -> Bool { 153 | !state.disabledNotches.map { indexMapper.overlayIndex(forDynamicIndex: $0) }.contains(index) 154 | } 155 | 156 | func overlayContainerViewController(_ containerViewController: OverlayContainerViewController, 157 | shouldStartDraggingOverlay overlayViewController: UIViewController, 158 | at point: CGPoint, 159 | in coordinateSpace: UICoordinateSpace) -> Bool { 160 | guard let overlay = containerViewController.topViewController else { return false } 161 | let inOverlayPoint = overlay.view.convert(point, from: coordinateSpace) 162 | if state.dragArea.isEmpty { 163 | return overlay.view.frame.contains(inOverlayPoint) 164 | } 165 | return state.dragArea.contains(inOverlayPoint) 166 | } 167 | 168 | func overlayContainerViewController(_ containerViewController: OverlayContainerViewController, 169 | transitioningDelegateForOverlay overlayViewController: UIViewController) -> OverlayTransitioningDelegate? { 170 | self 171 | } 172 | } 173 | 174 | extension OverlayContainerCoordinator: OverlayTransitioningDelegate { 175 | 176 | // MARK: - OverlayTransitioningDelegate 177 | 178 | func animationController(for overlayViewController: UIViewController) -> OverlayAnimatedTransitioning? { 179 | animationController 180 | } 181 | } 182 | 183 | private extension OverlayContainerState { 184 | 185 | static func initial(_ layout: OverlayContainerLayout) -> OverlayContainerState { 186 | OverlayContainerState( 187 | dragArea: .default, 188 | drivingScrollViewProxy: .default, 189 | notchIndex: nil, 190 | disabledNotches: [], 191 | layout: layout 192 | ) 193 | } 194 | 195 | func withNewNotch(_ notch: Int) -> OverlayContainerState { 196 | OverlayContainerState( 197 | dragArea: dragArea, 198 | drivingScrollViewProxy: drivingScrollViewProxy, 199 | notchIndex: notch, 200 | disabledNotches: disabledNotches, 201 | layout: layout 202 | ) 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | CFPropertyList (3.0.5) 5 | rexml 6 | activesupport (6.1.7) 7 | concurrent-ruby (~> 1.0, >= 1.0.2) 8 | i18n (>= 1.6, < 2) 9 | minitest (>= 5.1) 10 | tzinfo (~> 2.0) 11 | zeitwerk (~> 2.3) 12 | addressable (2.8.1) 13 | public_suffix (>= 2.0.2, < 6.0) 14 | algoliasearch (1.27.5) 15 | httpclient (~> 2.8, >= 2.8.3) 16 | json (>= 1.5.1) 17 | artifactory (3.0.15) 18 | atomos (0.1.3) 19 | aws-eventstream (1.2.0) 20 | aws-partitions (1.665.0) 21 | aws-sdk-core (3.168.1) 22 | aws-eventstream (~> 1, >= 1.0.2) 23 | aws-partitions (~> 1, >= 1.651.0) 24 | aws-sigv4 (~> 1.5) 25 | jmespath (~> 1, >= 1.6.1) 26 | aws-sdk-kms (1.59.0) 27 | aws-sdk-core (~> 3, >= 3.165.0) 28 | aws-sigv4 (~> 1.1) 29 | aws-sdk-s3 (1.117.1) 30 | aws-sdk-core (~> 3, >= 3.165.0) 31 | aws-sdk-kms (~> 1) 32 | aws-sigv4 (~> 1.4) 33 | aws-sigv4 (1.5.2) 34 | aws-eventstream (~> 1, >= 1.0.2) 35 | babosa (1.0.4) 36 | claide (1.1.0) 37 | cocoapods (1.11.3) 38 | addressable (~> 2.8) 39 | claide (>= 1.0.2, < 2.0) 40 | cocoapods-core (= 1.11.3) 41 | cocoapods-deintegrate (>= 1.0.3, < 2.0) 42 | cocoapods-downloader (>= 1.4.0, < 2.0) 43 | cocoapods-plugins (>= 1.0.0, < 2.0) 44 | cocoapods-search (>= 1.0.0, < 2.0) 45 | cocoapods-trunk (>= 1.4.0, < 2.0) 46 | cocoapods-try (>= 1.1.0, < 2.0) 47 | colored2 (~> 3.1) 48 | escape (~> 0.0.4) 49 | fourflusher (>= 2.3.0, < 3.0) 50 | gh_inspector (~> 1.0) 51 | molinillo (~> 0.8.0) 52 | nap (~> 1.0) 53 | ruby-macho (>= 1.0, < 3.0) 54 | xcodeproj (>= 1.21.0, < 2.0) 55 | cocoapods-core (1.11.3) 56 | activesupport (>= 5.0, < 7) 57 | addressable (~> 2.8) 58 | algoliasearch (~> 1.0) 59 | concurrent-ruby (~> 1.1) 60 | fuzzy_match (~> 2.0.4) 61 | nap (~> 1.0) 62 | netrc (~> 0.11) 63 | public_suffix (~> 4.0) 64 | typhoeus (~> 1.0) 65 | cocoapods-deintegrate (1.0.5) 66 | cocoapods-downloader (1.6.3) 67 | cocoapods-plugins (1.0.0) 68 | nap 69 | cocoapods-search (1.0.1) 70 | cocoapods-trunk (1.6.0) 71 | nap (>= 0.8, < 2.0) 72 | netrc (~> 0.11) 73 | cocoapods-try (1.2.0) 74 | colored (1.2) 75 | colored2 (3.1.2) 76 | commander (4.6.0) 77 | highline (~> 2.0.0) 78 | concurrent-ruby (1.1.10) 79 | declarative (0.0.20) 80 | digest-crc (0.6.4) 81 | rake (>= 12.0.0, < 14.0.0) 82 | domain_name (0.5.20190701) 83 | unf (>= 0.0.5, < 1.0.0) 84 | dotenv (2.8.1) 85 | emoji_regex (3.2.3) 86 | escape (0.0.4) 87 | ethon (0.16.0) 88 | ffi (>= 1.15.0) 89 | excon (0.94.0) 90 | faraday (1.10.2) 91 | faraday-em_http (~> 1.0) 92 | faraday-em_synchrony (~> 1.0) 93 | faraday-excon (~> 1.1) 94 | faraday-httpclient (~> 1.0) 95 | faraday-multipart (~> 1.0) 96 | faraday-net_http (~> 1.0) 97 | faraday-net_http_persistent (~> 1.0) 98 | faraday-patron (~> 1.0) 99 | faraday-rack (~> 1.0) 100 | faraday-retry (~> 1.0) 101 | ruby2_keywords (>= 0.0.4) 102 | faraday-cookie_jar (0.0.7) 103 | faraday (>= 0.8.0) 104 | http-cookie (~> 1.0.0) 105 | faraday-em_http (1.0.0) 106 | faraday-em_synchrony (1.0.0) 107 | faraday-excon (1.1.0) 108 | faraday-httpclient (1.0.1) 109 | faraday-multipart (1.0.4) 110 | multipart-post (~> 2) 111 | faraday-net_http (1.0.1) 112 | faraday-net_http_persistent (1.2.0) 113 | faraday-patron (1.0.0) 114 | faraday-rack (1.0.0) 115 | faraday-retry (1.0.3) 116 | faraday_middleware (1.2.0) 117 | faraday (~> 1.0) 118 | fastimage (2.2.6) 119 | fastlane (2.211.0) 120 | CFPropertyList (>= 2.3, < 4.0.0) 121 | addressable (>= 2.8, < 3.0.0) 122 | artifactory (~> 3.0) 123 | aws-sdk-s3 (~> 1.0) 124 | babosa (>= 1.0.3, < 2.0.0) 125 | bundler (>= 1.12.0, < 3.0.0) 126 | colored 127 | commander (~> 4.6) 128 | dotenv (>= 2.1.1, < 3.0.0) 129 | emoji_regex (>= 0.1, < 4.0) 130 | excon (>= 0.71.0, < 1.0.0) 131 | faraday (~> 1.0) 132 | faraday-cookie_jar (~> 0.0.6) 133 | faraday_middleware (~> 1.0) 134 | fastimage (>= 2.1.0, < 3.0.0) 135 | gh_inspector (>= 1.1.2, < 2.0.0) 136 | google-apis-androidpublisher_v3 (~> 0.3) 137 | google-apis-playcustomapp_v1 (~> 0.1) 138 | google-cloud-storage (~> 1.31) 139 | highline (~> 2.0) 140 | json (< 3.0.0) 141 | jwt (>= 2.1.0, < 3) 142 | mini_magick (>= 4.9.4, < 5.0.0) 143 | multipart-post (~> 2.0.0) 144 | naturally (~> 2.2) 145 | optparse (~> 0.1.1) 146 | plist (>= 3.1.0, < 4.0.0) 147 | rubyzip (>= 2.0.0, < 3.0.0) 148 | security (= 0.1.3) 149 | simctl (~> 1.6.3) 150 | terminal-notifier (>= 2.0.0, < 3.0.0) 151 | terminal-table (>= 1.4.5, < 2.0.0) 152 | tty-screen (>= 0.6.3, < 1.0.0) 153 | tty-spinner (>= 0.8.0, < 1.0.0) 154 | word_wrap (~> 1.0.0) 155 | xcodeproj (>= 1.13.0, < 2.0.0) 156 | xcpretty (~> 0.3.0) 157 | xcpretty-travis-formatter (>= 0.0.3) 158 | fastlane-plugin-changelog (0.16.0) 159 | ffi (1.15.5) 160 | fourflusher (2.3.1) 161 | fuzzy_match (2.0.4) 162 | gh_inspector (1.1.3) 163 | google-apis-androidpublisher_v3 (0.31.0) 164 | google-apis-core (>= 0.9.1, < 2.a) 165 | google-apis-core (0.9.1) 166 | addressable (~> 2.5, >= 2.5.1) 167 | googleauth (>= 0.16.2, < 2.a) 168 | httpclient (>= 2.8.1, < 3.a) 169 | mini_mime (~> 1.0) 170 | representable (~> 3.0) 171 | retriable (>= 2.0, < 4.a) 172 | rexml 173 | webrick 174 | google-apis-iamcredentials_v1 (0.16.0) 175 | google-apis-core (>= 0.9.1, < 2.a) 176 | google-apis-playcustomapp_v1 (0.12.0) 177 | google-apis-core (>= 0.9.1, < 2.a) 178 | google-apis-storage_v1 (0.19.0) 179 | google-apis-core (>= 0.9.0, < 2.a) 180 | google-cloud-core (1.6.0) 181 | google-cloud-env (~> 1.0) 182 | google-cloud-errors (~> 1.0) 183 | google-cloud-env (1.6.0) 184 | faraday (>= 0.17.3, < 3.0) 185 | google-cloud-errors (1.3.0) 186 | google-cloud-storage (1.44.0) 187 | addressable (~> 2.8) 188 | digest-crc (~> 0.4) 189 | google-apis-iamcredentials_v1 (~> 0.1) 190 | google-apis-storage_v1 (~> 0.19.0) 191 | google-cloud-core (~> 1.6) 192 | googleauth (>= 0.16.2, < 2.a) 193 | mini_mime (~> 1.0) 194 | googleauth (1.3.0) 195 | faraday (>= 0.17.3, < 3.a) 196 | jwt (>= 1.4, < 3.0) 197 | memoist (~> 0.16) 198 | multi_json (~> 1.11) 199 | os (>= 0.9, < 2.0) 200 | signet (>= 0.16, < 2.a) 201 | highline (2.0.3) 202 | http-cookie (1.0.5) 203 | domain_name (~> 0.5) 204 | httpclient (2.8.3) 205 | i18n (1.12.0) 206 | concurrent-ruby (~> 1.0) 207 | jmespath (1.6.1) 208 | json (2.6.2) 209 | jwt (2.5.0) 210 | memoist (0.16.2) 211 | mini_magick (4.11.0) 212 | mini_mime (1.1.2) 213 | minitest (5.16.3) 214 | molinillo (0.8.0) 215 | multi_json (1.15.0) 216 | multipart-post (2.0.0) 217 | nanaimo (0.3.0) 218 | nap (1.1.0) 219 | naturally (2.2.1) 220 | netrc (0.11.0) 221 | optparse (0.1.1) 222 | os (1.1.4) 223 | plist (3.6.0) 224 | public_suffix (4.0.7) 225 | rake (13.0.6) 226 | representable (3.2.0) 227 | declarative (< 0.1.0) 228 | trailblazer-option (>= 0.1.1, < 0.2.0) 229 | uber (< 0.2.0) 230 | retriable (3.1.2) 231 | rexml (3.2.5) 232 | rouge (2.0.7) 233 | ruby-macho (2.5.1) 234 | ruby2_keywords (0.0.5) 235 | rubyzip (2.3.2) 236 | security (0.1.3) 237 | signet (0.17.0) 238 | addressable (~> 2.8) 239 | faraday (>= 0.17.5, < 3.a) 240 | jwt (>= 1.5, < 3.0) 241 | multi_json (~> 1.10) 242 | simctl (1.6.8) 243 | CFPropertyList 244 | naturally 245 | terminal-notifier (2.0.0) 246 | terminal-table (1.8.0) 247 | unicode-display_width (~> 1.1, >= 1.1.1) 248 | trailblazer-option (0.1.2) 249 | tty-cursor (0.7.1) 250 | tty-screen (0.8.1) 251 | tty-spinner (0.9.3) 252 | tty-cursor (~> 0.7) 253 | typhoeus (1.4.0) 254 | ethon (>= 0.9.0) 255 | tzinfo (2.0.5) 256 | concurrent-ruby (~> 1.0) 257 | uber (0.1.0) 258 | unf (0.1.4) 259 | unf_ext 260 | unf_ext (0.0.8.2) 261 | unicode-display_width (1.8.0) 262 | webrick (1.7.0) 263 | word_wrap (1.0.0) 264 | xcodeproj (1.22.0) 265 | CFPropertyList (>= 2.3.3, < 4.0) 266 | atomos (~> 0.1.3) 267 | claide (>= 1.0.2, < 2.0) 268 | colored2 (~> 3.1) 269 | nanaimo (~> 0.3.0) 270 | rexml (~> 3.2.4) 271 | xcpretty (0.3.0) 272 | rouge (~> 2.0.7) 273 | xcpretty-travis-formatter (1.0.1) 274 | xcpretty (~> 0.2, >= 0.0.7) 275 | zeitwerk (2.6.6) 276 | 277 | PLATFORMS 278 | ruby 279 | 280 | DEPENDENCIES 281 | cocoapods (~> 1.11) 282 | fastlane (~> 2.1) 283 | fastlane-plugin-changelog 284 | 285 | BUNDLED WITH 286 | 2.3.15 287 | -------------------------------------------------------------------------------- /Tests/DynamicOverlayTests/MagneticNotchOverlayBehaviorValueTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MagneticNotchOverlayBehaviorValueTests.swift 3 | // DynamicOverlayTests 4 | // 5 | // Created by Gaétan Zanella on 16/04/2021. 6 | // Copyright © 2021 Fabernovel. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import XCTest 11 | import SwiftUI 12 | @testable import DynamicOverlay 13 | 14 | private enum Constant { 15 | 16 | static func dimension(for notch: TestNotch) -> NotchDimension { 17 | switch notch { 18 | case .min: 19 | return .fractional(0.3) 20 | case .max: 21 | return .fractional(0.5) 22 | } 23 | } 24 | } 25 | 26 | private enum TestNotch: CaseIterable, Equatable { 27 | case min, max 28 | } 29 | 30 | private typealias Behavior = MagneticNotchOverlayBehavior 31 | 32 | class MagneticNotchOverlayBehaviorValueTests: XCTestCase { 33 | 34 | func testDisabledIndexesOverlayValue() { 35 | var behavior = Behavior.empty() 36 | XCTAssertEqual(behavior.buildValue().disabledNotchIndexes, []) 37 | behavior = behavior.disable(.min) 38 | XCTAssertEqual(behavior.buildValue().disabledNotchIndexes, [0]) 39 | behavior = behavior.disable(.max) 40 | XCTAssertEqual(behavior.buildValue().disabledNotchIndexes, [0, 1]) 41 | } 42 | 43 | func testNotchChangeOverlayValue() { 44 | var behavior = Behavior.empty() 45 | XCTAssertTrue(behavior.buildValue().binding == nil) 46 | behavior = behavior.notchChange(.constant(.min)) 47 | XCTAssertTrue(behavior.buildValue().binding?.wrappedValue == 0) 48 | behavior = behavior.notchChange(.constant(.max)) 49 | XCTAssertTrue(behavior.buildValue().binding?.wrappedValue == 1) 50 | } 51 | 52 | func testNotchDimensionOverlayValue() { 53 | XCTAssertEqual( 54 | Behavior.empty().buildValue().notchDimensions, 55 | [ 56 | 0: Constant.dimension(for: .min), 57 | 1: Constant.dimension(for: .max), 58 | ] 59 | ) 60 | } 61 | 62 | func testBlockOverlayValue() { 63 | var behavior = Behavior.empty() 64 | XCTAssertTrue(behavior.buildValue().block == nil) 65 | behavior = behavior.onTranslation { _ in } 66 | XCTAssertTrue(behavior.buildValue().block != nil) 67 | behavior = behavior.onTranslation { _ in } 68 | XCTAssertTrue(behavior.buildValue().block != nil) 69 | } 70 | 71 | func testTranslationMapping() { 72 | let expectation = XCTestExpectation() 73 | let overlayTranslations = OverlayTranslation.translations() 74 | let expectedTranslations = Behavior.Translation.translations() 75 | XCTAssertEqual(overlayTranslations.count, expectedTranslations.count) 76 | expectation.expectedFulfillmentCount = overlayTranslations.count 77 | class Context { 78 | var translation: Behavior.Translation! 79 | } 80 | let context = Context() 81 | let action = { (translation: Behavior.Translation) in 82 | XCTAssertEqual(context.translation.containerSize, translation.containerSize) 83 | XCTAssertEqual(context.translation.height, translation.height) 84 | XCTAssertEqual(context.translation.progress, translation.progress) 85 | TestNotch.allCases.forEach { 86 | XCTAssertEqual(context.translation.height(for: $0), translation.height(for: $0)) 87 | } 88 | expectation.fulfill() 89 | } 90 | zip(overlayTranslations, expectedTranslations).forEach { value, translation in 91 | context.translation = translation 92 | Behavior.empty().onTranslation(action).buildValue().block?(value) 93 | } 94 | } 95 | } 96 | 97 | private extension MagneticNotchOverlayBehavior.Translation where Notch == TestNotch { 98 | 99 | static func translations() -> [Self] { 100 | [ 101 | Behavior.Translation( 102 | height: 30.0, 103 | transaction: Transaction(), 104 | progress: 0.0, 105 | containerSize: CGSize(width: 30.0, height: 30.0), 106 | heightForNotch: { 107 | switch $0 { 108 | case .max: 109 | return 400 110 | case .min: 111 | return 200 112 | } 113 | } 114 | ), 115 | Behavior.Translation( 116 | height: 30.0, 117 | transaction: Transaction(), 118 | progress: 0.0, 119 | containerSize: CGSize(width: 30.0, height: 30.0), 120 | heightForNotch: { 121 | switch $0 { 122 | case .max: 123 | return 400 124 | case .min: 125 | return 200 126 | } 127 | } 128 | ), 129 | Behavior.Translation( 130 | height: 30.0, 131 | transaction: Transaction(), 132 | progress: 0.5, 133 | containerSize: CGSize(width: 30.0, height: 30.0), 134 | heightForNotch: { 135 | switch $0 { 136 | case .max: 137 | return 400 138 | case .min: 139 | return 200 140 | } 141 | } 142 | ), 143 | Behavior.Translation( 144 | height: 10.0, 145 | transaction: Transaction(), 146 | progress: 1.0, 147 | containerSize: CGSize(width: 60.0, height: 90.0), 148 | heightForNotch: { 149 | switch $0 { 150 | case .max: 151 | return 400 152 | case .min: 153 | return 200 154 | } 155 | } 156 | ) 157 | ] 158 | } 159 | } 160 | 161 | private extension OverlayTranslation { 162 | 163 | static func translations() -> [OverlayTranslation] { 164 | [ 165 | OverlayTranslation( 166 | height: 30.0, 167 | transaction: Transaction(), 168 | isDragging: false, 169 | translationProgress: 0.0, 170 | containerFrame: CGRect(origin: .zero, size: CGSize(width: 30.0, height: 30.0)), 171 | velocity: .zero, 172 | heightForNotchIndex: { i -> CGFloat in 173 | switch i { 174 | case 0: 175 | return 200.0 176 | case 1: 177 | return 400.0 178 | default: 179 | fatalError() 180 | } 181 | } 182 | ), 183 | OverlayTranslation( 184 | height: 30.0, 185 | transaction: Transaction(), 186 | isDragging: false, 187 | translationProgress: -1.0, 188 | containerFrame: CGRect(origin: .zero, size: CGSize(width: 30.0, height: 30.0)), 189 | velocity: .zero, 190 | heightForNotchIndex: { i -> CGFloat in 191 | switch i { 192 | case 0: 193 | return 200.0 194 | case 1: 195 | return 400.0 196 | default: 197 | fatalError() 198 | } 199 | } 200 | ), 201 | OverlayTranslation( 202 | height: 30.0, 203 | transaction: Transaction(), 204 | isDragging: false, 205 | translationProgress: 0.5, 206 | containerFrame: CGRect(origin: .zero, size: CGSize(width: 30.0, height: 30.0)), 207 | velocity: .zero, 208 | heightForNotchIndex: { i -> CGFloat in 209 | switch i { 210 | case 0: 211 | return 200.0 212 | case 1: 213 | return 400.0 214 | default: 215 | fatalError() 216 | } 217 | } 218 | ), 219 | OverlayTranslation( 220 | height: 10.0, 221 | transaction: Transaction(), 222 | isDragging: false, 223 | translationProgress: 1.5, 224 | containerFrame: CGRect(origin: .zero, size: CGSize(width: 60.0, height: 90.0)), 225 | velocity: .zero, 226 | heightForNotchIndex: { i -> CGFloat in 227 | switch i { 228 | case 0: 229 | return 200.0 230 | case 1: 231 | return 400.0 232 | default: 233 | fatalError() 234 | } 235 | } 236 | ) 237 | ] 238 | } 239 | } 240 | 241 | private extension Behavior { 242 | 243 | static func empty() -> Self { 244 | Behavior { Constant.dimension(for: $0) } 245 | } 246 | } 247 | -------------------------------------------------------------------------------- /Tests/DynamicOverlayTests/OverlayContainerRepresentableAdaptorTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OverlayContainerRepresentableAdaptorTests.swift 3 | // DynamicOverlayTests 4 | // 5 | // Created by Gaétan Zanella on 20/04/2021. 6 | // Copyright © 2021 Fabernovel. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import XCTest 11 | import SwiftUI 12 | import OverlayContainer 13 | @testable import DynamicOverlay 14 | 15 | private struct AdaptorParameters { 16 | var drivingHandle: DynamicOverlayScrollViewProxy 17 | var handleValue: DynamicOverlayDragArea 18 | var disabledNotches: Set = [] 19 | var indexToDimension: [Int: NotchDimension] = [:] 20 | var onIndexChange: ((Int) -> Void)? 21 | } 22 | 23 | class OverlayContainerRepresentableAdaptorTests: XCTestCase { 24 | 25 | class Context { 26 | let container: OverlayContainerViewController 27 | let coordinator: OverlayContainerCoordinator 28 | 29 | var overlay: UIViewController { 30 | container.topViewController! 31 | } 32 | 33 | init(container: OverlayContainerViewController, 34 | coordinator: OverlayContainerCoordinator) { 35 | self.container = container 36 | self.coordinator = coordinator 37 | } 38 | 39 | func layout() { 40 | container.view.frame = CGRect(origin: .zero, size: CGSize(width: 400, height: 400)) 41 | container.view.layoutIfNeeded() 42 | } 43 | } 44 | 45 | func testViewControllersSetup() { 46 | let context = makeContext( 47 | for: AdaptorParameters( 48 | drivingHandle: .default, 49 | handleValue: .default, 50 | indexToDimension: [0: .absolute(200.0)] 51 | ) 52 | ) 53 | context.layout() 54 | XCTAssertEqual(context.container.viewControllers.count, 2) 55 | } 56 | 57 | func testDefaultDraggingStart() { 58 | let context = makeContext( 59 | for: AdaptorParameters( 60 | drivingHandle: .default, 61 | handleValue: .default, 62 | indexToDimension: [0: .absolute(200.0)] 63 | ) 64 | ) 65 | context.layout() 66 | let aboveOverlayPoint = CGPoint(x: 100, y: -10) 67 | XCTAssertEqual( 68 | context.container.delegate?.overlayContainerViewController( 69 | context.container, 70 | shouldStartDraggingOverlay: context.overlay, 71 | at: aboveOverlayPoint, 72 | in: context.overlay.view 73 | ), 74 | false 75 | ) 76 | let inOverlayPoint = CGPoint(x: 100, y: 100) 77 | XCTAssertEqual( 78 | context.container.delegate?.overlayContainerViewController( 79 | context.container, 80 | shouldStartDraggingOverlay: context.overlay, 81 | at: inOverlayPoint, 82 | in: context.overlay.view 83 | ), 84 | true 85 | ) 86 | } 87 | 88 | func testDisabledDraggingStart() { 89 | let context = makeContext( 90 | for: AdaptorParameters( 91 | drivingHandle: .default, 92 | handleValue: DynamicOverlayDragArea(area: .inactive()), 93 | indexToDimension: [0: .absolute(200.0)] 94 | ) 95 | ) 96 | context.layout() 97 | let aboveOverlayPoint = CGPoint(x: 100, y: -10) 98 | XCTAssertEqual( 99 | context.container.delegate?.overlayContainerViewController( 100 | context.container, 101 | shouldStartDraggingOverlay: context.overlay, 102 | at: aboveOverlayPoint, 103 | in: context.overlay.view 104 | ), 105 | false 106 | ) 107 | let inOverlayPoint = CGPoint(x: 100, y: 100) 108 | XCTAssertEqual( 109 | context.container.delegate?.overlayContainerViewController( 110 | context.container, 111 | shouldStartDraggingOverlay: context.overlay, 112 | at: inOverlayPoint, 113 | in: context.overlay.view 114 | ), 115 | false 116 | ) 117 | } 118 | 119 | func testEnabledDraggingStart() { 120 | let context = makeContext( 121 | for: AdaptorParameters( 122 | drivingHandle: .default, 123 | handleValue: DynamicOverlayDragArea( 124 | area: .active(CGRect(origin: .zero, size: CGSize(width: 200, height: 300))) 125 | ), 126 | indexToDimension: [0: .absolute(200.0)] 127 | ) 128 | ) 129 | context.layout() 130 | let inZonePoint = CGPoint(x: 100, y: 100) 131 | XCTAssertEqual( 132 | context.container.delegate?.overlayContainerViewController( 133 | context.container, 134 | shouldStartDraggingOverlay: context.overlay, 135 | at: inZonePoint, 136 | in: context.overlay.view 137 | ), 138 | true 139 | ) 140 | let outZonePoint = CGPoint(x: 300, y: 100) 141 | XCTAssertEqual( 142 | context.container.delegate?.overlayContainerViewController( 143 | context.container, 144 | shouldStartDraggingOverlay: context.overlay, 145 | at: outZonePoint, 146 | in: context.overlay.view 147 | ), 148 | false 149 | ) 150 | } 151 | 152 | func testOverlayMoveNotification() { 153 | var index = 0 154 | let context = makeContext( 155 | for: AdaptorParameters( 156 | drivingHandle: .default, 157 | handleValue: DynamicOverlayDragArea(area: .default), 158 | indexToDimension: [0: .absolute(200.0), 1: .absolute(300.0)], 159 | onIndexChange: { index = $0 } 160 | ) 161 | ) 162 | context.layout() 163 | context.container.moveOverlay(toNotchAt: 1, animated: false) 164 | context.layout() 165 | XCTAssertEqual(index, 1) 166 | } 167 | 168 | func testNumberOfNotches() { 169 | let dimensions: [[Int: NotchDimension]] = [ 170 | [0: .absolute(200)], 171 | [0: .absolute(200), 1: .absolute(300)], 172 | [0: .absolute(200), 1: .absolute(300), 3: .absolute(400)], 173 | ] 174 | dimensions.forEach { layout in 175 | let context = makeContext( 176 | for: AdaptorParameters( 177 | drivingHandle: .default, 178 | handleValue: .default, 179 | indexToDimension: layout 180 | ) 181 | ) 182 | context.layout() 183 | XCTAssertEqual( 184 | context.container.delegate?.numberOfNotches(in: context.container) ?? 0, 185 | layout.count 186 | ) 187 | } 188 | } 189 | 190 | func testDisabledNotches() { 191 | let all: [Int] = [0, 1, 2] 192 | let indexes: [Set] = [ 193 | [], 194 | [0], 195 | [0, 1], 196 | [0, 1, 2], 197 | [2], 198 | ] 199 | indexes.forEach { disabledIndexes in 200 | let context = makeContext( 201 | for: AdaptorParameters( 202 | drivingHandle: .default, 203 | handleValue: .default, 204 | disabledNotches: disabledIndexes, 205 | indexToDimension: Dictionary(uniqueKeysWithValues: all.map { ($0, .absolute(100 * Double($0))) }) 206 | ) 207 | ) 208 | context.layout() 209 | Set(all).subtracting(disabledIndexes).forEach { index in 210 | XCTAssertEqual( 211 | context.container.delegate?.overlayContainerViewController( 212 | context.container, 213 | canReachNotchAt: index, 214 | forOverlay: context.overlay 215 | ) ?? false, 216 | true 217 | ) 218 | } 219 | disabledIndexes.forEach { index in 220 | XCTAssertEqual( 221 | context.container.delegate?.overlayContainerViewController( 222 | context.container, 223 | canReachNotchAt: index, 224 | forOverlay: context.overlay 225 | ) ?? true, 226 | false 227 | ) 228 | } 229 | } 230 | } 231 | 232 | private func makeContext(for parameters: AdaptorParameters) -> Context { 233 | let holder = OverlayContainerPassiveContainer() 234 | holder.onNotchChange = parameters.onIndexChange 235 | let adaptor = OverlayContainerRepresentableAdaptor.init( 236 | containerState: OverlayContainerState( 237 | dragArea: parameters.handleValue, 238 | drivingScrollViewProxy: parameters.drivingHandle, 239 | notchIndex: nil, 240 | disabledNotches: parameters.disabledNotches, 241 | layout: OverlayContainerLayout(indexToDimension: parameters.indexToDimension) 242 | ), 243 | passiveContainer: holder, 244 | content: ContentView(), 245 | background: Color.green 246 | ) 247 | let coordinator = adaptor.makeCoordinator() 248 | let context = OverlayContainerRepresentableAdaptor.Context( 249 | coordinator: coordinator, 250 | transaction: Transaction() 251 | ) 252 | let container = adaptor.makeUIViewController(context: context) 253 | adaptor.updateUIViewController(container, context: context) 254 | return Context(container: container, coordinator: coordinator) 255 | } 256 | } 257 | 258 | private struct ContentView: View { 259 | 260 | var body: some View { 261 | List { Text("") } 262 | } 263 | } 264 | 265 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DynamicOverlay 2 | 3 |

4 | 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. 5 |

6 | 7 |

8 | Platform 9 | Swift5 10 | CocoaPods 11 | Carthage 12 | Build Status 13 | License 14 |

15 | 16 | --- 17 | 18 | - [Requirements](#requirements) 19 | - [Getting started](#getting-started) 20 | - [Examples](#examples) 21 | - [Magnetic notch overlay](#magnetic-notch-overlay) 22 | - [Specifying the notches](#specifying-the-notches) 23 | - [Drag gesture support](#drag-gesture-support) 24 | - [Scroll view support](#scroll-view-support) 25 | - [Responding to overlay update](#responding-to-overlay-update) 26 | - [Moving the overlay](#moving-the-overlay) 27 | - [Disabling notches](#disabling-notches) 28 | - [Installation](#installation) 29 | - [CocoaPods](#cocoapods) 30 | - [Carthage](#carthage) 31 | - [Swift Package Manager](#swift-package-manager) 32 | - [Under the hood](#under-the-hood) 33 | - [Release](#release) 34 | - [Author](#author) 35 | - [License](#license) 36 | 37 | ## Requirements 38 | 39 | `DynamicOverlay` is written in Swift 5. Compatible with iOS 13.0+. 40 | 41 | ## Getting started 42 | 43 | A dynamic overlay is an overlay that dynamically reveals or hides the content underneath it. 44 | 45 | You add a dynamic overlay as a [regular one](https://developer.apple.com/documentation/swiftui/view/overlay(_:alignment:)) using a view modifier: 46 | 47 | ```swift 48 | Color.blue.dynamicOverlay(Color.red) 49 | ``` 50 | Its behavior is defined by the `DynamicOverlayBehavior` associated to it if any. 51 | 52 | ```swift 53 | 54 | Color.blue 55 | .dynamicOverlay(Color.red) 56 | .dynamicOverlayBehavior(myOverlayBehavior) 57 | 58 | var myOverlayBehavior: some DynamicOverlayBehavior { 59 | ... 60 | } 61 | ``` 62 | If you do not specify a behavior in the overlay view hierarchy, it uses a default one. 63 | 64 | ## Examples 65 | 66 | - [Map App](https://github.com/faberNovel/DynamicOverlay/blob/main/DynamicOverlay_Example/DynamicOverlay_Example/View/MapRootView.swift) 67 | 68 | | Min | Max | 69 | | ------------------- | ------------------ | 70 | | | | 71 | 72 | ## Magnetic notch overlay 73 | 74 | `MagneticNotchOverlayBehavior` is a `DynamicOverlayBehavior` instance. It is the only behavior available for now. 75 | 76 | 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. 77 | 78 | ### Specifying the notches 79 | 80 | The preferred way to define the notches is to declare an `CaseIterable` enum: 81 | 82 | ```swift 83 | enum Notch: CaseIterable, Equatable { 84 | case min, max 85 | } 86 | ``` 87 | You specify the dimensions of each notch when you create a `MagneticNotchOverlayBehavior` instance: 88 | 89 | ```swift 90 | @State var isCompact = false 91 | 92 | var myOverlayBehavior: some DynamicOverlayBehavior { 93 | MagneticNotchOverlayBehavior { notch in 94 | switch notch { 95 | case .max: 96 | return isCompact ? .fractional(0.5) : .fractional(0.8) 97 | case .min: 98 | return .fractional(0.3) 99 | } 100 | } 101 | } 102 | ``` 103 | There are two kinds of dimension: 104 | ```swift 105 | extension NotchDimension { 106 | 107 | /// Creates a dimension with an absolute point value. 108 | static func absolute(_ value: Double) -> NotchDimension 109 | 110 | /// Creates a dimension that is computed as a fraction of the height of the overlay parent view. 111 | static func fractional(_ value: Double) -> NotchDimension 112 | } 113 | ``` 114 | ### Drag gesture support 115 | 116 | By default, all the content of the overlay is draggable but you can limit this behavior using the `draggable` view modifier. 117 | 118 | Here only the list header is draggable: 119 | 120 | ```swift 121 | var body: some View { 122 | Color.green 123 | .dynamicOverlay(myOverlayContent) 124 | .dynamicOverlayBehavior(myOverlayBehavior) 125 | } 126 | 127 | var myOverlayContent: some View { 128 | VStack { 129 | Text("Header").draggable() 130 | List { 131 | Text("Row 1") 132 | Text("Row 2") 133 | Text("Row 3") 134 | } 135 | } 136 | } 137 | 138 | var myOverlayBehavior: some DynamicOverlayBehavior { 139 | MagneticNotchOverlayBehavior { ... } 140 | } 141 | ``` 142 | Here we disable the drag gesture entirely: 143 | ```swift 144 | var myOverlayContent: some View { 145 | VStack { 146 | Text("Header") 147 | List { 148 | Text("Row 1") 149 | Text("Row 2") 150 | Text("Row 3") 151 | } 152 | } 153 | .draggable(false) 154 | } 155 | ``` 156 | 157 | ### Scroll view support 158 | 159 | A magnetic notch overlay can coordinate its motion with the scrolling of a scroll view. 160 | 161 | Mark the ScrollView or List that should dictate the overlays movement with `divingScrollView()`. 162 | 163 | ```swift 164 | var myOverlayContent: some View { 165 | VStack { 166 | Text("Header").draggable() 167 | List { 168 | Text("Row 1") 169 | Text("Row 2") 170 | Text("Row 3") 171 | } 172 | .drivingScrollView() 173 | } 174 | } 175 | ``` 176 | 177 | ### Responding to overlay updates 178 | 179 | 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. 180 | 181 | Here we define a control that should be right above the overlay: 182 | 183 | ```swift 184 | struct ControlView: View { 185 | 186 | let height: CGFloat 187 | let action: () -> Void 188 | 189 | var body: some View { 190 | VStack { 191 | Button("Action", action: action) 192 | Spacer().frame(height: height) 193 | } 194 | } 195 | } 196 | ``` 197 | We make sure the control is always visible thanks to the translation parameter: 198 | 199 | ```swift 200 | @State var height: CGFloat = 0.0 201 | 202 | var body: some View { 203 | ZStack { 204 | Color.blue 205 | ControlView(height: height, action: {}) 206 | } 207 | .dynamicOverlay(Color.red) 208 | .dynamicOverlayBehavior(myOverlayBehavior) 209 | } 210 | 211 | var myOverlayBehavior: some DynamicOverlayBehavior { 212 | MagneticNotchOverlayBehavior { ... } 213 | .onTranslation { translation in 214 | height = translation.height 215 | } 216 | } 217 | ``` 218 | You can also be notified when a notch is reached using a binding: 219 | ```swift 220 | @State var notch: Notch = .min 221 | 222 | var body: some View { 223 | Color.blue 224 | .dynamicOverlay(Text("\(notch)")) 225 | .dynamicOverlayBehavior(myOverlayBehavior) 226 | } 227 | 228 | var myOverlayBehavior: some DynamicOverlayBehavior { 229 | MagneticNotchOverlayBehavior { ... } 230 | .notchChange($notch) 231 | } 232 | ``` 233 | 234 | ### Moving the overlay 235 | 236 | You can move explicitly the overlay using a notch binding. 237 | 238 | ```swift 239 | @State var notch: Notch = .min 240 | 241 | var body: some View { 242 | ZStack { 243 | Color.green 244 | Button("Move to top") { 245 | notch = .max 246 | } 247 | } 248 | .dynamicOverlay(Color.red) 249 | .dynamicOverlayBehavior(myOverlayBehavior) 250 | } 251 | 252 | var myOverlayBehavior: some DynamicOverlayBehavior { 253 | MagneticNotchOverlayBehavior { ... } 254 | .notchChange($notch) 255 | } 256 | ``` 257 | Wrap the change in an animation block to animate the change. 258 | 259 | ```swift 260 | Button("Move to top") { 261 | withAnimation { 262 | notch = .max 263 | } 264 | } 265 | ``` 266 | 267 | ### Disabling notches 268 | 269 | When a notch is disabled, the overlay will ignore it. Here we block the overlay in its `min` position: 270 | 271 | ```swift 272 | @State var notch: Notch = .max 273 | 274 | var myOverlayBehavior: some DynamicOverlayBehavior { 275 | MagneticNotchOverlayBehavior { ... } 276 | .notchChange($notch) 277 | .disable(.max, notch == .min) 278 | } 279 | ``` 280 | 281 | ## Under the hood 282 | 283 | `DynamicOverlay` is built on top of [OverlayContainer](https://github.com/applidium/OverlayContainer). If you need more control, consider using it or open an issue. 284 | 285 | ## Installation 286 | 287 | `DynamicOverlay` is available through [CocoaPods](https://cocoapods.org). To install it, simply add the following line to your Podfile: 288 | 289 | ### Cocoapods 290 | 291 | ```ruby 292 | pod 'DynamicOverlay' 293 | ``` 294 | 295 | ### Carthage 296 | 297 | Add the following to your Cartfile: 298 | 299 | ```ruby 300 | github "https://github.com/fabernovel/DynamicOverlay" 301 | ``` 302 | 303 | ### Swift Package Manager 304 | 305 | `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: 306 | 307 | ```swift 308 | .package(url: "https://github.com/fabernovel/DynamicOverlay.git") 309 | ``` 310 | 311 | ## Release 312 | 313 | - Create a release branch for the new version (release/#version#) 314 | - Update the [CHANGELOG.md](https://github.com/faberNovel/DynamicOverlay/blob/main/CHANGELOG.md) (Be sure to spell your release version correctly) 315 | - Push your release branch 316 | - Run the [release workflow](https://github.com/faberNovel/DynamicOverlay/actions/workflows/release.yml) from your release branch 317 | 318 | ## Author 319 | 320 | [@gaetanzanella](https://twitter.com/gaetanzanella), gaetan.zanella@fabernovel.com 321 | 322 | ## License 323 | 324 | `DynamicOverlay` is available under the MIT license. See the LICENSE file for more info. 325 | -------------------------------------------------------------------------------- /DynamicOverlay_Example/DynamicOverlay_Example.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 52; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | E739AD94291D45F00076B2AC /* DynamicOverlay in Frameworks */ = {isa = PBXBuildFile; productRef = E739AD93291D45F00076B2AC /* DynamicOverlay */; }; 11 | E73A7CE9262B2A8400959344 /* MapView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E73A7CE8262B2A8400959344 /* MapView.swift */; }; 12 | E73A7CEC262B2AA400959344 /* MapRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E73A7CEB262B2AA400959344 /* MapRootView.swift */; }; 13 | E750EE4E262B2B4800E79C6B /* OverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E750EE4D262B2B4800E79C6B /* OverlayView.swift */; }; 14 | E750EE51262C30F600E79C6B /* SearchBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = E750EE50262C30F600E79C6B /* SearchBar.swift */; }; 15 | E750EE63262C463100E79C6B /* MapApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = E742E3C422302B4B002A2BED /* MapApp.swift */; }; 16 | E750EE68262C69A900E79C6B /* UIKitAppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E750EE67262C69A900E79C6B /* UIKitAppDelegate.swift */; }; 17 | E750EE96262D772700E79C6B /* OverlayBackgroundView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E750EE95262D772700E79C6B /* OverlayBackgroundView.swift */; }; 18 | E750EE98262D798F00E79C6B /* ActionCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = E750EE97262D798F00E79C6B /* ActionCell.swift */; }; 19 | E750EE9A262D799B00E79C6B /* FavoriteCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = E750EE99262D799B00E79C6B /* FavoriteCell.swift */; }; 20 | E750EE9C262D7C9000E79C6B /* BackdropView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E750EE9B262D7C9000E79C6B /* BackdropView.swift */; }; 21 | E7691574222EA78B00FDEE7F /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = E7691573222EA78B00FDEE7F /* Assets.xcassets */; }; 22 | E7691577222EA78B00FDEE7F /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = E7691575222EA78B00FDEE7F /* LaunchScreen.storyboard */; }; 23 | E79705A0292F83100047839F /* OverlayContainerRepresentableAdaptorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7970598292F83100047839F /* OverlayContainerRepresentableAdaptorTests.swift */; }; 24 | E79705A1292F83100047839F /* NotchDimensionDynamicOverlayTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7970599292F83100047839F /* NotchDimensionDynamicOverlayTests.swift */; }; 25 | E79705A2292F83100047839F /* OverlayNotchIndexMapperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E797059A292F83100047839F /* OverlayNotchIndexMapperTests.swift */; }; 26 | E79705A3292F83100047839F /* DrivingScrollViewModifierTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E797059B292F83100047839F /* DrivingScrollViewModifierTests.swift */; }; 27 | E79705A4292F83100047839F /* DragHandleViewModifierTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E797059C292F83100047839F /* DragHandleViewModifierTests.swift */; }; 28 | E79705A5292F83100047839F /* NotchTranslationDynamicOverlayTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E797059D292F83100047839F /* NotchTranslationDynamicOverlayTests.swift */; }; 29 | E79705A6292F83100047839F /* MagneticNotchOverlayBehaviorValueTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E797059E292F83100047839F /* MagneticNotchOverlayBehaviorValueTests.swift */; }; 30 | E79705A7292F83100047839F /* NotchBindingDynamicOverlayTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E797059F292F83100047839F /* NotchBindingDynamicOverlayTests.swift */; }; 31 | E79705AC292F83190047839F /* ViewInspector.swift in Sources */ = {isa = PBXBuildFile; fileRef = E79705A8292F83190047839F /* ViewInspector.swift */; }; 32 | E79705AD292F83190047839F /* ValuePublisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = E79705A9292F83190047839F /* ValuePublisher.swift */; }; 33 | E79705AE292F83190047839F /* View+Measure.swift in Sources */ = {isa = PBXBuildFile; fileRef = E79705AA292F83190047839F /* View+Measure.swift */; }; 34 | E79705AF292F83190047839F /* ViewRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = E79705AB292F83190047839F /* ViewRenderer.swift */; }; 35 | /* End PBXBuildFile section */ 36 | 37 | /* Begin PBXContainerItemProxy section */ 38 | E7970593292F817F0047839F /* PBXContainerItemProxy */ = { 39 | isa = PBXContainerItemProxy; 40 | containerPortal = E7691561222EA78A00FDEE7F /* Project object */; 41 | proxyType = 1; 42 | remoteGlobalIDString = E7691568222EA78A00FDEE7F; 43 | remoteInfo = DynamicOverlay_Example; 44 | }; 45 | /* End PBXContainerItemProxy section */ 46 | 47 | /* Begin PBXFileReference section */ 48 | E739AD92291D45B10076B2AC /* DynamicOverlay */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = DynamicOverlay; path = ..; sourceTree = ""; }; 49 | E73A7CE8262B2A8400959344 /* MapView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapView.swift; sourceTree = ""; }; 50 | E73A7CEB262B2AA400959344 /* MapRootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapRootView.swift; sourceTree = ""; }; 51 | E741EE722576B10D0073FF6B /* DynamicOverlay.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = DynamicOverlay.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 52 | E742E3C422302B4B002A2BED /* MapApp.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = MapApp.swift; path = Classes/MapApp.swift; sourceTree = ""; }; 53 | E750EE4D262B2B4800E79C6B /* OverlayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverlayView.swift; sourceTree = ""; }; 54 | E750EE50262C30F600E79C6B /* SearchBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchBar.swift; sourceTree = ""; }; 55 | E750EE67262C69A900E79C6B /* UIKitAppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIKitAppDelegate.swift; sourceTree = ""; }; 56 | E750EE95262D772700E79C6B /* OverlayBackgroundView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverlayBackgroundView.swift; sourceTree = ""; }; 57 | E750EE97262D798F00E79C6B /* ActionCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionCell.swift; sourceTree = ""; }; 58 | E750EE99262D799B00E79C6B /* FavoriteCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoriteCell.swift; sourceTree = ""; }; 59 | E750EE9B262D7C9000E79C6B /* BackdropView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackdropView.swift; sourceTree = ""; }; 60 | E7691569222EA78A00FDEE7F /* DynamicOverlay_Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = DynamicOverlay_Example.app; sourceTree = BUILT_PRODUCTS_DIR; }; 61 | E7691573222EA78B00FDEE7F /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 62 | E7691576222EA78B00FDEE7F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 63 | E7691578222EA78B00FDEE7F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 64 | E797058F292F817F0047839F /* DynamicOverlay_ExampleTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = DynamicOverlay_ExampleTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 65 | E7970598292F83100047839F /* OverlayContainerRepresentableAdaptorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = OverlayContainerRepresentableAdaptorTests.swift; path = ../../Tests/DynamicOverlayTests/OverlayContainerRepresentableAdaptorTests.swift; sourceTree = ""; }; 66 | E7970599292F83100047839F /* NotchDimensionDynamicOverlayTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = NotchDimensionDynamicOverlayTests.swift; path = ../../Tests/DynamicOverlayTests/NotchDimensionDynamicOverlayTests.swift; sourceTree = ""; }; 67 | E797059A292F83100047839F /* OverlayNotchIndexMapperTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = OverlayNotchIndexMapperTests.swift; path = ../../Tests/DynamicOverlayTests/OverlayNotchIndexMapperTests.swift; sourceTree = ""; }; 68 | E797059B292F83100047839F /* DrivingScrollViewModifierTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = DrivingScrollViewModifierTests.swift; path = ../../Tests/DynamicOverlayTests/DrivingScrollViewModifierTests.swift; sourceTree = ""; }; 69 | E797059C292F83100047839F /* DragHandleViewModifierTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = DragHandleViewModifierTests.swift; path = ../../Tests/DynamicOverlayTests/DragHandleViewModifierTests.swift; sourceTree = ""; }; 70 | E797059D292F83100047839F /* NotchTranslationDynamicOverlayTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = NotchTranslationDynamicOverlayTests.swift; path = ../../Tests/DynamicOverlayTests/NotchTranslationDynamicOverlayTests.swift; sourceTree = ""; }; 71 | E797059E292F83100047839F /* MagneticNotchOverlayBehaviorValueTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = MagneticNotchOverlayBehaviorValueTests.swift; path = ../../Tests/DynamicOverlayTests/MagneticNotchOverlayBehaviorValueTests.swift; sourceTree = ""; }; 72 | E797059F292F83100047839F /* NotchBindingDynamicOverlayTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = NotchBindingDynamicOverlayTests.swift; path = ../../Tests/DynamicOverlayTests/NotchBindingDynamicOverlayTests.swift; sourceTree = ""; }; 73 | E79705A8292F83190047839F /* ViewInspector.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ViewInspector.swift; path = ../../Tests/DynamicOverlayTests/Utils/ViewInspector.swift; sourceTree = ""; }; 74 | E79705A9292F83190047839F /* ValuePublisher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ValuePublisher.swift; path = ../../Tests/DynamicOverlayTests/Utils/ValuePublisher.swift; sourceTree = ""; }; 75 | E79705AA292F83190047839F /* View+Measure.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "View+Measure.swift"; path = "../../Tests/DynamicOverlayTests/Utils/View+Measure.swift"; sourceTree = ""; }; 76 | E79705AB292F83190047839F /* ViewRenderer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ViewRenderer.swift; path = ../../Tests/DynamicOverlayTests/Utils/ViewRenderer.swift; sourceTree = ""; }; 77 | /* End PBXFileReference section */ 78 | 79 | /* Begin PBXFrameworksBuildPhase section */ 80 | E7691566222EA78A00FDEE7F /* Frameworks */ = { 81 | isa = PBXFrameworksBuildPhase; 82 | buildActionMask = 2147483647; 83 | files = ( 84 | E739AD94291D45F00076B2AC /* DynamicOverlay in Frameworks */, 85 | ); 86 | runOnlyForDeploymentPostprocessing = 0; 87 | }; 88 | E797058C292F817F0047839F /* Frameworks */ = { 89 | isa = PBXFrameworksBuildPhase; 90 | buildActionMask = 2147483647; 91 | files = ( 92 | ); 93 | runOnlyForDeploymentPostprocessing = 0; 94 | }; 95 | /* End PBXFrameworksBuildPhase section */ 96 | 97 | /* Begin PBXGroup section */ 98 | E739AD91291D45B10076B2AC /* Packages */ = { 99 | isa = PBXGroup; 100 | children = ( 101 | E739AD92291D45B10076B2AC /* DynamicOverlay */, 102 | ); 103 | name = Packages; 104 | sourceTree = ""; 105 | }; 106 | E73A7CEA262B2A8B00959344 /* View */ = { 107 | isa = PBXGroup; 108 | children = ( 109 | E73A7CEB262B2AA400959344 /* MapRootView.swift */, 110 | E73A7CE8262B2A8400959344 /* MapView.swift */, 111 | E750EE4D262B2B4800E79C6B /* OverlayView.swift */, 112 | E750EE50262C30F600E79C6B /* SearchBar.swift */, 113 | E750EE95262D772700E79C6B /* OverlayBackgroundView.swift */, 114 | E750EE97262D798F00E79C6B /* ActionCell.swift */, 115 | E750EE99262D799B00E79C6B /* FavoriteCell.swift */, 116 | E750EE9B262D7C9000E79C6B /* BackdropView.swift */, 117 | ); 118 | path = View; 119 | sourceTree = ""; 120 | }; 121 | E741EE712576B10D0073FF6B /* Frameworks */ = { 122 | isa = PBXGroup; 123 | children = ( 124 | E741EE722576B10D0073FF6B /* DynamicOverlay.framework */, 125 | ); 126 | name = Frameworks; 127 | sourceTree = ""; 128 | }; 129 | E750EE66262C699C00E79C6B /* UIKit */ = { 130 | isa = PBXGroup; 131 | children = ( 132 | E750EE67262C69A900E79C6B /* UIKitAppDelegate.swift */, 133 | ); 134 | path = UIKit; 135 | sourceTree = ""; 136 | }; 137 | E7691560222EA78A00FDEE7F = { 138 | isa = PBXGroup; 139 | children = ( 140 | E739AD91291D45B10076B2AC /* Packages */, 141 | E769156B222EA78A00FDEE7F /* DynamicOverlay_Example */, 142 | E7970590292F817F0047839F /* DynamicOverlay_ExampleTests */, 143 | E769156A222EA78A00FDEE7F /* Products */, 144 | E741EE712576B10D0073FF6B /* Frameworks */, 145 | ); 146 | sourceTree = ""; 147 | }; 148 | E769156A222EA78A00FDEE7F /* Products */ = { 149 | isa = PBXGroup; 150 | children = ( 151 | E7691569222EA78A00FDEE7F /* DynamicOverlay_Example.app */, 152 | E797058F292F817F0047839F /* DynamicOverlay_ExampleTests.xctest */, 153 | ); 154 | name = Products; 155 | sourceTree = ""; 156 | }; 157 | E769156B222EA78A00FDEE7F /* DynamicOverlay_Example */ = { 158 | isa = PBXGroup; 159 | children = ( 160 | E742E3C422302B4B002A2BED /* MapApp.swift */, 161 | E750EE66262C699C00E79C6B /* UIKit */, 162 | E73A7CEA262B2A8B00959344 /* View */, 163 | E7691582222EA7C100FDEE7F /* Resources */, 164 | E7691581222EA7B400FDEE7F /* Configuration */, 165 | ); 166 | path = DynamicOverlay_Example; 167 | sourceTree = ""; 168 | }; 169 | E7691581222EA7B400FDEE7F /* Configuration */ = { 170 | isa = PBXGroup; 171 | children = ( 172 | E7691578222EA78B00FDEE7F /* Info.plist */, 173 | ); 174 | path = Configuration; 175 | sourceTree = ""; 176 | }; 177 | E7691582222EA7C100FDEE7F /* Resources */ = { 178 | isa = PBXGroup; 179 | children = ( 180 | E7691575222EA78B00FDEE7F /* LaunchScreen.storyboard */, 181 | E7691573222EA78B00FDEE7F /* Assets.xcassets */, 182 | ); 183 | path = Resources; 184 | sourceTree = ""; 185 | }; 186 | E7970590292F817F0047839F /* DynamicOverlay_ExampleTests */ = { 187 | isa = PBXGroup; 188 | children = ( 189 | E797059C292F83100047839F /* DragHandleViewModifierTests.swift */, 190 | E797059B292F83100047839F /* DrivingScrollViewModifierTests.swift */, 191 | E797059E292F83100047839F /* MagneticNotchOverlayBehaviorValueTests.swift */, 192 | E797059F292F83100047839F /* NotchBindingDynamicOverlayTests.swift */, 193 | E7970599292F83100047839F /* NotchDimensionDynamicOverlayTests.swift */, 194 | E797059D292F83100047839F /* NotchTranslationDynamicOverlayTests.swift */, 195 | E7970598292F83100047839F /* OverlayContainerRepresentableAdaptorTests.swift */, 196 | E797059A292F83100047839F /* OverlayNotchIndexMapperTests.swift */, 197 | E79705A9292F83190047839F /* ValuePublisher.swift */, 198 | E79705AA292F83190047839F /* View+Measure.swift */, 199 | E79705A8292F83190047839F /* ViewInspector.swift */, 200 | E79705AB292F83190047839F /* ViewRenderer.swift */, 201 | ); 202 | path = DynamicOverlay_ExampleTests; 203 | sourceTree = ""; 204 | }; 205 | /* End PBXGroup section */ 206 | 207 | /* Begin PBXNativeTarget section */ 208 | E7691568222EA78A00FDEE7F /* DynamicOverlay_Example */ = { 209 | isa = PBXNativeTarget; 210 | buildConfigurationList = E769157B222EA78B00FDEE7F /* Build configuration list for PBXNativeTarget "DynamicOverlay_Example" */; 211 | buildPhases = ( 212 | E7691565222EA78A00FDEE7F /* Sources */, 213 | E7691566222EA78A00FDEE7F /* Frameworks */, 214 | E7691567222EA78A00FDEE7F /* Resources */, 215 | ); 216 | buildRules = ( 217 | ); 218 | dependencies = ( 219 | ); 220 | name = DynamicOverlay_Example; 221 | packageProductDependencies = ( 222 | E739AD93291D45F00076B2AC /* DynamicOverlay */, 223 | ); 224 | productName = DynamicOverlay_Example; 225 | productReference = E7691569222EA78A00FDEE7F /* DynamicOverlay_Example.app */; 226 | productType = "com.apple.product-type.application"; 227 | }; 228 | E797058E292F817F0047839F /* DynamicOverlay_ExampleTests */ = { 229 | isa = PBXNativeTarget; 230 | buildConfigurationList = E7970595292F817F0047839F /* Build configuration list for PBXNativeTarget "DynamicOverlay_ExampleTests" */; 231 | buildPhases = ( 232 | E797058B292F817F0047839F /* Sources */, 233 | E797058C292F817F0047839F /* Frameworks */, 234 | E797058D292F817F0047839F /* Resources */, 235 | ); 236 | buildRules = ( 237 | ); 238 | dependencies = ( 239 | E7970594292F817F0047839F /* PBXTargetDependency */, 240 | ); 241 | name = DynamicOverlay_ExampleTests; 242 | productName = DynamicOverlay_ExampleTests; 243 | productReference = E797058F292F817F0047839F /* DynamicOverlay_ExampleTests.xctest */; 244 | productType = "com.apple.product-type.bundle.unit-test"; 245 | }; 246 | /* End PBXNativeTarget section */ 247 | 248 | /* Begin PBXProject section */ 249 | E7691561222EA78A00FDEE7F /* Project object */ = { 250 | isa = PBXProject; 251 | attributes = { 252 | LastSwiftUpdateCheck = 1410; 253 | LastUpgradeCheck = 1220; 254 | ORGANIZATIONNAME = Fabernovel; 255 | TargetAttributes = { 256 | E7691568222EA78A00FDEE7F = { 257 | CreatedOnToolsVersion = 10.1; 258 | }; 259 | E797058E292F817F0047839F = { 260 | CreatedOnToolsVersion = 14.1; 261 | LastSwiftMigration = 1410; 262 | TestTargetID = E7691568222EA78A00FDEE7F; 263 | }; 264 | }; 265 | }; 266 | buildConfigurationList = E7691564222EA78A00FDEE7F /* Build configuration list for PBXProject "DynamicOverlay_Example" */; 267 | compatibilityVersion = "Xcode 9.3"; 268 | developmentRegion = en; 269 | hasScannedForEncodings = 0; 270 | knownRegions = ( 271 | en, 272 | Base, 273 | ); 274 | mainGroup = E7691560222EA78A00FDEE7F; 275 | productRefGroup = E769156A222EA78A00FDEE7F /* Products */; 276 | projectDirPath = ""; 277 | projectRoot = ""; 278 | targets = ( 279 | E7691568222EA78A00FDEE7F /* DynamicOverlay_Example */, 280 | E797058E292F817F0047839F /* DynamicOverlay_ExampleTests */, 281 | ); 282 | }; 283 | /* End PBXProject section */ 284 | 285 | /* Begin PBXResourcesBuildPhase section */ 286 | E7691567222EA78A00FDEE7F /* Resources */ = { 287 | isa = PBXResourcesBuildPhase; 288 | buildActionMask = 2147483647; 289 | files = ( 290 | E7691577222EA78B00FDEE7F /* LaunchScreen.storyboard in Resources */, 291 | E7691574222EA78B00FDEE7F /* Assets.xcassets in Resources */, 292 | ); 293 | runOnlyForDeploymentPostprocessing = 0; 294 | }; 295 | E797058D292F817F0047839F /* Resources */ = { 296 | isa = PBXResourcesBuildPhase; 297 | buildActionMask = 2147483647; 298 | files = ( 299 | ); 300 | runOnlyForDeploymentPostprocessing = 0; 301 | }; 302 | /* End PBXResourcesBuildPhase section */ 303 | 304 | /* Begin PBXSourcesBuildPhase section */ 305 | E7691565222EA78A00FDEE7F /* Sources */ = { 306 | isa = PBXSourcesBuildPhase; 307 | buildActionMask = 2147483647; 308 | files = ( 309 | E750EE68262C69A900E79C6B /* UIKitAppDelegate.swift in Sources */, 310 | E750EE98262D798F00E79C6B /* ActionCell.swift in Sources */, 311 | E73A7CEC262B2AA400959344 /* MapRootView.swift in Sources */, 312 | E73A7CE9262B2A8400959344 /* MapView.swift in Sources */, 313 | E750EE9A262D799B00E79C6B /* FavoriteCell.swift in Sources */, 314 | E750EE4E262B2B4800E79C6B /* OverlayView.swift in Sources */, 315 | E750EE63262C463100E79C6B /* MapApp.swift in Sources */, 316 | E750EE96262D772700E79C6B /* OverlayBackgroundView.swift in Sources */, 317 | E750EE9C262D7C9000E79C6B /* BackdropView.swift in Sources */, 318 | E750EE51262C30F600E79C6B /* SearchBar.swift in Sources */, 319 | ); 320 | runOnlyForDeploymentPostprocessing = 0; 321 | }; 322 | E797058B292F817F0047839F /* Sources */ = { 323 | isa = PBXSourcesBuildPhase; 324 | buildActionMask = 2147483647; 325 | files = ( 326 | E79705A2292F83100047839F /* OverlayNotchIndexMapperTests.swift in Sources */, 327 | E79705A7292F83100047839F /* NotchBindingDynamicOverlayTests.swift in Sources */, 328 | E79705AD292F83190047839F /* ValuePublisher.swift in Sources */, 329 | E79705A1292F83100047839F /* NotchDimensionDynamicOverlayTests.swift in Sources */, 330 | E79705A4292F83100047839F /* DragHandleViewModifierTests.swift in Sources */, 331 | E79705A3292F83100047839F /* DrivingScrollViewModifierTests.swift in Sources */, 332 | E79705A0292F83100047839F /* OverlayContainerRepresentableAdaptorTests.swift in Sources */, 333 | E79705AF292F83190047839F /* ViewRenderer.swift in Sources */, 334 | E79705A5292F83100047839F /* NotchTranslationDynamicOverlayTests.swift in Sources */, 335 | E79705AC292F83190047839F /* ViewInspector.swift in Sources */, 336 | E79705A6292F83100047839F /* MagneticNotchOverlayBehaviorValueTests.swift in Sources */, 337 | E79705AE292F83190047839F /* View+Measure.swift in Sources */, 338 | ); 339 | runOnlyForDeploymentPostprocessing = 0; 340 | }; 341 | /* End PBXSourcesBuildPhase section */ 342 | 343 | /* Begin PBXTargetDependency section */ 344 | E7970594292F817F0047839F /* PBXTargetDependency */ = { 345 | isa = PBXTargetDependency; 346 | target = E7691568222EA78A00FDEE7F /* DynamicOverlay_Example */; 347 | targetProxy = E7970593292F817F0047839F /* PBXContainerItemProxy */; 348 | }; 349 | /* End PBXTargetDependency section */ 350 | 351 | /* Begin PBXVariantGroup section */ 352 | E7691575222EA78B00FDEE7F /* LaunchScreen.storyboard */ = { 353 | isa = PBXVariantGroup; 354 | children = ( 355 | E7691576222EA78B00FDEE7F /* Base */, 356 | ); 357 | name = LaunchScreen.storyboard; 358 | sourceTree = ""; 359 | }; 360 | /* End PBXVariantGroup section */ 361 | 362 | /* Begin XCBuildConfiguration section */ 363 | E7691579222EA78B00FDEE7F /* Debug */ = { 364 | isa = XCBuildConfiguration; 365 | buildSettings = { 366 | ALWAYS_SEARCH_USER_PATHS = NO; 367 | CLANG_ANALYZER_NONNULL = YES; 368 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 369 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 370 | CLANG_CXX_LIBRARY = "libc++"; 371 | CLANG_ENABLE_MODULES = YES; 372 | CLANG_ENABLE_OBJC_ARC = YES; 373 | CLANG_ENABLE_OBJC_WEAK = YES; 374 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 375 | CLANG_WARN_BOOL_CONVERSION = YES; 376 | CLANG_WARN_COMMA = YES; 377 | CLANG_WARN_CONSTANT_CONVERSION = YES; 378 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 379 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 380 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 381 | CLANG_WARN_EMPTY_BODY = YES; 382 | CLANG_WARN_ENUM_CONVERSION = YES; 383 | CLANG_WARN_INFINITE_RECURSION = YES; 384 | CLANG_WARN_INT_CONVERSION = YES; 385 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 386 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 387 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 388 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 389 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 390 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 391 | CLANG_WARN_STRICT_PROTOTYPES = YES; 392 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 393 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 394 | CLANG_WARN_UNREACHABLE_CODE = YES; 395 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 396 | CODE_SIGN_IDENTITY = "iPhone Developer"; 397 | COPY_PHASE_STRIP = NO; 398 | DEBUG_INFORMATION_FORMAT = dwarf; 399 | ENABLE_STRICT_OBJC_MSGSEND = YES; 400 | ENABLE_TESTABILITY = YES; 401 | GCC_C_LANGUAGE_STANDARD = gnu11; 402 | GCC_DYNAMIC_NO_PIC = NO; 403 | GCC_NO_COMMON_BLOCKS = YES; 404 | GCC_OPTIMIZATION_LEVEL = 0; 405 | GCC_PREPROCESSOR_DEFINITIONS = ( 406 | "DEBUG=1", 407 | "$(inherited)", 408 | ); 409 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 410 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 411 | GCC_WARN_UNDECLARED_SELECTOR = YES; 412 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 413 | GCC_WARN_UNUSED_FUNCTION = YES; 414 | GCC_WARN_UNUSED_VARIABLE = YES; 415 | IPHONEOS_DEPLOYMENT_TARGET = 15.0; 416 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 417 | MTL_FAST_MATH = YES; 418 | ONLY_ACTIVE_ARCH = YES; 419 | SDKROOT = iphoneos; 420 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 421 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 422 | }; 423 | name = Debug; 424 | }; 425 | E769157A222EA78B00FDEE7F /* Release */ = { 426 | isa = XCBuildConfiguration; 427 | buildSettings = { 428 | ALWAYS_SEARCH_USER_PATHS = NO; 429 | CLANG_ANALYZER_NONNULL = YES; 430 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 431 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 432 | CLANG_CXX_LIBRARY = "libc++"; 433 | CLANG_ENABLE_MODULES = YES; 434 | CLANG_ENABLE_OBJC_ARC = YES; 435 | CLANG_ENABLE_OBJC_WEAK = YES; 436 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 437 | CLANG_WARN_BOOL_CONVERSION = YES; 438 | CLANG_WARN_COMMA = YES; 439 | CLANG_WARN_CONSTANT_CONVERSION = YES; 440 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 441 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 442 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 443 | CLANG_WARN_EMPTY_BODY = YES; 444 | CLANG_WARN_ENUM_CONVERSION = YES; 445 | CLANG_WARN_INFINITE_RECURSION = YES; 446 | CLANG_WARN_INT_CONVERSION = YES; 447 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 448 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 449 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 450 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 451 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 452 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 453 | CLANG_WARN_STRICT_PROTOTYPES = YES; 454 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 455 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 456 | CLANG_WARN_UNREACHABLE_CODE = YES; 457 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 458 | CODE_SIGN_IDENTITY = "iPhone Developer"; 459 | COPY_PHASE_STRIP = NO; 460 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 461 | ENABLE_NS_ASSERTIONS = NO; 462 | ENABLE_STRICT_OBJC_MSGSEND = YES; 463 | GCC_C_LANGUAGE_STANDARD = gnu11; 464 | GCC_NO_COMMON_BLOCKS = YES; 465 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 466 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 467 | GCC_WARN_UNDECLARED_SELECTOR = YES; 468 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 469 | GCC_WARN_UNUSED_FUNCTION = YES; 470 | GCC_WARN_UNUSED_VARIABLE = YES; 471 | IPHONEOS_DEPLOYMENT_TARGET = 15.0; 472 | MTL_ENABLE_DEBUG_INFO = NO; 473 | MTL_FAST_MATH = YES; 474 | SDKROOT = iphoneos; 475 | SWIFT_COMPILATION_MODE = wholemodule; 476 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 477 | VALIDATE_PRODUCT = YES; 478 | }; 479 | name = Release; 480 | }; 481 | E769157C222EA78B00FDEE7F /* Debug */ = { 482 | isa = XCBuildConfiguration; 483 | buildSettings = { 484 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 485 | CLANG_ENABLE_MODULES = YES; 486 | CODE_SIGN_STYLE = Automatic; 487 | INFOPLIST_FILE = DynamicOverlay_Example/Configuration/Info.plist; 488 | IPHONEOS_DEPLOYMENT_TARGET = 15; 489 | LD_RUNPATH_SEARCH_PATHS = ( 490 | "$(inherited)", 491 | "@executable_path/Frameworks", 492 | ); 493 | PRODUCT_BUNDLE_IDENTIFIER = "com.fabernovel.DynamicOverlay-Example"; 494 | PRODUCT_NAME = "$(TARGET_NAME)"; 495 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 496 | SWIFT_VERSION = 4.2; 497 | TARGETED_DEVICE_FAMILY = "1,2"; 498 | }; 499 | name = Debug; 500 | }; 501 | E769157D222EA78B00FDEE7F /* Release */ = { 502 | isa = XCBuildConfiguration; 503 | buildSettings = { 504 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 505 | CLANG_ENABLE_MODULES = YES; 506 | CODE_SIGN_STYLE = Automatic; 507 | INFOPLIST_FILE = DynamicOverlay_Example/Configuration/Info.plist; 508 | IPHONEOS_DEPLOYMENT_TARGET = 15; 509 | LD_RUNPATH_SEARCH_PATHS = ( 510 | "$(inherited)", 511 | "@executable_path/Frameworks", 512 | ); 513 | PRODUCT_BUNDLE_IDENTIFIER = "com.fabernovel.DynamicOverlay-Example"; 514 | PRODUCT_NAME = "$(TARGET_NAME)"; 515 | SWIFT_VERSION = 4.2; 516 | TARGETED_DEVICE_FAMILY = "1,2"; 517 | }; 518 | name = Release; 519 | }; 520 | E7970596292F817F0047839F /* Debug */ = { 521 | isa = XCBuildConfiguration; 522 | buildSettings = { 523 | BUNDLE_LOADER = "$(TEST_HOST)"; 524 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 525 | CLANG_ENABLE_MODULES = YES; 526 | CODE_SIGN_STYLE = Automatic; 527 | CURRENT_PROJECT_VERSION = 1; 528 | DEVELOPMENT_TEAM = C7G63Q6LZ9; 529 | GENERATE_INFOPLIST_FILE = YES; 530 | IPHONEOS_DEPLOYMENT_TARGET = 16.0; 531 | LD_RUNPATH_SEARCH_PATHS = ( 532 | "$(inherited)", 533 | "@executable_path/Frameworks", 534 | "@loader_path/Frameworks", 535 | ); 536 | MARKETING_VERSION = 1.0; 537 | PRODUCT_BUNDLE_IDENTIFIER = "com.gaetanzanella.DynamicOverlay-ExampleTests"; 538 | PRODUCT_NAME = "$(TARGET_NAME)"; 539 | SWIFT_EMIT_LOC_STRINGS = NO; 540 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 541 | SWIFT_VERSION = 5.0; 542 | TARGETED_DEVICE_FAMILY = "1,2"; 543 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/DynamicOverlay_Example.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/DynamicOverlay_Example"; 544 | }; 545 | name = Debug; 546 | }; 547 | E7970597292F817F0047839F /* Release */ = { 548 | isa = XCBuildConfiguration; 549 | buildSettings = { 550 | BUNDLE_LOADER = "$(TEST_HOST)"; 551 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 552 | CLANG_ENABLE_MODULES = YES; 553 | CODE_SIGN_STYLE = Automatic; 554 | CURRENT_PROJECT_VERSION = 1; 555 | DEVELOPMENT_TEAM = C7G63Q6LZ9; 556 | GENERATE_INFOPLIST_FILE = YES; 557 | IPHONEOS_DEPLOYMENT_TARGET = 16.0; 558 | LD_RUNPATH_SEARCH_PATHS = ( 559 | "$(inherited)", 560 | "@executable_path/Frameworks", 561 | "@loader_path/Frameworks", 562 | ); 563 | MARKETING_VERSION = 1.0; 564 | PRODUCT_BUNDLE_IDENTIFIER = "com.gaetanzanella.DynamicOverlay-ExampleTests"; 565 | PRODUCT_NAME = "$(TARGET_NAME)"; 566 | SWIFT_EMIT_LOC_STRINGS = NO; 567 | SWIFT_VERSION = 5.0; 568 | TARGETED_DEVICE_FAMILY = "1,2"; 569 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/DynamicOverlay_Example.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/DynamicOverlay_Example"; 570 | }; 571 | name = Release; 572 | }; 573 | /* End XCBuildConfiguration section */ 574 | 575 | /* Begin XCConfigurationList section */ 576 | E7691564222EA78A00FDEE7F /* Build configuration list for PBXProject "DynamicOverlay_Example" */ = { 577 | isa = XCConfigurationList; 578 | buildConfigurations = ( 579 | E7691579222EA78B00FDEE7F /* Debug */, 580 | E769157A222EA78B00FDEE7F /* Release */, 581 | ); 582 | defaultConfigurationIsVisible = 0; 583 | defaultConfigurationName = Release; 584 | }; 585 | E769157B222EA78B00FDEE7F /* Build configuration list for PBXNativeTarget "DynamicOverlay_Example" */ = { 586 | isa = XCConfigurationList; 587 | buildConfigurations = ( 588 | E769157C222EA78B00FDEE7F /* Debug */, 589 | E769157D222EA78B00FDEE7F /* Release */, 590 | ); 591 | defaultConfigurationIsVisible = 0; 592 | defaultConfigurationName = Release; 593 | }; 594 | E7970595292F817F0047839F /* Build configuration list for PBXNativeTarget "DynamicOverlay_ExampleTests" */ = { 595 | isa = XCConfigurationList; 596 | buildConfigurations = ( 597 | E7970596292F817F0047839F /* Debug */, 598 | E7970597292F817F0047839F /* Release */, 599 | ); 600 | defaultConfigurationIsVisible = 0; 601 | defaultConfigurationName = Release; 602 | }; 603 | /* End XCConfigurationList section */ 604 | 605 | /* Begin XCSwiftPackageProductDependency section */ 606 | E739AD93291D45F00076B2AC /* DynamicOverlay */ = { 607 | isa = XCSwiftPackageProductDependency; 608 | productName = DynamicOverlay; 609 | }; 610 | /* End XCSwiftPackageProductDependency section */ 611 | }; 612 | rootObject = E7691561222EA78A00FDEE7F /* Project object */; 613 | } 614 | --------------------------------------------------------------------------------