├── .gitignore ├── Demo ├── App │ ├── AppNotificationViewController.swift │ ├── AppOtherViewController.swift │ ├── AppRootViewController.swift │ ├── AppSearchViewController.swift │ ├── AppTabBarController.swift │ └── CustomNavigatedFluidViewController.swift ├── AppDelegate.swift ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── Contents.json ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard ├── ContentView.swift ├── Info.plist ├── NavigationSampleViewController.swift ├── SampleViewController.swift ├── Texture │ ├── Components.swift │ ├── StackScrollNode.swift │ └── StackScrollNodeViewController.swift └── ViewController.swift ├── FluidPresentation.podspec ├── FluidPresentation.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── xcshareddata │ └── xcschemes │ └── Demo.xcscheme ├── FluidPresentation.xcworkspace ├── contents.xcworkspacedata └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── FluidPresentation ├── Context.swift ├── FluidViewController.h ├── FluidViewController.swift ├── Info.plist ├── Log.swift ├── NavigatedFluidViewController.swift ├── ScrollView+Handling.swift └── TransitionControllers.swift ├── FluidPresentationTests ├── FluidPresentationTests.swift ├── Info.plist └── NavigationItemKVOTests.swift ├── LICENSE ├── Podfile ├── Podfile.lock ├── PresentationViewController.xcworkspace ├── contents.xcworkspacedata └── xcshareddata │ └── IDEWorkspaceChecks.plist └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.toptal.com/developers/gitignore/api/swift 3 | # Edit at https://www.toptal.com/developers/gitignore?templates=swift 4 | 5 | ### Swift ### 6 | # Xcode 7 | # 8 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 9 | 10 | ## User settings 11 | xcuserdata/ 12 | 13 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 14 | *.xcscmblueprint 15 | *.xccheckout 16 | 17 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 18 | build/ 19 | DerivedData/ 20 | *.moved-aside 21 | *.pbxuser 22 | !default.pbxuser 23 | *.mode1v3 24 | !default.mode1v3 25 | *.mode2v3 26 | !default.mode2v3 27 | *.perspectivev3 28 | !default.perspectivev3 29 | 30 | ## Obj-C/Swift specific 31 | *.hmap 32 | 33 | ## App packaging 34 | *.ipa 35 | *.dSYM.zip 36 | *.dSYM 37 | 38 | ## Playgrounds 39 | timeline.xctimeline 40 | playground.xcworkspace 41 | 42 | # Swift Package Manager 43 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 44 | # Packages/ 45 | # Package.pins 46 | # Package.resolved 47 | # *.xcodeproj 48 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 49 | # hence it is not needed unless you have added a package configuration file to your project 50 | # .swiftpm 51 | 52 | .build/ 53 | 54 | # CocoaPods 55 | # We recommend against adding the Pods directory to your .gitignore. However 56 | # you should judge for yourself, the pros and cons are mentioned at: 57 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 58 | Pods/ 59 | # Add this line if you want to avoid checking in source code from the Xcode workspace 60 | # *.xcworkspace 61 | 62 | # Carthage 63 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 64 | # Carthage/Checkouts 65 | 66 | Carthage/Build/ 67 | 68 | # Accio dependency management 69 | Dependencies/ 70 | .accio/ 71 | 72 | # fastlane 73 | # It is recommended to not store the screenshots in the git repo. 74 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 75 | # For more information about the recommended setup visit: 76 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 77 | 78 | fastlane/report.xml 79 | fastlane/Preview.html 80 | fastlane/screenshots/**/*.png 81 | fastlane/test_output 82 | 83 | # Code Injection 84 | # After new code Injection tools there's a generated folder /iOSInjectionProject 85 | # https://github.com/johnno1962/injectionforxcode 86 | 87 | iOSInjectionProject/ 88 | 89 | .DS_Store 90 | # End of https://www.toptal.com/developers/gitignore/api/swift 91 | -------------------------------------------------------------------------------- /Demo/App/AppNotificationViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppNotificationViewController.swift 3 | // Demo 4 | // 5 | // Created by Muukii on 2021/04/14. 6 | // 7 | 8 | import Foundation 9 | 10 | final class AppNotificationController: StackScrollNodeViewController { 11 | 12 | override func viewDidLoad() { 13 | super.viewDidLoad() 14 | 15 | stackScrollNode.append(nodes: [ 16 | Components.makeTitleCell(title: "Notifications") 17 | ]) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Demo/App/AppOtherViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppOtherViewController.swift 3 | // Demo 4 | // 5 | // Created by Muukii on 2021/04/14. 6 | // 7 | 8 | import UIKit 9 | import TextureSwiftSupport 10 | import FluidPresentation 11 | 12 | final class AppOtherController: StackScrollNodeViewController { 13 | 14 | override func viewDidLoad() { 15 | 16 | super.viewDidLoad() 17 | 18 | definesPresentationContext = true 19 | 20 | stackScrollNode.append(nodes: [ 21 | Components.makeTitleCell(title: "Other"), 22 | 23 | Components.makeSelectionCell(title: "Open", onTap: { [unowned self] in 24 | 25 | let controller = AppNotificationController().wrappingNavigatedFluidViewController(idiom: .navigationPush) 26 | 27 | controller.dismissingInteractions = [.init(trigger: .screen, startFrom: .left)] 28 | 29 | controller.modalPresentationStyle = .currentContext 30 | 31 | self.present(controller, animated: true, completion: nil) 32 | }) 33 | ]) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Demo/App/AppRootViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RootViewController.swift 3 | // Demo 4 | // 5 | // Created by Muukii on 2021/04/14. 6 | // 7 | 8 | import Foundation 9 | import TextureSwiftSupport 10 | import TinyConstraints 11 | 12 | final class AppRootViewController: UIViewController { 13 | 14 | override func viewDidLoad() { 15 | super.viewDidLoad() 16 | view.backgroundColor = .white 17 | 18 | let tab = AppTabBarController() 19 | addChild(tab) 20 | view.addSubview(tab.view) 21 | tab.view.edgesToSuperview() 22 | tab.didMove(toParent: self) 23 | 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Demo/App/AppSearchViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppOtherViewController.swift 3 | // Demo 4 | // 5 | // Created by Muukii on 2021/04/14. 6 | // 7 | 8 | import UIKit 9 | import TextureSwiftSupport 10 | 11 | final class AppSearchViewController: StackScrollNodeViewController { 12 | 13 | override func viewDidLoad() { 14 | super.viewDidLoad() 15 | 16 | title = "Search" 17 | 18 | stackScrollNode.append(nodes: [ 19 | 20 | Components.makeTitleCell(title: "Search"), 21 | 22 | Components.makeSelectionCell(title: "Open", onTap: { 23 | 24 | 25 | }) 26 | ]) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Demo/App/AppTabBarController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppTabBarController.swift 3 | // Demo 4 | // 5 | // Created by Muukii on 2021/04/14. 6 | // 7 | 8 | import UIKit 9 | 10 | final class AppTabBarController: UITabBarController { 11 | 12 | override func viewDidLoad() { 13 | super.viewDidLoad() 14 | view.backgroundColor = .white 15 | 16 | viewControllers = [ 17 | UINavigationController(rootViewController: AppSearchViewController()), 18 | UINavigationController(rootViewController: AppOtherController()), 19 | ] 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /Demo/App/CustomNavigatedFluidViewController.swift: -------------------------------------------------------------------------------- 1 | import FluidPresentation 2 | import UIKit 3 | 4 | open class CustomNavigatedFluidViewController: NavigatedFluidViewController { 5 | 6 | public init( 7 | idiom: Idiom = .presentation, 8 | bodyViewController: UIViewController? = nil, 9 | displaysUnwindButton: Bool = true 10 | ) { 11 | 12 | super.init( 13 | idiom: idiom, 14 | bodyViewController: bodyViewController 15 | ) 16 | 17 | if displaysUnwindButton { 18 | 19 | let button: UIBarButtonItem 20 | 21 | switch idiom { 22 | case .navigationPush: 23 | button = .init(title: "Back", style: .plain, target: nil, action: nil) 24 | case .presentation: 25 | button = .init(title: "Dismiss", style: .plain, target: nil, action: nil) 26 | } 27 | 28 | button.target = self 29 | button.action = #selector(onTapUnwindButton) 30 | 31 | if let bodyViewController = bodyViewController { 32 | bodyViewController.navigationItem.leftBarButtonItem = button 33 | } else { 34 | navigationItem.leftBarButtonItem = button 35 | } 36 | } 37 | 38 | } 39 | 40 | @objc private func onTapUnwindButton() { 41 | dismiss(animated: true, completion: nil) 42 | } 43 | } 44 | 45 | extension UIViewController { 46 | 47 | public func wrappingNavigatedFluidViewController( 48 | idiom: FluidViewController.Idiom 49 | ) -> CustomNavigatedFluidViewController { 50 | .init(idiom: idiom, bodyViewController: self, displaysUnwindButton: true) 51 | } 52 | 53 | } 54 | 55 | -------------------------------------------------------------------------------- /Demo/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2021 Copyright (c) 2021 Eureka, Inc. 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | 22 | 23 | import UIKit 24 | 25 | @main 26 | class AppDelegate: UIResponder, UIApplicationDelegate { 27 | 28 | var window: UIWindow? 29 | 30 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 31 | // Override point for customization after application launch. 32 | return true 33 | } 34 | 35 | // MARK: UISceneSession Lifecycle 36 | 37 | } 38 | 39 | -------------------------------------------------------------------------------- /Demo/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Demo/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "scale" : "2x", 6 | "size" : "20x20" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "scale" : "3x", 11 | "size" : "20x20" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "scale" : "2x", 16 | "size" : "29x29" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "scale" : "3x", 21 | "size" : "29x29" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "scale" : "2x", 26 | "size" : "40x40" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "scale" : "2x", 36 | "size" : "60x60" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "scale" : "3x", 41 | "size" : "60x60" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "scale" : "1x", 46 | "size" : "20x20" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "scale" : "2x", 51 | "size" : "20x20" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "scale" : "1x", 56 | "size" : "29x29" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "scale" : "2x", 61 | "size" : "29x29" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "scale" : "1x", 66 | "size" : "40x40" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "scale" : "2x", 71 | "size" : "40x40" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "scale" : "1x", 76 | "size" : "76x76" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "scale" : "2x", 81 | "size" : "76x76" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "scale" : "2x", 86 | "size" : "83.5x83.5" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "scale" : "1x", 91 | "size" : "1024x1024" 92 | } 93 | ], 94 | "info" : { 95 | "author" : "xcode", 96 | "version" : 1 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /Demo/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Demo/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 | -------------------------------------------------------------------------------- /Demo/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 30 | 37 | 44 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /Demo/ContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2021 Copyright (c) 2021 Eureka, Inc. 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | 22 | import SwiftUI 23 | 24 | var count = 0 25 | 26 | struct ContentView: View { 27 | 28 | enum Action { 29 | case dismiss 30 | case push 31 | case pushNavigationBar 32 | case pushInCurrentContext 33 | case present 34 | case presentInCurrentContext 35 | case presentInOverCurrentContext 36 | case makePresentationContext(Bool) 37 | } 38 | 39 | @State private var isPresentationContext = false 40 | 41 | var onAction: (Action) -> Void 42 | 43 | var body: some View { 44 | 45 | ZStack { 46 | 47 | Color(white: 1, opacity: 1) 48 | .edgesIgnoringSafeArea(.all) 49 | 50 | ScrollView(.vertical, showsIndicators: true) { 51 | VStack { 52 | 53 | Text("\(count)") 54 | .font(.title) 55 | 56 | Text("Good morning") 57 | 58 | Group { 59 | 60 | Button("Dismiss") { 61 | onAction(.dismiss) 62 | } 63 | 64 | Toggle.init( 65 | "Make PresentationContext", 66 | isOn: .init( 67 | get: { 68 | isPresentationContext 69 | }, 70 | set: { value in 71 | onAction(.makePresentationContext(value)) 72 | isPresentationContext = value 73 | } 74 | ) 75 | ) 76 | 77 | Button("Push - FullScreen") { 78 | onAction(.push) 79 | } 80 | 81 | Button("Push - FullScreen - Navigation") { 82 | onAction(.pushNavigationBar) 83 | } 84 | 85 | Button("Push - CurrentContext") { 86 | onAction(.pushInCurrentContext) 87 | } 88 | 89 | Button("Present - FullScreen") { 90 | onAction(.present) 91 | } 92 | 93 | Button("Present - CurrentContext") { 94 | onAction(.presentInCurrentContext) 95 | } 96 | 97 | Button("Present - OverCurrentContext") { 98 | onAction(.presentInOverCurrentContext) 99 | } 100 | 101 | } 102 | .padding(.horizontal, 20) 103 | 104 | TextField.init("Text", text: .constant("Hello")) 105 | .frame(height: 120) 106 | 107 | ScrollView(.horizontal, showsIndicators: true) { 108 | HStack { 109 | ForEach(0..<10) { (i) in 110 | 111 | Rectangle() 112 | .frame(width: 50, height: 50, alignment: .center) 113 | .foregroundColor(Color(white: 0.90, opacity: 1)) 114 | 115 | } 116 | } 117 | } 118 | 119 | ForEach(0..<6) { i in 120 | Text("Section") 121 | ScrollView(.horizontal, showsIndicators: true) { 122 | HStack { 123 | ForEach(0..<10) { (i) in 124 | 125 | Rectangle() 126 | .frame(width: 100, height: 100, alignment: .center) 127 | .foregroundColor(Color(white: 0.90, opacity: 1)) 128 | } 129 | } 130 | } 131 | .id(i) 132 | } 133 | 134 | } 135 | } 136 | 137 | } 138 | } 139 | 140 | } 141 | -------------------------------------------------------------------------------- /Demo/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | UIUserInterfaceStyle 6 | light 7 | CFBundleDevelopmentRegion 8 | $(DEVELOPMENT_LANGUAGE) 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | UIApplicationSupportsIndirectInputEvents 26 | 27 | UILaunchStoryboardName 28 | LaunchScreen 29 | UIMainStoryboardFile 30 | Main 31 | UIRequiredDeviceCapabilities 32 | 33 | armv7 34 | 35 | UISupportedInterfaceOrientations 36 | 37 | UIInterfaceOrientationPortrait 38 | UIInterfaceOrientationLandscapeLeft 39 | UIInterfaceOrientationLandscapeRight 40 | 41 | UISupportedInterfaceOrientations~ipad 42 | 43 | UIInterfaceOrientationPortrait 44 | UIInterfaceOrientationPortraitUpsideDown 45 | UIInterfaceOrientationLandscapeLeft 46 | UIInterfaceOrientationLandscapeRight 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /Demo/NavigationSampleViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2021 Copyright (c) 2021 Eureka, Inc. 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | 22 | 23 | import FluidPresentation 24 | import Foundation 25 | import SwiftUI 26 | import TinyConstraints 27 | 28 | final class NavigationSampleViewController: NavigatedFluidViewController { 29 | 30 | init() { 31 | super.init() 32 | 33 | navigationItem.leftBarButtonItem = UIBarButtonItem.init(barButtonSystemItem: .done, target: nil, action: nil) 34 | } 35 | 36 | override func viewDidLoad() { 37 | super.viewDidLoad() 38 | 39 | count += 1 40 | 41 | accessibilityLabel = count.description 42 | view.accessibilityIdentifier = count.description 43 | 44 | let hosting = UIHostingController( 45 | rootView: ContentView( 46 | onAction: { [unowned self] action in 47 | switch action { 48 | case .dismiss: 49 | self.dismiss(animated: true, completion: nil) 50 | case .push: 51 | let controller = SampleViewController() 52 | controller.setIdiom(.navigationPush(isScreenGestureEnabled: true)) 53 | present(controller, animated: true, completion: nil) 54 | case .pushNavigationBar: 55 | let controller = SampleViewController() 56 | controller.setIdiom(.navigationPush(isScreenGestureEnabled: true)) 57 | present(controller, animated: true, completion: nil) 58 | case .pushInCurrentContext: 59 | let controller = SampleViewController() 60 | controller.setIdiom(.navigationPush(isScreenGestureEnabled: true)) 61 | controller.modalPresentationStyle = .currentContext 62 | present(controller, animated: true, completion: nil) 63 | case .present: 64 | let controller = SampleViewController() 65 | controller.setIdiom(.presentation) 66 | present(controller, animated: true, completion: nil) 67 | case .presentInCurrentContext: 68 | let controller = SampleViewController() 69 | controller.setIdiom(.presentation) 70 | controller.modalPresentationStyle = .currentContext 71 | present(controller, animated: true, completion: nil) 72 | case .presentInOverCurrentContext: 73 | let controller = SampleViewController() 74 | controller.modalPresentationStyle = .overCurrentContext 75 | controller.setIdiom(.presentation) 76 | present(controller, animated: true, completion: nil) 77 | case .makePresentationContext(let isOn): 78 | self.definesPresentationContext = isOn 79 | 80 | } 81 | } 82 | ) 83 | ) 84 | 85 | addChild(hosting) 86 | view.addSubview(hosting.view) 87 | hosting.view.edgesToSuperview() 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /Demo/SampleViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2021 Copyright (c) 2021 Eureka, Inc. 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | 22 | import FluidPresentation 23 | import Foundation 24 | import SwiftUI 25 | import TinyConstraints 26 | 27 | final class SampleViewController: FluidViewController { 28 | 29 | override func viewDidLoad() { 30 | super.viewDidLoad() 31 | 32 | count += 1 33 | title = "\(count)" 34 | 35 | accessibilityLabel = count.description 36 | view.accessibilityIdentifier = count.description 37 | 38 | let hosting = UIHostingController( 39 | rootView: ContentView( 40 | onAction: { [unowned self] action in 41 | switch action { 42 | case .dismiss: 43 | self.dismiss(animated: true, completion: nil) 44 | case .push: 45 | let controller = SampleViewController() 46 | controller.setIdiom(.navigationPush(isScreenGestureEnabled: true)) 47 | present(controller, animated: true, completion: nil) 48 | case .pushNavigationBar: 49 | let controller = NavigatedFluidViewController( 50 | idiom: .navigationPush, 51 | bodyViewController: SampleViewController() 52 | ) 53 | controller.setIdiom(.navigationPush(isScreenGestureEnabled: true)) 54 | present(controller, animated: true, completion: nil) 55 | case .pushInCurrentContext: 56 | let controller = SampleViewController() 57 | controller.modalPresentationStyle = .currentContext 58 | controller.setIdiom(.navigationPush(isScreenGestureEnabled: true)) 59 | present(controller, animated: true, completion: nil) 60 | case .present: 61 | let controller = SampleViewController() 62 | controller.setIdiom(.presentation) 63 | present(controller, animated: true, completion: nil) 64 | case .presentInCurrentContext: 65 | let controller = SampleViewController() 66 | controller.modalPresentationStyle = .currentContext 67 | controller.setIdiom(.presentation) 68 | present(controller, animated: true, completion: nil) 69 | case .presentInOverCurrentContext: 70 | let controller = SampleViewController() 71 | controller.modalPresentationStyle = .overCurrentContext 72 | controller.setIdiom(.presentation) 73 | present(controller, animated: true, completion: nil) 74 | case .makePresentationContext(let isOn): 75 | self.definesPresentationContext = isOn 76 | } 77 | } 78 | ) 79 | ) 80 | 81 | addChild(hosting) 82 | view.addSubview(hosting.view) 83 | hosting.view.edgesToSuperview() 84 | } 85 | 86 | override func viewDidAppear(_ animated: Bool) { 87 | super.viewDidAppear(animated) 88 | 89 | print("Presented => \(String(describing: presentedViewController))") 90 | print("Presenting => \(String(describing: presentingViewController))") 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /Demo/Texture/Components.swift: -------------------------------------------------------------------------------- 1 | import AsyncDisplayKit 2 | import MobileCoreServices 3 | import TextureSwiftSupport 4 | import UIKit 5 | 6 | enum Components { 7 | 8 | static func makeTitleCell(title: String) -> ASCellNode { 9 | 10 | let label = ASTextNode() 11 | label.attributedText = NSAttributedString( 12 | string: title, 13 | attributes: [ 14 | .font: UIFont.preferredFont(forTextStyle: .title1), 15 | .foregroundColor: UIColor.darkGray, 16 | ] 17 | ) 18 | return WrapperCellNode { 19 | AnyDisplayNode { _, _ in 20 | LayoutSpec { 21 | label 22 | .padding(8) 23 | } 24 | } 25 | } 26 | } 27 | 28 | static func makeSelectionCell( 29 | title: String, 30 | description: String? = nil, 31 | onTap: @escaping () -> Void 32 | ) -> ASCellNode { 33 | 34 | let shape = ShapeLayerNode.roundedCorner(radius: 8) 35 | 36 | let descriptionLabel = ASTextNode() 37 | descriptionLabel.attributedText = description.map { 38 | NSAttributedString( 39 | string: $0, 40 | attributes: [ 41 | .font: UIFont.preferredFont(forTextStyle: .caption1), 42 | .foregroundColor: UIColor.lightGray, 43 | ] 44 | ) 45 | } 46 | 47 | let label = ASTextNode() 48 | label.attributedText = NSAttributedString( 49 | string: title, 50 | attributes: [ 51 | .font: UIFont.preferredFont(forTextStyle: .subheadline), 52 | .foregroundColor: UIColor.darkGray, 53 | ] 54 | ) 55 | 56 | return WrapperCellNode { 57 | return InteractiveNode(animation: .translucent) { 58 | return AnyDisplayNode { _, _ in 59 | 60 | LayoutSpec { 61 | VStackLayout(spacing: 8) { 62 | HStackLayout { 63 | label 64 | .flexGrow(1) 65 | } 66 | if description != nil { 67 | descriptionLabel 68 | } 69 | } 70 | .padding(.horizontal, 8) 71 | .padding(.vertical, 12) 72 | .background(shape) 73 | .padding(4) 74 | } 75 | } 76 | .onDidLoad { _ in 77 | shape.shapeFillColor = .init(white: 0.95, alpha: 1) 78 | } 79 | } 80 | .onTap { 81 | onTap() 82 | } 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /Demo/Texture/StackScrollNode.swift: -------------------------------------------------------------------------------- 1 | 2 | import AsyncDisplayKit 3 | 4 | // v0.1.0 5 | 6 | /// Backing Component is ASCollectionNode 7 | open class StackScrollNode : ASDisplayNode, ASCollectionDelegate, ASCollectionDataSource { 8 | 9 | public final var onScrollViewDidScroll: (UIScrollView) -> Void = { _ in } 10 | 11 | open var shouldWaitUntilAllUpdatesAreCommitted: Bool = false 12 | 13 | open var isScrollEnabled: Bool { 14 | get { 15 | return collectionNode.view.isScrollEnabled 16 | } 17 | set { 18 | collectionNode.view.isScrollEnabled = newValue 19 | } 20 | } 21 | 22 | open var scrollView: UIScrollView { 23 | return collectionNode.view 24 | } 25 | 26 | open var collectionViewLayout: UICollectionViewLayout { 27 | return collectionNode.view.collectionViewLayout 28 | } 29 | 30 | open private(set) var nodes: [ASCellNode] = [] 31 | 32 | /// It should not be accessed unless there is special. 33 | internal let collectionNode: ASCollectionNode 34 | 35 | public init(layout: UICollectionViewFlowLayout) { 36 | 37 | collectionNode = ASCollectionNode(collectionViewLayout: layout) 38 | collectionNode.backgroundColor = .clear 39 | 40 | super.init() 41 | } 42 | 43 | public override convenience init() { 44 | 45 | let layout = UICollectionViewFlowLayout() 46 | layout.minimumInteritemSpacing = 0 47 | layout.minimumLineSpacing = 0 48 | layout.sectionInset = .zero 49 | 50 | self.init(layout: layout) 51 | } 52 | 53 | open func append(nodes: [ASCellNode]) { 54 | 55 | self.nodes += nodes 56 | 57 | collectionNode.reloadData() 58 | if shouldWaitUntilAllUpdatesAreCommitted { 59 | collectionNode.waitUntilAllUpdatesAreProcessed() 60 | } 61 | } 62 | 63 | open func removeAll() { 64 | self.nodes = [] 65 | 66 | collectionNode.reloadData() 67 | if shouldWaitUntilAllUpdatesAreCommitted { 68 | collectionNode.waitUntilAllUpdatesAreProcessed() 69 | } 70 | } 71 | 72 | open func replaceAll(nodes: [ASCellNode]) { 73 | 74 | self.nodes = nodes 75 | 76 | collectionNode.reloadData() 77 | if shouldWaitUntilAllUpdatesAreCommitted { 78 | collectionNode.waitUntilAllUpdatesAreProcessed() 79 | } 80 | } 81 | 82 | open override func didLoad() { 83 | 84 | super.didLoad() 85 | 86 | addSubnode(collectionNode) 87 | 88 | collectionNode.delegate = self 89 | collectionNode.dataSource = self 90 | collectionNode.view.alwaysBounceVertical = true 91 | } 92 | 93 | open override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec { 94 | 95 | return ASWrapperLayoutSpec(layoutElement: collectionNode) 96 | } 97 | 98 | // MARK: - ASCollectionDelegate 99 | 100 | public func collectionNode(_ collectionNode: ASCollectionNode, constrainedSizeForItemAt indexPath: IndexPath) -> ASSizeRange { 101 | 102 | return ASSizeRange( 103 | min: .init(width: collectionNode.bounds.width, height: 0), 104 | max: .init(width: collectionNode.bounds.width, height: .infinity) 105 | ) 106 | } 107 | 108 | // MARK: - ASCollectionDataSource 109 | open var numberOfSections: Int { 110 | return 1 111 | } 112 | 113 | public func collectionNode(_ collectionNode: ASCollectionNode, numberOfItemsInSection section: Int) -> Int { 114 | 115 | return nodes.count 116 | } 117 | 118 | public func collectionNode(_ collectionNode: ASCollectionNode, nodeForItemAt indexPath: IndexPath) -> ASCellNode { 119 | return nodes[indexPath.item] 120 | } 121 | 122 | // MARK: - UIScrollViewDelegate 123 | public func scrollViewDidScroll(_ scrollView: UIScrollView) { 124 | onScrollViewDidScroll(scrollView) 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /Demo/Texture/StackScrollNodeViewController.swift: -------------------------------------------------------------------------------- 1 | import AsyncDisplayKit 2 | import TextureSwiftSupport 3 | import UIKit 4 | 5 | class StackScrollNodeViewController: DisplayNodeViewController { 6 | 7 | let stackScrollNode = StackScrollNode() 8 | 9 | override init() { 10 | super.init() 11 | // stackScrollNode.scrollView.delaysContentTouches = false 12 | } 13 | 14 | override func viewDidLoad() { 15 | super.viewDidLoad() 16 | 17 | if #available(iOS 13.0, *) { 18 | view.backgroundColor = .systemBackground 19 | } else { 20 | view.backgroundColor = .white 21 | } 22 | 23 | } 24 | 25 | override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec { 26 | LayoutSpec { 27 | stackScrollNode 28 | } 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /Demo/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2021 Copyright (c) 2021 Eureka, Inc. 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | 22 | 23 | import FluidPresentation 24 | import SwiftUI 25 | import UIKit 26 | 27 | class ViewController: UIViewController { 28 | 29 | override func viewDidLoad() { 30 | super.viewDidLoad() 31 | // Do any additional setup after loading the view. 32 | } 33 | 34 | @IBAction func onTapAnyLeft(_ sender: Any) { 35 | 36 | let controller = SampleViewController() 37 | controller.dismissingInteractions = [.init(trigger: .screen, startFrom: .left)] 38 | 39 | present(controller, animated: true, completion: nil) 40 | 41 | } 42 | 43 | @IBAction func onTapEdgeLeft(_ sender: Any) { 44 | 45 | let controller = SampleViewController() 46 | controller.dismissingInteractions = [.init(trigger: .edge, startFrom: .left)] 47 | 48 | present(controller, animated: true, completion: nil) 49 | 50 | } 51 | 52 | @IBAction func onTapNavigation(_ sender: Any) { 53 | 54 | let controller = NavigationSampleViewController() 55 | controller.dismissingInteractions = [.init(trigger: .screen, startFrom: .left)] 56 | present(controller, animated: true, completion: nil) 57 | 58 | } 59 | 60 | @IBAction func onTapTab(_ sender: Any) { 61 | 62 | let rootViewController = AppRootViewController() 63 | view.window?.rootViewController = rootViewController 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /FluidPresentation.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = "FluidPresentation" 3 | s.version = "0.1.0" 4 | s.summary = "Presentation-based view controller which can unwind by any gestures." 5 | 6 | s.homepage = "https://github.com/muukii/FluidPresentation" 7 | s.license = "MIT" 8 | s.author = "muukii" 9 | s.source = { :git => "https://github.com/muukii/FluidPresentation.git", :tag => s.version } 10 | 11 | s.swift_version = "5.3" 12 | s.module_name = s.name 13 | s.requires_arc = true 14 | s.ios.deployment_target = "12.0" 15 | s.ios.frameworks = ["UIKit"] 16 | s.source_files = "FluidPresentation/**/*.swift" 17 | end 18 | -------------------------------------------------------------------------------- /FluidPresentation.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 51; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 4B32B824265494AB001A31F3 /* Context.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B32B823265494AB001A31F3 /* Context.swift */; }; 11 | 4B32B82C2654E31B001A31F3 /* FluidPresentationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B32B82B2654E31B001A31F3 /* FluidPresentationTests.swift */; }; 12 | 4B32B82E2654E31B001A31F3 /* FluidPresentation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4B895B0826210D7D00EB7312 /* FluidPresentation.framework */; }; 13 | 4B4A6E532654F6EB00F9D385 /* NavigationItemKVOTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4A6E522654F6EB00F9D385 /* NavigationItemKVOTests.swift */; }; 14 | 4B61048725C999430058D264 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B61048625C999430058D264 /* AppDelegate.swift */; }; 15 | 4B61048B25C999430058D264 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B61048A25C999430058D264 /* ViewController.swift */; }; 16 | 4B61048E25C999430058D264 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 4B61048C25C999430058D264 /* Main.storyboard */; }; 17 | 4B61049025C999440058D264 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 4B61048F25C999440058D264 /* Assets.xcassets */; }; 18 | 4B61049325C999440058D264 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 4B61049125C999440058D264 /* LaunchScreen.storyboard */; }; 19 | 4B895B0C26210D7D00EB7312 /* FluidViewController.h in Headers */ = {isa = PBXBuildFile; fileRef = 4B895B0A26210D7D00EB7312 /* FluidViewController.h */; settings = {ATTRIBUTES = (Public, ); }; }; 20 | 4B895B0F26210D7D00EB7312 /* FluidPresentation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4B895B0826210D7D00EB7312 /* FluidPresentation.framework */; }; 21 | 4B895B1026210D7D00EB7312 /* FluidPresentation.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 4B895B0826210D7D00EB7312 /* FluidPresentation.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 22 | 4B895B1626210DA200EB7312 /* FluidViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BA9B2792620409900CA3D45 /* FluidViewController.swift */; }; 23 | 4B895B1726210DA200EB7312 /* TransitionControllers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BA9B27C26204CBB00CA3D45 /* TransitionControllers.swift */; }; 24 | 4B895B2226210F1700EB7312 /* SampleViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B895B2126210F1700EB7312 /* SampleViewController.swift */; }; 25 | 4BB357A12627171200C8387F /* AppRootViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BB357A02627171200C8387F /* AppRootViewController.swift */; }; 26 | 4BB357A62627173600C8387F /* AppTabBarController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BB357A52627173600C8387F /* AppTabBarController.swift */; }; 27 | 4BB357A92627176D00C8387F /* AppOtherViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BB357A82627176D00C8387F /* AppOtherViewController.swift */; }; 28 | 4BB357AD262717DF00C8387F /* StackScrollNodeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BB357AB262717BE00C8387F /* StackScrollNodeViewController.swift */; }; 29 | 4BB357AE262717DF00C8387F /* Components.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BB3579D2627153D00C8387F /* Components.swift */; }; 30 | 4BB357AF262717DF00C8387F /* StackScrollNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BB3579C2627152F00C8387F /* StackScrollNode.swift */; }; 31 | 4BB357B22627182900C8387F /* AppNotificationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BB357B12627182900C8387F /* AppNotificationViewController.swift */; }; 32 | 4BB357B6262718F900C8387F /* AppSearchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BB357B5262718F900C8387F /* AppSearchViewController.swift */; }; 33 | 4BB357B926271A8300C8387F /* CustomNavigatedFluidViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BB357B826271A8300C8387F /* CustomNavigatedFluidViewController.swift */; }; 34 | 4BF23D5426218B1B001D508B /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BF23D5326218B1B001D508B /* ContentView.swift */; }; 35 | 4BF23D5E2621B11D001D508B /* ScrollView+Handling.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BF23D5D2621B11D001D508B /* ScrollView+Handling.swift */; }; 36 | 4BF23D612621B857001D508B /* Log.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BF23D602621B857001D508B /* Log.swift */; }; 37 | 4BF23D6526220CFF001D508B /* NavigatedFluidViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BF23D6426220CFF001D508B /* NavigatedFluidViewController.swift */; }; 38 | 4BF23D6E26221103001D508B /* NavigationSampleViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BF23D6A262210DC001D508B /* NavigationSampleViewController.swift */; }; 39 | AC692B0DA75BB218B66F84E1 /* Pods_Demo.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2EE9A524C37C223A149BBA4B /* Pods_Demo.framework */; }; 40 | /* End PBXBuildFile section */ 41 | 42 | /* Begin PBXContainerItemProxy section */ 43 | 4B32B82F2654E31B001A31F3 /* PBXContainerItemProxy */ = { 44 | isa = PBXContainerItemProxy; 45 | containerPortal = 4B61047B25C999430058D264 /* Project object */; 46 | proxyType = 1; 47 | remoteGlobalIDString = 4B895B0726210D7D00EB7312; 48 | remoteInfo = FluidPresentation; 49 | }; 50 | 4B895B0D26210D7D00EB7312 /* PBXContainerItemProxy */ = { 51 | isa = PBXContainerItemProxy; 52 | containerPortal = 4B61047B25C999430058D264 /* Project object */; 53 | proxyType = 1; 54 | remoteGlobalIDString = 4B895B0726210D7D00EB7312; 55 | remoteInfo = FluidViewController; 56 | }; 57 | /* End PBXContainerItemProxy section */ 58 | 59 | /* Begin PBXCopyFilesBuildPhase section */ 60 | 4B895B1126210D7D00EB7312 /* Embed Frameworks */ = { 61 | isa = PBXCopyFilesBuildPhase; 62 | buildActionMask = 2147483647; 63 | dstPath = ""; 64 | dstSubfolderSpec = 10; 65 | files = ( 66 | 4B895B1026210D7D00EB7312 /* FluidPresentation.framework in Embed Frameworks */, 67 | ); 68 | name = "Embed Frameworks"; 69 | runOnlyForDeploymentPostprocessing = 0; 70 | }; 71 | /* End PBXCopyFilesBuildPhase section */ 72 | 73 | /* Begin PBXFileReference section */ 74 | 2EE9A524C37C223A149BBA4B /* Pods_Demo.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Demo.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 75 | 354789D630D2F51EDA44AF92 /* Pods-Demo.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Demo.release.xcconfig"; path = "Target Support Files/Pods-Demo/Pods-Demo.release.xcconfig"; sourceTree = ""; }; 76 | 4B32B823265494AB001A31F3 /* Context.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Context.swift; sourceTree = ""; }; 77 | 4B32B8292654E31B001A31F3 /* FluidPresentationTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = FluidPresentationTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 78 | 4B32B82B2654E31B001A31F3 /* FluidPresentationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FluidPresentationTests.swift; sourceTree = ""; }; 79 | 4B32B82D2654E31B001A31F3 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 80 | 4B4A6E522654F6EB00F9D385 /* NavigationItemKVOTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationItemKVOTests.swift; sourceTree = ""; }; 81 | 4B61048325C999430058D264 /* Demo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Demo.app; sourceTree = BUILT_PRODUCTS_DIR; }; 82 | 4B61048625C999430058D264 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 83 | 4B61048A25C999430058D264 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 84 | 4B61048D25C999430058D264 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 85 | 4B61048F25C999440058D264 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 86 | 4B61049225C999440058D264 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 87 | 4B61049425C999440058D264 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 88 | 4B895B0826210D7D00EB7312 /* FluidPresentation.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = FluidPresentation.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 89 | 4B895B0A26210D7D00EB7312 /* FluidViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FluidViewController.h; sourceTree = ""; }; 90 | 4B895B0B26210D7D00EB7312 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 91 | 4B895B2126210F1700EB7312 /* SampleViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SampleViewController.swift; sourceTree = ""; }; 92 | 4BA9B2792620409900CA3D45 /* FluidViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FluidViewController.swift; sourceTree = ""; }; 93 | 4BA9B27C26204CBB00CA3D45 /* TransitionControllers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransitionControllers.swift; sourceTree = ""; }; 94 | 4BB3579C2627152F00C8387F /* StackScrollNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StackScrollNode.swift; sourceTree = ""; }; 95 | 4BB3579D2627153D00C8387F /* Components.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Components.swift; sourceTree = ""; }; 96 | 4BB357A02627171200C8387F /* AppRootViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppRootViewController.swift; sourceTree = ""; }; 97 | 4BB357A52627173600C8387F /* AppTabBarController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppTabBarController.swift; sourceTree = ""; }; 98 | 4BB357A82627176D00C8387F /* AppOtherViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppOtherViewController.swift; sourceTree = ""; }; 99 | 4BB357AB262717BE00C8387F /* StackScrollNodeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StackScrollNodeViewController.swift; sourceTree = ""; }; 100 | 4BB357B12627182900C8387F /* AppNotificationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppNotificationViewController.swift; sourceTree = ""; }; 101 | 4BB357B5262718F900C8387F /* AppSearchViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSearchViewController.swift; sourceTree = ""; }; 102 | 4BB357B826271A8300C8387F /* CustomNavigatedFluidViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomNavigatedFluidViewController.swift; sourceTree = ""; }; 103 | 4BF23D5326218B1B001D508B /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 104 | 4BF23D5D2621B11D001D508B /* ScrollView+Handling.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ScrollView+Handling.swift"; sourceTree = ""; }; 105 | 4BF23D602621B857001D508B /* Log.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Log.swift; sourceTree = ""; }; 106 | 4BF23D6426220CFF001D508B /* NavigatedFluidViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigatedFluidViewController.swift; sourceTree = ""; }; 107 | 4BF23D6A262210DC001D508B /* NavigationSampleViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationSampleViewController.swift; sourceTree = ""; }; 108 | B60E67EC035A1585AB95AE5F /* Pods-Demo.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Demo.debug.xcconfig"; path = "Target Support Files/Pods-Demo/Pods-Demo.debug.xcconfig"; sourceTree = ""; }; 109 | /* End PBXFileReference section */ 110 | 111 | /* Begin PBXFrameworksBuildPhase section */ 112 | 4B32B8262654E31B001A31F3 /* Frameworks */ = { 113 | isa = PBXFrameworksBuildPhase; 114 | buildActionMask = 2147483647; 115 | files = ( 116 | 4B32B82E2654E31B001A31F3 /* FluidPresentation.framework in Frameworks */, 117 | ); 118 | runOnlyForDeploymentPostprocessing = 0; 119 | }; 120 | 4B61048025C999430058D264 /* Frameworks */ = { 121 | isa = PBXFrameworksBuildPhase; 122 | buildActionMask = 2147483647; 123 | files = ( 124 | 4B895B0F26210D7D00EB7312 /* FluidPresentation.framework in Frameworks */, 125 | AC692B0DA75BB218B66F84E1 /* Pods_Demo.framework in Frameworks */, 126 | ); 127 | runOnlyForDeploymentPostprocessing = 0; 128 | }; 129 | 4B895B0526210D7D00EB7312 /* Frameworks */ = { 130 | isa = PBXFrameworksBuildPhase; 131 | buildActionMask = 2147483647; 132 | files = ( 133 | ); 134 | runOnlyForDeploymentPostprocessing = 0; 135 | }; 136 | /* End PBXFrameworksBuildPhase section */ 137 | 138 | /* Begin PBXGroup section */ 139 | 4B32B82A2654E31B001A31F3 /* FluidPresentationTests */ = { 140 | isa = PBXGroup; 141 | children = ( 142 | 4B32B82B2654E31B001A31F3 /* FluidPresentationTests.swift */, 143 | 4B4A6E522654F6EB00F9D385 /* NavigationItemKVOTests.swift */, 144 | 4B32B82D2654E31B001A31F3 /* Info.plist */, 145 | ); 146 | path = FluidPresentationTests; 147 | sourceTree = ""; 148 | }; 149 | 4B61047A25C999430058D264 = { 150 | isa = PBXGroup; 151 | children = ( 152 | 4B895B0926210D7D00EB7312 /* FluidPresentation */, 153 | 4B61048525C999430058D264 /* Demo */, 154 | 4B32B82A2654E31B001A31F3 /* FluidPresentationTests */, 155 | 4B61048425C999430058D264 /* Products */, 156 | E2635A8DE8B4B14D214A4A03 /* Pods */, 157 | D6DCF2BDECBB46843463B733 /* Frameworks */, 158 | ); 159 | sourceTree = ""; 160 | }; 161 | 4B61048425C999430058D264 /* Products */ = { 162 | isa = PBXGroup; 163 | children = ( 164 | 4B61048325C999430058D264 /* Demo.app */, 165 | 4B895B0826210D7D00EB7312 /* FluidPresentation.framework */, 166 | 4B32B8292654E31B001A31F3 /* FluidPresentationTests.xctest */, 167 | ); 168 | name = Products; 169 | sourceTree = ""; 170 | }; 171 | 4B61048525C999430058D264 /* Demo */ = { 172 | isa = PBXGroup; 173 | children = ( 174 | 4BB3579F2627170500C8387F /* App */, 175 | 4BB3579B2627152700C8387F /* Texture */, 176 | 4B61048625C999430058D264 /* AppDelegate.swift */, 177 | 4B61048A25C999430058D264 /* ViewController.swift */, 178 | 4BF23D5326218B1B001D508B /* ContentView.swift */, 179 | 4B895B2126210F1700EB7312 /* SampleViewController.swift */, 180 | 4BF23D6A262210DC001D508B /* NavigationSampleViewController.swift */, 181 | 4B61048C25C999430058D264 /* Main.storyboard */, 182 | 4B61048F25C999440058D264 /* Assets.xcassets */, 183 | 4B61049125C999440058D264 /* LaunchScreen.storyboard */, 184 | 4B61049425C999440058D264 /* Info.plist */, 185 | ); 186 | path = Demo; 187 | sourceTree = ""; 188 | }; 189 | 4B895B0926210D7D00EB7312 /* FluidPresentation */ = { 190 | isa = PBXGroup; 191 | children = ( 192 | 4B895B0A26210D7D00EB7312 /* FluidViewController.h */, 193 | 4BA9B2792620409900CA3D45 /* FluidViewController.swift */, 194 | 4B32B823265494AB001A31F3 /* Context.swift */, 195 | 4BF23D6426220CFF001D508B /* NavigatedFluidViewController.swift */, 196 | 4BF23D602621B857001D508B /* Log.swift */, 197 | 4BF23D5D2621B11D001D508B /* ScrollView+Handling.swift */, 198 | 4BA9B27C26204CBB00CA3D45 /* TransitionControllers.swift */, 199 | 4B895B0B26210D7D00EB7312 /* Info.plist */, 200 | ); 201 | path = FluidPresentation; 202 | sourceTree = ""; 203 | }; 204 | 4BB3579B2627152700C8387F /* Texture */ = { 205 | isa = PBXGroup; 206 | children = ( 207 | 4BB357AB262717BE00C8387F /* StackScrollNodeViewController.swift */, 208 | 4BB3579D2627153D00C8387F /* Components.swift */, 209 | 4BB3579C2627152F00C8387F /* StackScrollNode.swift */, 210 | ); 211 | path = Texture; 212 | sourceTree = ""; 213 | }; 214 | 4BB3579F2627170500C8387F /* App */ = { 215 | isa = PBXGroup; 216 | children = ( 217 | 4BB357A02627171200C8387F /* AppRootViewController.swift */, 218 | 4BB357A52627173600C8387F /* AppTabBarController.swift */, 219 | 4BB357B5262718F900C8387F /* AppSearchViewController.swift */, 220 | 4BB357A82627176D00C8387F /* AppOtherViewController.swift */, 221 | 4BB357B12627182900C8387F /* AppNotificationViewController.swift */, 222 | 4BB357B826271A8300C8387F /* CustomNavigatedFluidViewController.swift */, 223 | ); 224 | path = App; 225 | sourceTree = ""; 226 | }; 227 | D6DCF2BDECBB46843463B733 /* Frameworks */ = { 228 | isa = PBXGroup; 229 | children = ( 230 | 2EE9A524C37C223A149BBA4B /* Pods_Demo.framework */, 231 | ); 232 | name = Frameworks; 233 | sourceTree = ""; 234 | }; 235 | E2635A8DE8B4B14D214A4A03 /* Pods */ = { 236 | isa = PBXGroup; 237 | children = ( 238 | B60E67EC035A1585AB95AE5F /* Pods-Demo.debug.xcconfig */, 239 | 354789D630D2F51EDA44AF92 /* Pods-Demo.release.xcconfig */, 240 | ); 241 | path = Pods; 242 | sourceTree = ""; 243 | }; 244 | /* End PBXGroup section */ 245 | 246 | /* Begin PBXHeadersBuildPhase section */ 247 | 4B895B0326210D7D00EB7312 /* Headers */ = { 248 | isa = PBXHeadersBuildPhase; 249 | buildActionMask = 2147483647; 250 | files = ( 251 | 4B895B0C26210D7D00EB7312 /* FluidViewController.h in Headers */, 252 | ); 253 | runOnlyForDeploymentPostprocessing = 0; 254 | }; 255 | /* End PBXHeadersBuildPhase section */ 256 | 257 | /* Begin PBXNativeTarget section */ 258 | 4B32B8282654E31B001A31F3 /* FluidPresentationTests */ = { 259 | isa = PBXNativeTarget; 260 | buildConfigurationList = 4B32B8332654E31B001A31F3 /* Build configuration list for PBXNativeTarget "FluidPresentationTests" */; 261 | buildPhases = ( 262 | 4B32B8252654E31B001A31F3 /* Sources */, 263 | 4B32B8262654E31B001A31F3 /* Frameworks */, 264 | 4B32B8272654E31B001A31F3 /* Resources */, 265 | ); 266 | buildRules = ( 267 | ); 268 | dependencies = ( 269 | 4B32B8302654E31B001A31F3 /* PBXTargetDependency */, 270 | ); 271 | name = FluidPresentationTests; 272 | productName = FluidPresentationTests; 273 | productReference = 4B32B8292654E31B001A31F3 /* FluidPresentationTests.xctest */; 274 | productType = "com.apple.product-type.bundle.unit-test"; 275 | }; 276 | 4B61048225C999430058D264 /* Demo */ = { 277 | isa = PBXNativeTarget; 278 | buildConfigurationList = 4B61049725C999440058D264 /* Build configuration list for PBXNativeTarget "Demo" */; 279 | buildPhases = ( 280 | 8BFCA8130302C217738E221E /* [CP] Check Pods Manifest.lock */, 281 | 4B61047F25C999430058D264 /* Sources */, 282 | 4B61048025C999430058D264 /* Frameworks */, 283 | 4B61048125C999430058D264 /* Resources */, 284 | 4B895B1126210D7D00EB7312 /* Embed Frameworks */, 285 | A820DD4310DB979193045DF9 /* [CP] Embed Pods Frameworks */, 286 | ); 287 | buildRules = ( 288 | ); 289 | dependencies = ( 290 | 4B895B0E26210D7D00EB7312 /* PBXTargetDependency */, 291 | ); 292 | name = Demo; 293 | productName = PresentationViewController; 294 | productReference = 4B61048325C999430058D264 /* Demo.app */; 295 | productType = "com.apple.product-type.application"; 296 | }; 297 | 4B895B0726210D7D00EB7312 /* FluidPresentation */ = { 298 | isa = PBXNativeTarget; 299 | buildConfigurationList = 4B895B1426210D7D00EB7312 /* Build configuration list for PBXNativeTarget "FluidPresentation" */; 300 | buildPhases = ( 301 | 4B895B0326210D7D00EB7312 /* Headers */, 302 | 4B895B0426210D7D00EB7312 /* Sources */, 303 | 4B895B0526210D7D00EB7312 /* Frameworks */, 304 | 4B895B0626210D7D00EB7312 /* Resources */, 305 | ); 306 | buildRules = ( 307 | ); 308 | dependencies = ( 309 | ); 310 | name = FluidPresentation; 311 | productName = FluidViewController; 312 | productReference = 4B895B0826210D7D00EB7312 /* FluidPresentation.framework */; 313 | productType = "com.apple.product-type.framework"; 314 | }; 315 | /* End PBXNativeTarget section */ 316 | 317 | /* Begin PBXProject section */ 318 | 4B61047B25C999430058D264 /* Project object */ = { 319 | isa = PBXProject; 320 | attributes = { 321 | LastSwiftUpdateCheck = 1250; 322 | LastUpgradeCheck = 1240; 323 | TargetAttributes = { 324 | 4B32B8282654E31B001A31F3 = { 325 | CreatedOnToolsVersion = 12.5; 326 | }; 327 | 4B61048225C999430058D264 = { 328 | CreatedOnToolsVersion = 12.4; 329 | }; 330 | 4B895B0726210D7D00EB7312 = { 331 | CreatedOnToolsVersion = 12.4; 332 | }; 333 | }; 334 | }; 335 | buildConfigurationList = 4B61047E25C999430058D264 /* Build configuration list for PBXProject "FluidPresentation" */; 336 | compatibilityVersion = "Xcode 9.3"; 337 | developmentRegion = en; 338 | hasScannedForEncodings = 0; 339 | knownRegions = ( 340 | en, 341 | Base, 342 | ); 343 | mainGroup = 4B61047A25C999430058D264; 344 | productRefGroup = 4B61048425C999430058D264 /* Products */; 345 | projectDirPath = ""; 346 | projectRoot = ""; 347 | targets = ( 348 | 4B61048225C999430058D264 /* Demo */, 349 | 4B895B0726210D7D00EB7312 /* FluidPresentation */, 350 | 4B32B8282654E31B001A31F3 /* FluidPresentationTests */, 351 | ); 352 | }; 353 | /* End PBXProject section */ 354 | 355 | /* Begin PBXResourcesBuildPhase section */ 356 | 4B32B8272654E31B001A31F3 /* Resources */ = { 357 | isa = PBXResourcesBuildPhase; 358 | buildActionMask = 2147483647; 359 | files = ( 360 | ); 361 | runOnlyForDeploymentPostprocessing = 0; 362 | }; 363 | 4B61048125C999430058D264 /* Resources */ = { 364 | isa = PBXResourcesBuildPhase; 365 | buildActionMask = 2147483647; 366 | files = ( 367 | 4B61049325C999440058D264 /* LaunchScreen.storyboard in Resources */, 368 | 4B61049025C999440058D264 /* Assets.xcassets in Resources */, 369 | 4B61048E25C999430058D264 /* Main.storyboard in Resources */, 370 | ); 371 | runOnlyForDeploymentPostprocessing = 0; 372 | }; 373 | 4B895B0626210D7D00EB7312 /* Resources */ = { 374 | isa = PBXResourcesBuildPhase; 375 | buildActionMask = 2147483647; 376 | files = ( 377 | ); 378 | runOnlyForDeploymentPostprocessing = 0; 379 | }; 380 | /* End PBXResourcesBuildPhase section */ 381 | 382 | /* Begin PBXShellScriptBuildPhase section */ 383 | 8BFCA8130302C217738E221E /* [CP] Check Pods Manifest.lock */ = { 384 | isa = PBXShellScriptBuildPhase; 385 | buildActionMask = 2147483647; 386 | files = ( 387 | ); 388 | inputFileListPaths = ( 389 | ); 390 | inputPaths = ( 391 | "${PODS_PODFILE_DIR_PATH}/Podfile.lock", 392 | "${PODS_ROOT}/Manifest.lock", 393 | ); 394 | name = "[CP] Check Pods Manifest.lock"; 395 | outputFileListPaths = ( 396 | ); 397 | outputPaths = ( 398 | "$(DERIVED_FILE_DIR)/Pods-Demo-checkManifestLockResult.txt", 399 | ); 400 | runOnlyForDeploymentPostprocessing = 0; 401 | shellPath = /bin/sh; 402 | shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; 403 | showEnvVarsInLog = 0; 404 | }; 405 | A820DD4310DB979193045DF9 /* [CP] Embed Pods Frameworks */ = { 406 | isa = PBXShellScriptBuildPhase; 407 | buildActionMask = 2147483647; 408 | files = ( 409 | ); 410 | inputFileListPaths = ( 411 | "${PODS_ROOT}/Target Support Files/Pods-Demo/Pods-Demo-frameworks-${CONFIGURATION}-input-files.xcfilelist", 412 | ); 413 | name = "[CP] Embed Pods Frameworks"; 414 | outputFileListPaths = ( 415 | "${PODS_ROOT}/Target Support Files/Pods-Demo/Pods-Demo-frameworks-${CONFIGURATION}-output-files.xcfilelist", 416 | ); 417 | runOnlyForDeploymentPostprocessing = 0; 418 | shellPath = /bin/sh; 419 | shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Demo/Pods-Demo-frameworks.sh\"\n"; 420 | showEnvVarsInLog = 0; 421 | }; 422 | /* End PBXShellScriptBuildPhase section */ 423 | 424 | /* Begin PBXSourcesBuildPhase section */ 425 | 4B32B8252654E31B001A31F3 /* Sources */ = { 426 | isa = PBXSourcesBuildPhase; 427 | buildActionMask = 2147483647; 428 | files = ( 429 | 4B32B82C2654E31B001A31F3 /* FluidPresentationTests.swift in Sources */, 430 | 4B4A6E532654F6EB00F9D385 /* NavigationItemKVOTests.swift in Sources */, 431 | ); 432 | runOnlyForDeploymentPostprocessing = 0; 433 | }; 434 | 4B61047F25C999430058D264 /* Sources */ = { 435 | isa = PBXSourcesBuildPhase; 436 | buildActionMask = 2147483647; 437 | files = ( 438 | 4BF23D6E26221103001D508B /* NavigationSampleViewController.swift in Sources */, 439 | 4BB357A92627176D00C8387F /* AppOtherViewController.swift in Sources */, 440 | 4B61048B25C999430058D264 /* ViewController.swift in Sources */, 441 | 4BB357A62627173600C8387F /* AppTabBarController.swift in Sources */, 442 | 4BB357AF262717DF00C8387F /* StackScrollNode.swift in Sources */, 443 | 4BB357B6262718F900C8387F /* AppSearchViewController.swift in Sources */, 444 | 4BB357A12627171200C8387F /* AppRootViewController.swift in Sources */, 445 | 4B61048725C999430058D264 /* AppDelegate.swift in Sources */, 446 | 4BF23D5426218B1B001D508B /* ContentView.swift in Sources */, 447 | 4BB357AE262717DF00C8387F /* Components.swift in Sources */, 448 | 4BB357B22627182900C8387F /* AppNotificationViewController.swift in Sources */, 449 | 4B895B2226210F1700EB7312 /* SampleViewController.swift in Sources */, 450 | 4BB357B926271A8300C8387F /* CustomNavigatedFluidViewController.swift in Sources */, 451 | 4BB357AD262717DF00C8387F /* StackScrollNodeViewController.swift in Sources */, 452 | ); 453 | runOnlyForDeploymentPostprocessing = 0; 454 | }; 455 | 4B895B0426210D7D00EB7312 /* Sources */ = { 456 | isa = PBXSourcesBuildPhase; 457 | buildActionMask = 2147483647; 458 | files = ( 459 | 4B895B1726210DA200EB7312 /* TransitionControllers.swift in Sources */, 460 | 4BF23D5E2621B11D001D508B /* ScrollView+Handling.swift in Sources */, 461 | 4BF23D612621B857001D508B /* Log.swift in Sources */, 462 | 4BF23D6526220CFF001D508B /* NavigatedFluidViewController.swift in Sources */, 463 | 4B895B1626210DA200EB7312 /* FluidViewController.swift in Sources */, 464 | 4B32B824265494AB001A31F3 /* Context.swift in Sources */, 465 | ); 466 | runOnlyForDeploymentPostprocessing = 0; 467 | }; 468 | /* End PBXSourcesBuildPhase section */ 469 | 470 | /* Begin PBXTargetDependency section */ 471 | 4B32B8302654E31B001A31F3 /* PBXTargetDependency */ = { 472 | isa = PBXTargetDependency; 473 | target = 4B895B0726210D7D00EB7312 /* FluidPresentation */; 474 | targetProxy = 4B32B82F2654E31B001A31F3 /* PBXContainerItemProxy */; 475 | }; 476 | 4B895B0E26210D7D00EB7312 /* PBXTargetDependency */ = { 477 | isa = PBXTargetDependency; 478 | target = 4B895B0726210D7D00EB7312 /* FluidPresentation */; 479 | targetProxy = 4B895B0D26210D7D00EB7312 /* PBXContainerItemProxy */; 480 | }; 481 | /* End PBXTargetDependency section */ 482 | 483 | /* Begin PBXVariantGroup section */ 484 | 4B61048C25C999430058D264 /* Main.storyboard */ = { 485 | isa = PBXVariantGroup; 486 | children = ( 487 | 4B61048D25C999430058D264 /* Base */, 488 | ); 489 | name = Main.storyboard; 490 | sourceTree = ""; 491 | }; 492 | 4B61049125C999440058D264 /* LaunchScreen.storyboard */ = { 493 | isa = PBXVariantGroup; 494 | children = ( 495 | 4B61049225C999440058D264 /* Base */, 496 | ); 497 | name = LaunchScreen.storyboard; 498 | sourceTree = ""; 499 | }; 500 | /* End PBXVariantGroup section */ 501 | 502 | /* Begin XCBuildConfiguration section */ 503 | 4B32B8312654E31B001A31F3 /* Debug */ = { 504 | isa = XCBuildConfiguration; 505 | buildSettings = { 506 | CODE_SIGN_STYLE = Automatic; 507 | INFOPLIST_FILE = FluidPresentationTests/Info.plist; 508 | IPHONEOS_DEPLOYMENT_TARGET = 14.5; 509 | LD_RUNPATH_SEARCH_PATHS = ( 510 | "$(inherited)", 511 | "@executable_path/Frameworks", 512 | "@loader_path/Frameworks", 513 | ); 514 | PRODUCT_BUNDLE_IDENTIFIER = app.muukii.FluidPresentationTests; 515 | PRODUCT_NAME = "$(TARGET_NAME)"; 516 | SWIFT_VERSION = 5.0; 517 | TARGETED_DEVICE_FAMILY = "1,2"; 518 | }; 519 | name = Debug; 520 | }; 521 | 4B32B8322654E31B001A31F3 /* Release */ = { 522 | isa = XCBuildConfiguration; 523 | buildSettings = { 524 | CODE_SIGN_STYLE = Automatic; 525 | INFOPLIST_FILE = FluidPresentationTests/Info.plist; 526 | IPHONEOS_DEPLOYMENT_TARGET = 14.5; 527 | LD_RUNPATH_SEARCH_PATHS = ( 528 | "$(inherited)", 529 | "@executable_path/Frameworks", 530 | "@loader_path/Frameworks", 531 | ); 532 | PRODUCT_BUNDLE_IDENTIFIER = app.muukii.FluidPresentationTests; 533 | PRODUCT_NAME = "$(TARGET_NAME)"; 534 | SWIFT_VERSION = 5.0; 535 | TARGETED_DEVICE_FAMILY = "1,2"; 536 | }; 537 | name = Release; 538 | }; 539 | 4B61049525C999440058D264 /* Debug */ = { 540 | isa = XCBuildConfiguration; 541 | buildSettings = { 542 | ALWAYS_SEARCH_USER_PATHS = NO; 543 | CLANG_ANALYZER_NONNULL = YES; 544 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 545 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 546 | CLANG_CXX_LIBRARY = "libc++"; 547 | CLANG_ENABLE_MODULES = YES; 548 | CLANG_ENABLE_OBJC_ARC = YES; 549 | CLANG_ENABLE_OBJC_WEAK = YES; 550 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 551 | CLANG_WARN_BOOL_CONVERSION = YES; 552 | CLANG_WARN_COMMA = YES; 553 | CLANG_WARN_CONSTANT_CONVERSION = YES; 554 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 555 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 556 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 557 | CLANG_WARN_EMPTY_BODY = YES; 558 | CLANG_WARN_ENUM_CONVERSION = YES; 559 | CLANG_WARN_INFINITE_RECURSION = YES; 560 | CLANG_WARN_INT_CONVERSION = YES; 561 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 562 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 563 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 564 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 565 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 566 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 567 | CLANG_WARN_STRICT_PROTOTYPES = YES; 568 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 569 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 570 | CLANG_WARN_UNREACHABLE_CODE = YES; 571 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 572 | COPY_PHASE_STRIP = NO; 573 | DEBUG_INFORMATION_FORMAT = dwarf; 574 | ENABLE_STRICT_OBJC_MSGSEND = YES; 575 | ENABLE_TESTABILITY = YES; 576 | GCC_C_LANGUAGE_STANDARD = gnu11; 577 | GCC_DYNAMIC_NO_PIC = NO; 578 | GCC_NO_COMMON_BLOCKS = YES; 579 | GCC_OPTIMIZATION_LEVEL = 0; 580 | GCC_PREPROCESSOR_DEFINITIONS = ( 581 | "DEBUG=1", 582 | "$(inherited)", 583 | ); 584 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 585 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 586 | GCC_WARN_UNDECLARED_SELECTOR = YES; 587 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 588 | GCC_WARN_UNUSED_FUNCTION = YES; 589 | GCC_WARN_UNUSED_VARIABLE = YES; 590 | IPHONEOS_DEPLOYMENT_TARGET = 14.4; 591 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 592 | MTL_FAST_MATH = YES; 593 | ONLY_ACTIVE_ARCH = YES; 594 | SDKROOT = iphoneos; 595 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 596 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 597 | }; 598 | name = Debug; 599 | }; 600 | 4B61049625C999440058D264 /* Release */ = { 601 | isa = XCBuildConfiguration; 602 | buildSettings = { 603 | ALWAYS_SEARCH_USER_PATHS = NO; 604 | CLANG_ANALYZER_NONNULL = YES; 605 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 606 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 607 | CLANG_CXX_LIBRARY = "libc++"; 608 | CLANG_ENABLE_MODULES = YES; 609 | CLANG_ENABLE_OBJC_ARC = YES; 610 | CLANG_ENABLE_OBJC_WEAK = YES; 611 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 612 | CLANG_WARN_BOOL_CONVERSION = YES; 613 | CLANG_WARN_COMMA = YES; 614 | CLANG_WARN_CONSTANT_CONVERSION = YES; 615 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 616 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 617 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 618 | CLANG_WARN_EMPTY_BODY = YES; 619 | CLANG_WARN_ENUM_CONVERSION = YES; 620 | CLANG_WARN_INFINITE_RECURSION = YES; 621 | CLANG_WARN_INT_CONVERSION = YES; 622 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 623 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 624 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 625 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 626 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 627 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 628 | CLANG_WARN_STRICT_PROTOTYPES = YES; 629 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 630 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 631 | CLANG_WARN_UNREACHABLE_CODE = YES; 632 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 633 | COPY_PHASE_STRIP = NO; 634 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 635 | ENABLE_NS_ASSERTIONS = NO; 636 | ENABLE_STRICT_OBJC_MSGSEND = YES; 637 | GCC_C_LANGUAGE_STANDARD = gnu11; 638 | GCC_NO_COMMON_BLOCKS = YES; 639 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 640 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 641 | GCC_WARN_UNDECLARED_SELECTOR = YES; 642 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 643 | GCC_WARN_UNUSED_FUNCTION = YES; 644 | GCC_WARN_UNUSED_VARIABLE = YES; 645 | IPHONEOS_DEPLOYMENT_TARGET = 14.4; 646 | MTL_ENABLE_DEBUG_INFO = NO; 647 | MTL_FAST_MATH = YES; 648 | SDKROOT = iphoneos; 649 | SWIFT_COMPILATION_MODE = wholemodule; 650 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 651 | VALIDATE_PRODUCT = YES; 652 | }; 653 | name = Release; 654 | }; 655 | 4B61049825C999440058D264 /* Debug */ = { 656 | isa = XCBuildConfiguration; 657 | baseConfigurationReference = B60E67EC035A1585AB95AE5F /* Pods-Demo.debug.xcconfig */; 658 | buildSettings = { 659 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 660 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 661 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 662 | CODE_SIGN_STYLE = Automatic; 663 | DEVELOPMENT_TEAM = KU2QEJ9K3Z; 664 | INFOPLIST_FILE = Demo/Info.plist; 665 | IPHONEOS_DEPLOYMENT_TARGET = 13.0; 666 | LD_RUNPATH_SEARCH_PATHS = ( 667 | "$(inherited)", 668 | "@executable_path/Frameworks", 669 | ); 670 | PRODUCT_BUNDLE_IDENTIFIER = jp.eure.FluidPresentation.Demo; 671 | PRODUCT_NAME = "$(TARGET_NAME)"; 672 | SWIFT_VERSION = 5.0; 673 | TARGETED_DEVICE_FAMILY = "1,2"; 674 | }; 675 | name = Debug; 676 | }; 677 | 4B61049925C999440058D264 /* Release */ = { 678 | isa = XCBuildConfiguration; 679 | baseConfigurationReference = 354789D630D2F51EDA44AF92 /* Pods-Demo.release.xcconfig */; 680 | buildSettings = { 681 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 682 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 683 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 684 | CODE_SIGN_STYLE = Automatic; 685 | DEVELOPMENT_TEAM = KU2QEJ9K3Z; 686 | INFOPLIST_FILE = Demo/Info.plist; 687 | IPHONEOS_DEPLOYMENT_TARGET = 13.0; 688 | LD_RUNPATH_SEARCH_PATHS = ( 689 | "$(inherited)", 690 | "@executable_path/Frameworks", 691 | ); 692 | PRODUCT_BUNDLE_IDENTIFIER = jp.eure.FluidPresentation.Demo; 693 | PRODUCT_NAME = "$(TARGET_NAME)"; 694 | SWIFT_VERSION = 5.0; 695 | TARGETED_DEVICE_FAMILY = "1,2"; 696 | }; 697 | name = Release; 698 | }; 699 | 4B895B1226210D7D00EB7312 /* Debug */ = { 700 | isa = XCBuildConfiguration; 701 | buildSettings = { 702 | CODE_SIGN_STYLE = Automatic; 703 | CURRENT_PROJECT_VERSION = 1; 704 | DEFINES_MODULE = YES; 705 | DEVELOPMENT_TEAM = KU2QEJ9K3Z; 706 | DYLIB_COMPATIBILITY_VERSION = 1; 707 | DYLIB_CURRENT_VERSION = 1; 708 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 709 | INFOPLIST_FILE = FluidPresentation/Info.plist; 710 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 711 | IPHONEOS_DEPLOYMENT_TARGET = 12.1; 712 | LD_RUNPATH_SEARCH_PATHS = ( 713 | "$(inherited)", 714 | "@executable_path/Frameworks", 715 | "@loader_path/Frameworks", 716 | ); 717 | PRODUCT_BUNDLE_IDENTIFIER = jp.eure.FluidPresentation; 718 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; 719 | SKIP_INSTALL = YES; 720 | SWIFT_VERSION = 5.0; 721 | TARGETED_DEVICE_FAMILY = "1,2"; 722 | VERSIONING_SYSTEM = "apple-generic"; 723 | VERSION_INFO_PREFIX = ""; 724 | }; 725 | name = Debug; 726 | }; 727 | 4B895B1326210D7D00EB7312 /* Release */ = { 728 | isa = XCBuildConfiguration; 729 | buildSettings = { 730 | CODE_SIGN_STYLE = Automatic; 731 | CURRENT_PROJECT_VERSION = 1; 732 | DEFINES_MODULE = YES; 733 | DEVELOPMENT_TEAM = KU2QEJ9K3Z; 734 | DYLIB_COMPATIBILITY_VERSION = 1; 735 | DYLIB_CURRENT_VERSION = 1; 736 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 737 | INFOPLIST_FILE = FluidPresentation/Info.plist; 738 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 739 | IPHONEOS_DEPLOYMENT_TARGET = 12.1; 740 | LD_RUNPATH_SEARCH_PATHS = ( 741 | "$(inherited)", 742 | "@executable_path/Frameworks", 743 | "@loader_path/Frameworks", 744 | ); 745 | PRODUCT_BUNDLE_IDENTIFIER = jp.eure.FluidPresentation; 746 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; 747 | SKIP_INSTALL = YES; 748 | SWIFT_VERSION = 5.0; 749 | TARGETED_DEVICE_FAMILY = "1,2"; 750 | VERSIONING_SYSTEM = "apple-generic"; 751 | VERSION_INFO_PREFIX = ""; 752 | }; 753 | name = Release; 754 | }; 755 | /* End XCBuildConfiguration section */ 756 | 757 | /* Begin XCConfigurationList section */ 758 | 4B32B8332654E31B001A31F3 /* Build configuration list for PBXNativeTarget "FluidPresentationTests" */ = { 759 | isa = XCConfigurationList; 760 | buildConfigurations = ( 761 | 4B32B8312654E31B001A31F3 /* Debug */, 762 | 4B32B8322654E31B001A31F3 /* Release */, 763 | ); 764 | defaultConfigurationIsVisible = 0; 765 | defaultConfigurationName = Release; 766 | }; 767 | 4B61047E25C999430058D264 /* Build configuration list for PBXProject "FluidPresentation" */ = { 768 | isa = XCConfigurationList; 769 | buildConfigurations = ( 770 | 4B61049525C999440058D264 /* Debug */, 771 | 4B61049625C999440058D264 /* Release */, 772 | ); 773 | defaultConfigurationIsVisible = 0; 774 | defaultConfigurationName = Release; 775 | }; 776 | 4B61049725C999440058D264 /* Build configuration list for PBXNativeTarget "Demo" */ = { 777 | isa = XCConfigurationList; 778 | buildConfigurations = ( 779 | 4B61049825C999440058D264 /* Debug */, 780 | 4B61049925C999440058D264 /* Release */, 781 | ); 782 | defaultConfigurationIsVisible = 0; 783 | defaultConfigurationName = Release; 784 | }; 785 | 4B895B1426210D7D00EB7312 /* Build configuration list for PBXNativeTarget "FluidPresentation" */ = { 786 | isa = XCConfigurationList; 787 | buildConfigurations = ( 788 | 4B895B1226210D7D00EB7312 /* Debug */, 789 | 4B895B1326210D7D00EB7312 /* Release */, 790 | ); 791 | defaultConfigurationIsVisible = 0; 792 | defaultConfigurationName = Release; 793 | }; 794 | /* End XCConfigurationList section */ 795 | }; 796 | rootObject = 4B61047B25C999430058D264 /* Project object */; 797 | } 798 | -------------------------------------------------------------------------------- /FluidPresentation.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /FluidPresentation.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /FluidPresentation.xcodeproj/xcshareddata/xcschemes/Demo.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 45 | 51 | 52 | 53 | 54 | 60 | 62 | 68 | 69 | 70 | 71 | 73 | 74 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /FluidPresentation.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /FluidPresentation.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /FluidPresentation/Context.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2021 Copyright (c) 2021 Eureka, Inc. 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | 22 | import UIKit 23 | 24 | extension FluidViewController { 25 | 26 | public var fluidContext: FluidPresentationContext { 27 | .init(presentedViewController: self) 28 | } 29 | 30 | } 31 | 32 | public struct FluidPresentationContext { 33 | 34 | public let presentedViewController: FluidViewController 35 | 36 | init(presentedViewController: FluidViewController) { 37 | self.presentedViewController = presentedViewController 38 | } 39 | 40 | } 41 | 42 | extension FluidPresentationContext { 43 | 44 | /** 45 | Presents this view controller in the view controller as contextually. 46 | The presenting view controller must be a presentation context. 47 | Make sure the view controller is the presentation context with `UIViewController.definesPresentationContext`. 48 | */ 49 | public func present( 50 | in presentingViewController: UIViewController, 51 | animated: Bool, 52 | completion: (() -> Void)? 53 | ) { 54 | 55 | assert( 56 | presentingViewController.definesPresentationContext == true, 57 | """ 58 | The presenting view controller \(presentingViewController) does not define PresentationContext. 59 | Make sure \(presentingViewController).definesPresentationContext returns `true`. 60 | """ 61 | ) 62 | 63 | presentedViewController.modalPresentationStyle = presentedViewController.wantsTransparentBackground ? .overCurrentContext : .currentContext 64 | 65 | presentingViewController 66 | .present( 67 | presentedViewController, 68 | animated: animated, 69 | completion: completion 70 | ) 71 | 72 | } 73 | 74 | /** 75 | Presents this view controller as full screen. 76 | Technically, `modalPresentationStyle` would be set `.overFullScreen` or `fullScreen`. 77 | Which would be used depends on `wantsTransparentBackground` 78 | 79 | How's finding the presenting view controller. 80 | - a child view controller forwards calling `present` to the parent view controller. 81 | */ 82 | public func present( 83 | from presentingViewController: UIViewController, 84 | animated: Bool, 85 | completion: (() -> Void)? 86 | ) { 87 | 88 | presentedViewController.modalPresentationStyle = presentedViewController.wantsTransparentBackground ? .overFullScreen : .fullScreen 89 | 90 | presentingViewController 91 | .present( 92 | presentedViewController, 93 | animated: animated, 94 | completion: completion 95 | ) 96 | 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /FluidPresentation/FluidViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2021 Copyright (c) 2021 Eureka, Inc. 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | 22 | #import 23 | 24 | //! Project version number for FluidViewController. 25 | FOUNDATION_EXPORT double FluidViewControllerVersionNumber; 26 | 27 | //! Project version string for FluidViewController. 28 | FOUNDATION_EXPORT const unsigned char FluidViewControllerVersionString[]; 29 | 30 | // In this header, you should import all the public headers of your framework using statements like #import 31 | 32 | 33 | -------------------------------------------------------------------------------- /FluidPresentation/FluidViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2021 Copyright (c) 2021 Eureka, Inc. 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | 22 | import Foundation 23 | import UIKit 24 | 25 | open class FluidViewController: UIViewController, UIViewControllerTransitioningDelegate, UIGestureRecognizerDelegate { 26 | 27 | /// Indicating the transition how it animates. 28 | public enum Idiom { 29 | case presentation 30 | case navigationPush(isScreenGestureEnabled: Bool) 31 | 32 | public static var navigationPush: Self { 33 | .navigationPush(isScreenGestureEnabled: false) 34 | } 35 | } 36 | 37 | /** 38 | - Warning: Under constructions 39 | */ 40 | public enum PresentingTransition { 41 | 42 | public enum SlideInFrom: Hashable { 43 | case right 44 | case bottom 45 | } 46 | 47 | case slideIn(from: SlideInFrom) 48 | case custom(using: () -> UIViewControllerAnimatedTransitioning) 49 | 50 | } 51 | 52 | /** 53 | - Warning: Under constructions 54 | */ 55 | public enum DismissingTransition { 56 | 57 | public enum SlideOutTo: Hashable { 58 | case right 59 | case bottom 60 | } 61 | 62 | case slideOut(to: SlideOutTo) 63 | case custom(using: () -> UIViewControllerAnimatedTransitioning) 64 | } 65 | 66 | /** 67 | - Warning: Under constructions 68 | */ 69 | public struct DismissingIntereaction: Hashable { 70 | 71 | public enum Trigger: Hashable { 72 | 73 | /// Available dismissing gesture in the edge of the screen. 74 | case edge 75 | 76 | /// Available dismissing gesture in screen anywhere. 77 | case screen 78 | } 79 | 80 | public enum StartFrom: Hashable { 81 | case left 82 | // case right 83 | // case top 84 | // case bottom 85 | } 86 | 87 | public let trigger: Trigger 88 | public let startFrom: StartFrom 89 | 90 | public init( 91 | trigger: FluidViewController.DismissingIntereaction.Trigger, 92 | startFrom: FluidViewController.DismissingIntereaction.StartFrom 93 | ) { 94 | self.trigger = trigger 95 | self.startFrom = startFrom 96 | } 97 | 98 | } 99 | 100 | // MARK: - Properties 101 | 102 | public var wantsTransparentBackground: Bool = false 103 | 104 | public override var childForStatusBarStyle: UIViewController? { 105 | return bodyViewController 106 | } 107 | 108 | public override var childForStatusBarHidden: UIViewController? { 109 | return bodyViewController 110 | } 111 | 112 | public let bodyViewController: UIViewController? 113 | 114 | @available(*, unavailable, message: "Unsupported") 115 | open override var navigationController: UINavigationController? { 116 | super.navigationController 117 | } 118 | 119 | private var leftToRightTrackingContext: LeftToRightTrackingContext? 120 | 121 | private var isTracking = false 122 | 123 | private var isValidGestureDismissal: Bool { 124 | modalPresentationStyle != .pageSheet 125 | } 126 | 127 | private let scrollController = ScrollController() 128 | 129 | public var presentingTransition: PresentingTransition = .slideIn(from: .bottom) 130 | public var dismissingTransition: DismissingTransition = .slideOut(to: .bottom) 131 | 132 | public var dismissingInteractions: Set = [] { 133 | didSet { 134 | if isViewLoaded { 135 | setupGestures() 136 | } 137 | } 138 | } 139 | 140 | public var interactiveUnwindGestureRecognizer: UIPanGestureRecognizer? 141 | 142 | public var interactiveEdgeUnwindGestureRecognizer: UIScreenEdgePanGestureRecognizer? 143 | 144 | private var registeredGestures: [UIGestureRecognizer] = [] 145 | 146 | // MARK: - Initializers 147 | 148 | /// Creates an instance 149 | /// 150 | /// - Parameters: 151 | /// - idiom: 152 | /// - bodyViewController: a view controller that displays as a child view controller. It helps a case of can't create a subclass of FluidViewController. 153 | public init( 154 | idiom: Idiom? = nil, 155 | bodyViewController: UIViewController? = nil 156 | ) { 157 | self.bodyViewController = bodyViewController 158 | super.init(nibName: nil, bundle: nil) 159 | setIdiom(idiom ?? .presentation) 160 | 161 | modalPresentationStyle = .fullScreen 162 | transitioningDelegate = self 163 | modalPresentationCapturesStatusBarAppearance = true 164 | } 165 | 166 | @available(*, unavailable) 167 | public required init?( 168 | coder: NSCoder 169 | ) { 170 | fatalError() 171 | } 172 | 173 | // MARK: - Functions 174 | 175 | open override func viewDidLoad() { 176 | super.viewDidLoad() 177 | 178 | setupGestures() 179 | 180 | if let bodyViewController = bodyViewController { 181 | addChild(bodyViewController) 182 | view.addSubview(bodyViewController.view) 183 | NSLayoutConstraint.activate([ 184 | bodyViewController.view.topAnchor.constraint(equalTo: view.topAnchor), 185 | bodyViewController.view.rightAnchor.constraint(equalTo: view.rightAnchor), 186 | bodyViewController.view.leftAnchor.constraint(equalTo: view.leftAnchor), 187 | bodyViewController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor), 188 | ]) 189 | bodyViewController.didMove(toParent: self) 190 | } 191 | } 192 | 193 | /// Set presenting and dismissing transition according to the idiom 194 | /// - Parameter idiom: 195 | public func setIdiom(_ idiom: Idiom) { 196 | 197 | switch idiom { 198 | case .presentation: 199 | self.presentingTransition = .slideIn(from: .bottom) 200 | self.dismissingTransition = .slideOut(to: .bottom) 201 | self.dismissingInteractions = [] 202 | case .navigationPush(let isScreenGestureEnabled): 203 | self.presentingTransition = .slideIn(from: .right) 204 | self.dismissingTransition = .slideOut(to: .right) 205 | 206 | if isScreenGestureEnabled { 207 | self.dismissingInteractions = [.init(trigger: .screen, startFrom: .left)] 208 | } else { 209 | self.dismissingInteractions = [.init(trigger: .edge, startFrom: .left)] 210 | } 211 | 212 | } 213 | 214 | } 215 | 216 | private func setupGestures() { 217 | 218 | if leftToRightTrackingContext != nil { 219 | assertionFailure("Unable to set gestures up while transitioning.") 220 | return 221 | } 222 | 223 | registeredGestures.forEach { 224 | view.removeGestureRecognizer($0) 225 | } 226 | registeredGestures = [] 227 | 228 | do { 229 | if dismissingInteractions.filter({ $0.trigger == .screen }).isEmpty == false { 230 | let panGesture = _PanGestureRecognizer(target: self, action: #selector(handlePanGesture)) 231 | view.addGestureRecognizer(panGesture) 232 | panGesture.delegate = self 233 | self.interactiveUnwindGestureRecognizer = panGesture 234 | 235 | registeredGestures.append(panGesture) 236 | } 237 | } 238 | 239 | do { 240 | 241 | dismissingInteractions 242 | .filter { 243 | $0.trigger == .edge 244 | } 245 | .forEach { 246 | switch $0.startFrom { 247 | case .left: 248 | let edgeGesture = _EdgePanGestureRecognizer(target: self, action: #selector(handleEdgeLeftPanGesture)) 249 | edgeGesture.edges = .left 250 | view.addGestureRecognizer(edgeGesture) 251 | edgeGesture.delegate = self 252 | self.interactiveEdgeUnwindGestureRecognizer = edgeGesture 253 | registeredGestures.append(edgeGesture) 254 | 255 | } 256 | } 257 | 258 | } 259 | } 260 | 261 | @objc 262 | private func handleEdgeLeftPanGesture(_ gesture: _EdgePanGestureRecognizer) { 263 | 264 | guard parent == nil else { return } 265 | 266 | switch gesture.state { 267 | case .possible: 268 | break 269 | case .began: 270 | 271 | if leftToRightTrackingContext == nil { 272 | 273 | if let scrollView = gesture.trackingScrollView { 274 | 275 | scrollController.startTracking(scrollView: scrollView) 276 | scrollController.lockScrolling() 277 | } 278 | 279 | leftToRightTrackingContext = .init( 280 | viewFrame: view.bounds, 281 | beganPoint: gesture.location(in: view), 282 | controller: .init() 283 | ) 284 | 285 | dismiss(animated: true, completion: nil) 286 | } 287 | 288 | case .changed: 289 | leftToRightTrackingContext?.handleChanged(gesture: gesture) 290 | case .ended: 291 | scrollController.unlockScrolling() 292 | scrollController.endTracking() 293 | leftToRightTrackingContext?.handleEnded(gesture: gesture) 294 | leftToRightTrackingContext = nil 295 | case .cancelled, .failed: 296 | scrollController.unlockScrolling() 297 | scrollController.endTracking() 298 | leftToRightTrackingContext?.handleCancel(gesture: gesture) 299 | leftToRightTrackingContext = nil 300 | @unknown default: 301 | break 302 | } 303 | 304 | } 305 | 306 | @objc 307 | private func handlePanGesture(_ gesture: _PanGestureRecognizer) { 308 | 309 | guard parent == nil else { return } 310 | 311 | switch gesture.state { 312 | case .possible: 313 | break 314 | case .began: 315 | 316 | break 317 | 318 | case .changed: 319 | 320 | if leftToRightTrackingContext == nil { 321 | 322 | if abs(gesture.translation(in: view).y) > 5 { 323 | gesture.state = .failed 324 | return 325 | } 326 | 327 | if gesture.translation(in: view).x < -5 { 328 | gesture.state = .failed 329 | return 330 | } 331 | 332 | if gesture.translation(in: view).x > 0 { 333 | 334 | if let scrollView = gesture.trackingScrollView { 335 | 336 | let representation = ScrollViewRepresentation(from: scrollView) 337 | 338 | if representation.isReachedToEdge(.left) { 339 | 340 | scrollController.startTracking(scrollView: scrollView) 341 | 342 | } else { 343 | gesture.state = .failed 344 | return 345 | } 346 | 347 | } 348 | 349 | leftToRightTrackingContext = .init( 350 | viewFrame: view.bounds, 351 | beganPoint: gesture.location(in: view), 352 | controller: .init() 353 | ) 354 | 355 | scrollController.lockScrolling() 356 | dismiss(animated: true, completion: { 357 | /// Transition was completed or cancelled. 358 | self.leftToRightTrackingContext = nil 359 | }) 360 | } 361 | } 362 | 363 | if isBeingDismissed { 364 | leftToRightTrackingContext?.handleChanged(gesture: gesture) 365 | } 366 | case .ended: 367 | scrollController.unlockScrolling() 368 | scrollController.endTracking() 369 | leftToRightTrackingContext?.handleEnded(gesture: gesture) 370 | case .cancelled, .failed: 371 | scrollController.unlockScrolling() 372 | scrollController.endTracking() 373 | leftToRightTrackingContext?.handleCancel(gesture: gesture) 374 | @unknown default: 375 | break 376 | } 377 | 378 | } 379 | 380 | public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { 381 | 382 | if gestureRecognizer is UIScreenEdgePanGestureRecognizer { 383 | 384 | return (otherGestureRecognizer is UIScreenEdgePanGestureRecognizer) == false 385 | } 386 | 387 | return true 388 | } 389 | 390 | public func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? { 391 | 392 | switch modalPresentationStyle { 393 | case .fullScreen, .currentContext, .overFullScreen, .overCurrentContext: 394 | switch presentingTransition { 395 | case .custom(let transitionController): 396 | return transitionController() 397 | case .slideIn(let from): 398 | switch from { 399 | case .bottom: 400 | return PresentingTransitionControllers.BottomToTopTransitionController() 401 | case .right: 402 | return PresentingTransitionControllers.RightToLeftTransitionController() 403 | } 404 | } 405 | case .pageSheet, .formSheet, .custom, .popover, .none, .automatic: 406 | return nil 407 | @unknown default: 408 | return nil 409 | } 410 | 411 | } 412 | 413 | public func interactionControllerForPresentation(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? { 414 | 415 | switch modalPresentationStyle { 416 | case .fullScreen, .currentContext, .overFullScreen, .overCurrentContext: 417 | switch presentingTransition { 418 | case .custom: 419 | return nil 420 | case .slideIn(let from): 421 | switch from { 422 | case .bottom: 423 | return PresentingTransitionControllers.BottomToTopTransitionController() 424 | case .right: 425 | return PresentingTransitionControllers.RightToLeftTransitionController() 426 | } 427 | } 428 | case .pageSheet, .formSheet, .custom, .popover, .none, .automatic: 429 | return nil 430 | @unknown default: 431 | return nil 432 | } 433 | 434 | } 435 | 436 | public func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? { 437 | 438 | switch modalPresentationStyle { 439 | case .fullScreen, .currentContext, .overFullScreen, .overCurrentContext: 440 | switch dismissingTransition { 441 | case .custom(let transitionController): 442 | return transitionController() 443 | case .slideOut(let to): 444 | switch to { 445 | case .bottom: 446 | return DismissingTransitionControllers.TopToBottomTransitionController() 447 | case .right: 448 | return DismissingTransitionControllers.LeftToRightTransitionController() 449 | } 450 | } 451 | case .pageSheet, .formSheet, .custom, .popover, .none, .automatic: 452 | return nil 453 | @unknown default: 454 | return nil 455 | } 456 | 457 | } 458 | 459 | 460 | 461 | public func interactionControllerForDismissal(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? { 462 | if let controller = leftToRightTrackingContext?.controller { 463 | Log.debug(.generic, "Start Interactive Dismiss") 464 | return controller 465 | } 466 | return nil 467 | } 468 | } 469 | 470 | extension FluidViewController { 471 | 472 | private final class LeftToRightTrackingContext { 473 | 474 | typealias TransitionController = DismissingInteractiveTransitionControllers.LeftToRightTransitionController 475 | 476 | let viewFrame: CGRect 477 | let beganPoint: CGPoint 478 | let controller: TransitionController 479 | 480 | init( 481 | viewFrame: CGRect, 482 | beganPoint: CGPoint, 483 | controller: TransitionController 484 | ) { 485 | self.viewFrame = viewFrame 486 | self.beganPoint = beganPoint 487 | self.controller = controller 488 | } 489 | 490 | func handleChanged(gesture: UIPanGestureRecognizer) { 491 | let progress = calulateProgress(gesture: gesture) 492 | controller.updateProgress(progress) 493 | } 494 | 495 | func handleEnded(gesture: UIPanGestureRecognizer) { 496 | 497 | let progress = calulateProgress(gesture: gesture) 498 | let velocity = gesture.velocity(in: gesture.view) 499 | 500 | if progress > 0.5 || velocity.x > 300 { 501 | controller.finishInteractiveTransition(velocityX: normalizedVelocity(gesture: gesture)) 502 | } else { 503 | controller.cancelInteractiveTransition() 504 | } 505 | 506 | } 507 | 508 | func handleCancel(gesture: UIPanGestureRecognizer) { 509 | controller.cancelInteractiveTransition() 510 | } 511 | 512 | private func normalizedVelocity(gesture: UIPanGestureRecognizer) -> CGFloat { 513 | let velocityX = gesture.velocity(in: gesture.view).x 514 | return velocityX / viewFrame.width 515 | } 516 | 517 | private func calulateProgress(gesture: UIPanGestureRecognizer) -> CGFloat { 518 | let targetView = gesture.view! 519 | let t = targetView.transform 520 | targetView.transform = .identity 521 | let position = gesture.location(in: targetView) 522 | targetView.transform = t 523 | 524 | let progress = (position.x - beganPoint.x) / viewFrame.width 525 | return progress 526 | } 527 | } 528 | 529 | } 530 | 531 | final class _EdgePanGestureRecognizer: UIScreenEdgePanGestureRecognizer { 532 | 533 | weak var trackingScrollView: UIScrollView? 534 | 535 | override func touchesBegan(_ touches: Set, with event: UIEvent) { 536 | trackingScrollView = event.findScrollView() 537 | super.touchesBegan(touches, with: event) 538 | } 539 | 540 | override func touchesMoved(_ touches: Set, with event: UIEvent) { 541 | 542 | super.touchesMoved(touches, with: event) 543 | } 544 | 545 | override func touchesEnded(_ touches: Set, with event: UIEvent) { 546 | super.touchesEnded(touches, with: event) 547 | } 548 | 549 | override func touchesCancelled(_ touches: Set, with event: UIEvent) { 550 | super.touchesCancelled(touches, with: event) 551 | } 552 | 553 | } 554 | 555 | final class _PanGestureRecognizer: UIPanGestureRecognizer { 556 | 557 | weak var trackingScrollView: UIScrollView? 558 | 559 | override func touchesBegan(_ touches: Set, with event: UIEvent) { 560 | trackingScrollView = event.findScrollView() 561 | super.touchesBegan(touches, with: event) 562 | } 563 | 564 | override func touchesMoved(_ touches: Set, with event: UIEvent) { 565 | 566 | super.touchesMoved(touches, with: event) 567 | } 568 | 569 | override func touchesEnded(_ touches: Set, with event: UIEvent) { 570 | super.touchesEnded(touches, with: event) 571 | } 572 | 573 | override func touchesCancelled(_ touches: Set, with event: UIEvent) { 574 | super.touchesCancelled(touches, with: event) 575 | } 576 | 577 | } 578 | 579 | extension UIEvent { 580 | 581 | fileprivate func findScrollView() -> UIScrollView? { 582 | 583 | guard 584 | let firstTouch = allTouches?.first, 585 | let targetView = firstTouch.view 586 | else { return nil } 587 | 588 | let scrollView = sequence(first: targetView, next: \.next).map { $0 } 589 | .first { 590 | guard let scrollView = $0 as? UIScrollView else { 591 | return false 592 | } 593 | 594 | func isScrollable(scrollView: UIScrollView) -> Bool { 595 | 596 | let contentInset: UIEdgeInsets 597 | 598 | if #available(iOS 11.0, *) { 599 | contentInset = scrollView.adjustedContentInset 600 | } else { 601 | contentInset = scrollView.contentInset 602 | } 603 | 604 | return (scrollView.bounds.width - (contentInset.right + contentInset.left) <= scrollView.contentSize.width) || (scrollView.bounds.height - (contentInset.top + contentInset.bottom) <= scrollView.contentSize.height) 605 | } 606 | 607 | return isScrollable(scrollView: scrollView) 608 | } 609 | 610 | return (scrollView as? UIScrollView) 611 | } 612 | 613 | } 614 | -------------------------------------------------------------------------------- /FluidPresentation/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 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | 22 | 23 | -------------------------------------------------------------------------------- /FluidPresentation/Log.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2021 Copyright (c) 2021 Eureka, Inc. 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | 22 | import os.log 23 | 24 | enum Log { 25 | 26 | static func debug(_ log: OSLog, _ object: Any...) { 27 | os_log(.debug, log: log, "%@", object.map { "\($0)" }.joined(separator: " ")) 28 | } 29 | 30 | static func error(_ log: OSLog, _ object: Any...) { 31 | os_log(.error, log: log, "%@", object.map { "\($0)" }.joined(separator: " ")) 32 | } 33 | } 34 | 35 | extension OSLog { 36 | 37 | static let generic: OSLog = { 38 | #if DEBUG 39 | return OSLog.init(subsystem: "FluidViewController", category: "general") 40 | #else 41 | return .disabled 42 | #endif 43 | }() 44 | 45 | static let interactive: OSLog = { 46 | #if DEBUG 47 | return OSLog.init(subsystem: "FluidViewController", category: "interactive") 48 | #else 49 | return .disabled 50 | #endif 51 | }() 52 | 53 | static let transition: OSLog = { 54 | #if DEBUG 55 | return OSLog.init(subsystem: "FluidViewController", category: "transition") 56 | #else 57 | return .disabled 58 | #endif 59 | }() 60 | 61 | } 62 | 63 | -------------------------------------------------------------------------------- /FluidPresentation/NavigatedFluidViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2021 Copyright (c) 2021 Eureka, Inc. 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | 22 | import Foundation 23 | import UIKit 24 | 25 | /// A extended view controller from FluidViewController. 26 | /// Witch has a standalone UINavigationBar that displays navigationItem from itself or bodyViewController. 27 | open class NavigatedFluidViewController: FluidViewController, UINavigationBarDelegate { 28 | 29 | public let navigationBar: UINavigationBar 30 | 31 | public init( 32 | idiom: Idiom = .presentation, 33 | bodyViewController: UIViewController? = nil, 34 | unwindBarButtonItem: UIBarButtonItem? = nil, 35 | navigationBarClass: UINavigationBar.Type = UINavigationBar.self 36 | ) { 37 | self.navigationBar = navigationBarClass.init() 38 | super.init(idiom: idiom, bodyViewController: bodyViewController) 39 | 40 | let targetNavigationItem = bodyViewController?.navigationItem ?? navigationItem 41 | 42 | if targetNavigationItem.leftBarButtonItem == nil { 43 | 44 | let _unwindBarButtonItem = unwindBarButtonItem ?? { 45 | let button: UIBarButtonItem 46 | 47 | switch idiom { 48 | case .navigationPush: 49 | button = .init(barButtonSystemItem: .init(rawValue: 101)!, target: nil, action: nil) 50 | case .presentation: 51 | button = .init(title: "Dismiss", style: .plain, target: nil, action: nil) 52 | } 53 | return button 54 | }() 55 | 56 | _unwindBarButtonItem.target = self 57 | _unwindBarButtonItem.action = #selector(_onTapUnwindButton) 58 | targetNavigationItem.leftBarButtonItem = _unwindBarButtonItem 59 | 60 | } 61 | 62 | } 63 | 64 | open override func viewDidLoad() { 65 | super.viewDidLoad() 66 | 67 | view.addSubview(navigationBar) 68 | 69 | navigationBar.translatesAutoresizingMaskIntoConstraints = false 70 | NSLayoutConstraint.activate([ 71 | navigationBar.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), 72 | navigationBar.rightAnchor.constraint(equalTo: view.rightAnchor), 73 | navigationBar.leftAnchor.constraint(equalTo: view.leftAnchor), 74 | ]) 75 | 76 | navigationBar.delegate = self 77 | 78 | if let bodyViewController = bodyViewController { 79 | navigationBar.pushItem(bodyViewController.navigationItem, animated: false) 80 | } else { 81 | navigationBar.pushItem(navigationItem, animated: false) 82 | } 83 | 84 | } 85 | 86 | @objc private func _onTapUnwindButton() { 87 | dismiss(animated: true, completion: nil) 88 | } 89 | 90 | open override func viewDidLayoutSubviews() { 91 | additionalSafeAreaInsets.top = navigationBar.frame.height 92 | view.bringSubviewToFront(navigationBar) 93 | } 94 | 95 | public func position(for bar: UIBarPositioning) -> UIBarPosition { 96 | return .topAttached 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /FluidPresentation/ScrollView+Handling.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2021 Copyright (c) 2021 Eureka, Inc. 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | 22 | 23 | import UIKit 24 | 25 | final class ScrollController { 26 | 27 | private var scrollObserver: NSKeyValueObservation? 28 | private var shouldStop: Bool = false 29 | private var previousValue: CGPoint? 30 | private weak var trackingScrollView: UIScrollView? 31 | private var originalShowsVerticalScrollIndicator = true 32 | 33 | init() { 34 | 35 | } 36 | 37 | func lockScrolling() { 38 | shouldStop = true 39 | } 40 | 41 | func unlockScrolling() { 42 | shouldStop = false 43 | } 44 | 45 | func startTracking(scrollView: UIScrollView) { 46 | self.trackingScrollView = scrollView 47 | self.originalShowsVerticalScrollIndicator = scrollView.showsVerticalScrollIndicator 48 | 49 | scrollObserver?.invalidate() 50 | scrollObserver = scrollView.observe(\.contentOffset, options: .old) { [weak self, weak _scrollView = scrollView] scrollView, change in 51 | 52 | guard let scrollView = _scrollView else { return } 53 | guard let self = self else { return } 54 | self.handleScrollViewEvent(scrollView: scrollView, change: change) 55 | } 56 | } 57 | 58 | func endTracking() { 59 | scrollObserver?.invalidate() 60 | trackingScrollView?.showsVerticalScrollIndicator = originalShowsVerticalScrollIndicator 61 | scrollObserver = nil 62 | } 63 | 64 | private func handleScrollViewEvent(scrollView: UIScrollView, change: NSKeyValueObservedChange) { 65 | 66 | guard var proposedValue = change.oldValue else { return } 67 | 68 | guard shouldStop else { 69 | scrollView.showsVerticalScrollIndicator = true 70 | return 71 | } 72 | 73 | guard scrollView.contentOffset != proposedValue else { return } 74 | 75 | guard proposedValue != previousValue else { return } 76 | 77 | let representation = ScrollViewRepresentation(from: scrollView) 78 | 79 | if representation.isReachedToEdge(.top) { 80 | proposedValue = representation.contentOffsetFitToEdge(.top, contentOffset: proposedValue) 81 | } 82 | 83 | if representation.isReachedToEdge(.right) { 84 | proposedValue = representation.contentOffsetFitToEdge(.right, contentOffset: proposedValue) 85 | } 86 | 87 | if representation.isReachedToEdge(.left) { 88 | proposedValue = representation.contentOffsetFitToEdge(.left, contentOffset: proposedValue) 89 | } 90 | 91 | if representation.isReachedToEdge(.bottom) { 92 | proposedValue = representation.contentOffsetFitToEdge(.bottom, contentOffset: proposedValue) 93 | } 94 | 95 | previousValue = scrollView.contentOffset 96 | 97 | scrollView.setContentOffset(proposedValue, animated: false) 98 | scrollView.showsVerticalScrollIndicator = false 99 | } 100 | 101 | } 102 | 103 | struct ScrollViewRepresentation { 104 | 105 | enum Edge { 106 | case top 107 | case left 108 | case right 109 | case bottom 110 | } 111 | 112 | let contentInset: UIEdgeInsets 113 | let contentOffset: CGPoint 114 | let contentSize: CGSize 115 | let bounds: CGRect 116 | 117 | init( 118 | from scrollView: UIScrollView 119 | ) { 120 | 121 | self.contentOffset = scrollView.contentOffset 122 | if #available(iOS 11.0, *) { 123 | self.contentInset = scrollView.adjustedContentInset 124 | } else { 125 | self.contentInset = scrollView.contentInset 126 | } 127 | self.bounds = scrollView.bounds 128 | self.contentSize = scrollView.contentSize 129 | } 130 | 131 | func isReachedToEdge(_ edge: Edge) -> Bool { 132 | 133 | switch edge { 134 | case .top: 135 | return -contentInset.top >= contentOffset.y 136 | case .left: 137 | return -contentInset.left >= contentOffset.x 138 | case .right: 139 | return (contentSize.width - bounds.width + contentInset.right) <= contentOffset.x 140 | case .bottom: 141 | return (contentSize.height - bounds.height + contentInset.bottom) <= contentOffset.y 142 | } 143 | 144 | } 145 | 146 | func contentOffsetFitToEdge(_ edge: Edge, contentOffset: CGPoint) -> CGPoint { 147 | 148 | switch edge { 149 | case .top: 150 | return .init(x: contentOffset.x, y: -contentInset.top) 151 | case .left: 152 | return .init(x: -contentInset.left, y: contentOffset.y) 153 | case .right: 154 | return .init(x: (contentSize.width - bounds.width + contentInset.right), y: contentOffset.y) 155 | case .bottom: 156 | return .init(x: contentOffset.x, y: (contentSize.height - bounds.height + contentInset.bottom)) 157 | } 158 | 159 | } 160 | 161 | } 162 | -------------------------------------------------------------------------------- /FluidPresentation/TransitionControllers.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2021 Copyright (c) 2021 Eureka, Inc. 3 | // 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy 5 | // of this software and associated documentation files (the "Software"), to deal 6 | // in the Software without restriction, including without limitation the rights 7 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | // copies of the Software, and to permit persons to whom the Software is 9 | // furnished to do so, subject to the following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included in 12 | // all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | // THE SOFTWARE. 21 | 22 | import UIKit 23 | 24 | private final class DropShadowContainerView: UIView { 25 | 26 | override func layoutSubviews() { 27 | 28 | super.layoutSubviews() 29 | 30 | layer.shadowPath = UIBezierPath(rect: bounds).cgPath 31 | layer.shadowColor = UIColor.init(white: 0, alpha: 0.2).cgColor 32 | layer.shadowRadius = 2 33 | layer.shadowOffset = .zero 34 | layer.shadowOpacity = 1 35 | 36 | } 37 | 38 | } 39 | 40 | private func resorationHierarchyForDismissing(toView view: UIView) -> (Bool) -> Void { 41 | 42 | guard let superview = view.superview else { 43 | return { [weak view] didComplete in 44 | if !didComplete { 45 | /** 46 | If toView has not superview, needs to be removed from superview to prevent vanishing toView in the next time dismissing completed. 47 | - toView would be added in UITransitionView in the current context. 48 | */ 49 | view?.removeFromSuperview() 50 | } 51 | } 52 | } 53 | 54 | guard let index = superview.subviews.firstIndex(of: view) else { 55 | return { _ in } 56 | } 57 | 58 | return { [weak superview, weak view] didComplete in 59 | guard let superview = superview, let view = view else { return } 60 | superview.insertSubview(view, at: index) 61 | } 62 | } 63 | 64 | private struct ViewProperties { 65 | 66 | var alpha: CGFloat 67 | var transform: CGAffineTransform 68 | 69 | init( 70 | from view: UIView 71 | ) { 72 | self.alpha = view.alpha 73 | self.transform = view.transform 74 | } 75 | 76 | func restore(in view: UIView) { 77 | view.alpha = alpha 78 | view.transform = transform 79 | } 80 | 81 | } 82 | 83 | func _makeResorationClosure(view: UIView?) -> () -> Void { 84 | guard let view = view else { return {} } 85 | let properties = ViewProperties(from: view) 86 | return { [weak view] in 87 | guard let view = view else { return } 88 | properties.restore(in: view) 89 | } 90 | } 91 | 92 | func _makeResorationClosure(views: [UIView?]) -> () -> Void { 93 | 94 | let restorations = views.map { 95 | _makeResorationClosure(view: $0) 96 | } 97 | 98 | return { 99 | restorations.forEach { 100 | $0() 101 | } 102 | } 103 | 104 | } 105 | 106 | enum PresentingTransitionControllers { 107 | 108 | final class BottomToTopTransitionController: NSObject, UIViewControllerAnimatedTransitioning, 109 | UIViewControllerInteractiveTransitioning 110 | { 111 | 112 | func transitionDuration( 113 | using transitionContext: UIViewControllerContextTransitioning? 114 | ) -> TimeInterval { 115 | 0 116 | } 117 | 118 | func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { 119 | 120 | assertionFailure("Unimplemented") 121 | 122 | } 123 | 124 | func startInteractiveTransition(_ transitionContext: UIViewControllerContextTransitioning) { 125 | 126 | transitionContext.logDebug() 127 | 128 | let toView = transitionContext.view(forKey: .to)! 129 | 130 | transitionContext.containerView.addSubview(toView) 131 | 132 | toView.transform = .init(translationX: 0, y: toView.bounds.height) 133 | 134 | let animator = UIViewPropertyAnimator(duration: 0.55, dampingRatio: 1) { 135 | toView.transform = .identity 136 | } 137 | 138 | animator.addCompletion { _ in 139 | transitionContext.updateInteractiveTransition(1) 140 | transitionContext.finishInteractiveTransition() 141 | transitionContext.completeTransition(true) 142 | } 143 | 144 | animator.startAnimation() 145 | 146 | } 147 | 148 | } 149 | 150 | final class RightToLeftTransitionController: NSObject, UIViewControllerAnimatedTransitioning, 151 | UIViewControllerInteractiveTransitioning 152 | { 153 | 154 | func transitionDuration( 155 | using transitionContext: UIViewControllerContextTransitioning? 156 | ) -> TimeInterval { 157 | 0 158 | } 159 | 160 | func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { 161 | assertionFailure() 162 | 163 | } 164 | 165 | func startInteractiveTransition(_ transitionContext: UIViewControllerContextTransitioning) { 166 | 167 | transitionContext.logDebug() 168 | 169 | let fromViewController = transitionContext.viewController(forKey: .from)! 170 | let toViewController = transitionContext.viewController(forKey: .to)! 171 | 172 | let fromNavigationBar = (fromViewController as? NavigatedFluidViewController)?.navigationBar 173 | let toNavigationBar = (toViewController as? NavigatedFluidViewController)?.navigationBar 174 | 175 | let fromView = fromViewController.view! 176 | let toView = toViewController.view! 177 | 178 | transitionContext.containerView.backgroundColor = .white 179 | transitionContext.containerView.addSubview(fromView) 180 | transitionContext.containerView.addSubview(toView) 181 | 182 | let restoration = _makeResorationClosure(views: [ 183 | toView, 184 | fromView, 185 | fromNavigationBar, 186 | toNavigationBar, 187 | ]) 188 | 189 | makeInitialState: do { 190 | if let _ = fromNavigationBar, let toNavigationBar = toNavigationBar { 191 | /// NavigationBar transition 192 | toNavigationBar.transform = .init(translationX: -fromView.bounds.width, y: 0) 193 | } 194 | toView.transform = .init(translationX: toView.bounds.width, y: 0) 195 | toView.alpha = 0 196 | } 197 | 198 | let animator = UIViewPropertyAnimator(duration: 0.65, dampingRatio: 1) { 199 | 200 | if let fromNavigationBar = fromNavigationBar, let toNavigationBar = toNavigationBar { 201 | /// NavigationBar transition 202 | fromNavigationBar.transform = .init(translationX: fromView.bounds.width, y: 0) 203 | toNavigationBar.transform = .identity 204 | } 205 | 206 | fromView.transform = .init(translationX: -toView.bounds.width, y: 0) 207 | fromView.alpha = 0.02 208 | toView.transform = .identity 209 | toView.alpha = 1 210 | } 211 | 212 | animator.addCompletion { _ in 213 | restoration() 214 | transitionContext.completeTransition(true) 215 | } 216 | 217 | animator.startAnimation() 218 | } 219 | 220 | } 221 | } 222 | 223 | enum DismissingTransitionControllers { 224 | 225 | final class TopToBottomTransitionController: NSObject, UIViewControllerAnimatedTransitioning { 226 | 227 | func transitionDuration( 228 | using transitionContext: UIViewControllerContextTransitioning? 229 | ) -> TimeInterval { 230 | 0 231 | } 232 | 233 | func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { 234 | 235 | transitionContext.logDebug() 236 | 237 | let fromView = transitionContext.viewController(forKey: .from)!.view! 238 | let toView = transitionContext.viewController(forKey: .to)!.view! 239 | 240 | let restore = resorationHierarchyForDismissing(toView: toView) 241 | 242 | transitionContext.containerView.addSubview(toView) 243 | transitionContext.containerView.addSubview(fromView) 244 | 245 | let animator = UIViewPropertyAnimator(duration: 0.55, dampingRatio: 1) { 246 | fromView.transform = .init(translationX: 0, y: fromView.bounds.height) 247 | } 248 | 249 | animator.addCompletion { _ in 250 | restore(true) 251 | transitionContext.completeTransition(true) 252 | } 253 | 254 | animator.startAnimation() 255 | 256 | } 257 | 258 | } 259 | 260 | final class LeftToRightTransitionController: NSObject, UIViewControllerAnimatedTransitioning { 261 | 262 | func transitionDuration( 263 | using transitionContext: UIViewControllerContextTransitioning? 264 | ) -> TimeInterval { 265 | 0 266 | } 267 | 268 | func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { 269 | 270 | transitionContext.logDebug() 271 | 272 | let fromViewController = transitionContext.viewController(forKey: .from)! 273 | let toViewController = transitionContext.viewController(forKey: .to)! 274 | 275 | let fromNavigationBar = (fromViewController as? NavigatedFluidViewController)?.navigationBar 276 | let toNavigationBar = (toViewController as? NavigatedFluidViewController)?.navigationBar 277 | 278 | let fromView = fromViewController.view! 279 | let toView = toViewController.view! 280 | let restoreHierarchy = resorationHierarchyForDismissing(toView: toView) 281 | 282 | assert(fromView.bounds.width == transitionContext.containerView.bounds.width) 283 | assert(toView.bounds.width == transitionContext.containerView.bounds.width) 284 | 285 | transitionContext.containerView.backgroundColor = .white 286 | transitionContext.containerView.addSubview(toView) 287 | transitionContext.containerView.addSubview(fromView) 288 | 289 | let restoreViewProperties = _makeResorationClosure(views: [ 290 | toView, 291 | fromView, 292 | fromNavigationBar, 293 | toNavigationBar, 294 | ]) 295 | 296 | makeInitialState: do { 297 | toView.transform = .init(translationX: -fromView.bounds.width, y: 0) 298 | toView.alpha = 0 299 | 300 | if let _ = fromNavigationBar, let toNavigationBar = toNavigationBar { 301 | /// NavigationBar transition 302 | toNavigationBar.transform = .init(translationX: fromView.bounds.width, y: 0) 303 | } 304 | } 305 | 306 | let animator = UIViewPropertyAnimator(duration: 0.62, dampingRatio: 1) { 307 | 308 | if let fromNavigationBar = fromNavigationBar, let toNavigationBar = toNavigationBar { 309 | /// NavigationBar transition 310 | fromNavigationBar.transform = .init(translationX: -fromView.bounds.width, y: 0) 311 | toNavigationBar.transform = .identity 312 | } 313 | 314 | fromView.transform = .init(translationX: fromView.bounds.width, y: 0) 315 | fromView.alpha = 0.02 316 | toView.transform = .identity 317 | toView.alpha = 1 318 | } 319 | 320 | animator.addCompletion { position in 321 | switch position { 322 | case .current: 323 | assertionFailure() 324 | // TODO: ??? 325 | break 326 | case .end: 327 | transitionContext.completeTransition(true) 328 | /** 329 | This must be after `completeTransition`. 330 | In some cases, the presentation controller removes `toView` after completion. I don't know why 🤷🏻‍♂️. 331 | */ 332 | restoreHierarchy(true) 333 | restoreViewProperties() 334 | case .start: 335 | preconditionFailure("It never happen") 336 | @unknown default: 337 | fatalError() 338 | } 339 | 340 | } 341 | 342 | animator.startAnimation() 343 | 344 | } 345 | 346 | } 347 | 348 | } 349 | 350 | enum DismissingInteractiveTransitionControllers { 351 | 352 | final class LeftToRightTransitionController: NSObject, UIViewControllerInteractiveTransitioning { 353 | 354 | private weak var currentTransitionContext: UIViewControllerContextTransitioning? 355 | private var currentAnimator: UIViewPropertyAnimator? 356 | 357 | func startInteractiveTransition(_ transitionContext: UIViewControllerContextTransitioning) { 358 | 359 | transitionContext.logDebug() 360 | Log.debug(.interactive, "Start Interactive Transition") 361 | 362 | self.currentTransitionContext = transitionContext 363 | 364 | let fromViewController = transitionContext.viewController(forKey: .from)! 365 | let toViewController = transitionContext.viewController(forKey: .to)! 366 | 367 | let fromNavigationBar = (fromViewController as? NavigatedFluidViewController)?.navigationBar 368 | let toNavigationBar = (toViewController as? NavigatedFluidViewController)?.navigationBar 369 | 370 | let fromView = fromViewController.view! 371 | let toView = toViewController.view! 372 | 373 | let restoreHierarchy = resorationHierarchyForDismissing(toView: toView) 374 | 375 | assert(fromView.bounds.width == transitionContext.containerView.bounds.width) 376 | assert(toView.bounds.width == transitionContext.containerView.bounds.width) 377 | 378 | transitionContext.containerView.backgroundColor = .white 379 | transitionContext.containerView.addSubview(toView) 380 | transitionContext.containerView.addSubview(fromView) 381 | 382 | let restoreViewProperties = _makeResorationClosure(views: [ 383 | toView, 384 | fromView, 385 | fromNavigationBar, 386 | toNavigationBar, 387 | ]) 388 | 389 | makeInitialState: do { 390 | toView.transform = .init(translationX: -fromView.bounds.width, y: 0) 391 | toView.alpha = 0 392 | 393 | if let _ = fromNavigationBar, let toNavigationBar = toNavigationBar { 394 | /// NavigationBar transition 395 | toNavigationBar.transform = .init(translationX: fromView.bounds.width, y: 0) 396 | } 397 | } 398 | 399 | let animator = UIViewPropertyAnimator(duration: 0.62, dampingRatio: 1) { 400 | 401 | if let fromNavigationBar = fromNavigationBar, let toNavigationBar = toNavigationBar { 402 | /// NavigationBar transition 403 | fromNavigationBar.transform = .init(translationX: -fromView.bounds.width, y: 0) 404 | toNavigationBar.transform = .identity 405 | } 406 | 407 | fromView.transform = .init(translationX: fromView.bounds.width, y: 0) 408 | 409 | // 0.02 is workaround value to enable grabbing the view from the gesture. 410 | // Since, alpha 0 ignores touches. 411 | fromView.alpha = 0.02 412 | toView.transform = .identity 413 | toView.alpha = 1 414 | } 415 | 416 | animator.addCompletion { position in 417 | switch position { 418 | case .current: 419 | assertionFailure() 420 | // TODO: ??? 421 | break 422 | case .end: 423 | 424 | transitionContext.updateInteractiveTransition(1) 425 | transitionContext.finishInteractiveTransition() 426 | transitionContext.completeTransition(true) 427 | 428 | /** 429 | This must be after `completeTransition`. 430 | In some cases, the presentation controller removes `toView` after completion. I don't know why 🤷🏻‍♂️. 431 | */ 432 | restoreHierarchy(true) 433 | restoreViewProperties() 434 | 435 | case .start: 436 | 437 | transitionContext.updateInteractiveTransition(0) 438 | transitionContext.cancelInteractiveTransition() 439 | transitionContext.completeTransition(false) 440 | 441 | restoreHierarchy(false) 442 | restoreViewProperties() 443 | @unknown default: 444 | fatalError() 445 | } 446 | 447 | } 448 | 449 | animator.pauseAnimation() 450 | 451 | self.currentAnimator = animator 452 | } 453 | 454 | func finishInteractiveTransition(velocityX: CGFloat) { 455 | Log.debug(.generic, "Finish Interactive Transition") 456 | 457 | guard 458 | let animator = currentAnimator 459 | else { 460 | 461 | return 462 | } 463 | 464 | animator.continueAnimation( 465 | withTimingParameters: UISpringTimingParameters( 466 | dampingRatio: 1, 467 | initialVelocity: .init(dx: velocityX, dy: 0) 468 | ), 469 | durationFactor: 1 470 | ) 471 | } 472 | 473 | func cancelInteractiveTransition() { 474 | Log.debug(.generic, "Cancel Interactive Transition") 475 | 476 | guard 477 | let animator = currentAnimator 478 | else { 479 | 480 | return 481 | } 482 | 483 | animator.isReversed = true 484 | animator.continueAnimation( 485 | withTimingParameters: UISpringTimingParameters( 486 | dampingRatio: 1, 487 | initialVelocity: .zero 488 | ), 489 | durationFactor: 1 490 | ) 491 | } 492 | 493 | func updateProgress(_ progress: CGFloat) { 494 | Log.debug(.generic, "Update progress") 495 | 496 | guard 497 | let context = currentTransitionContext, 498 | let animator = currentAnimator 499 | else { 500 | 501 | return 502 | } 503 | 504 | context.updateInteractiveTransition(progress) 505 | animator.isReversed = false 506 | animator.pauseAnimation() 507 | animator.fractionComplete = progress 508 | } 509 | 510 | } 511 | 512 | } 513 | 514 | 515 | extension UIViewControllerContextTransitioning { 516 | 517 | func logDebug() { 518 | Log.debug(.transition, "fromVC:\(viewController(forKey: .from)), toVC: \(viewController(forKey: .to))") 519 | } 520 | } 521 | -------------------------------------------------------------------------------- /FluidPresentationTests/FluidPresentationTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FluidPresentationTests.swift 3 | // FluidPresentationTests 4 | // 5 | // Created by Muukii on 2021/05/19. 6 | // 7 | 8 | import XCTest 9 | 10 | class FluidPresentationTests: XCTestCase { 11 | 12 | override func setUpWithError() throws { 13 | // Put setup code here. This method is called before the invocation of each test method in the class. 14 | } 15 | 16 | override func tearDownWithError() throws { 17 | // Put teardown code here. This method is called after the invocation of each test method in the class. 18 | } 19 | 20 | func testExample() throws { 21 | // This is an example of a functional test case. 22 | // Use XCTAssert and related functions to verify your tests produce the correct results. 23 | } 24 | 25 | func testPerformanceExample() throws { 26 | // This is an example of a performance test case. 27 | measure { 28 | // Put the code you want to measure the time of here. 29 | } 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /FluidPresentationTests/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 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /FluidPresentationTests/NavigationItemKVOTests.swift: -------------------------------------------------------------------------------- 1 | 2 | import Foundation 3 | import XCTest 4 | 5 | final class NavigationItemKVOTests: XCTestCase { 6 | 7 | func testKVO() { 8 | 9 | let navigationItem = UINavigationItem() 10 | 11 | let titleExpectation = expectation(description: "") 12 | 13 | navigationItem.observe(\.title, options: [.new]) { item, _ in 14 | 15 | } 16 | 17 | 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Eureka, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment the next line to define a global platform for your project 2 | # platform :ios, '9.0' 3 | 4 | target 'Demo' do 5 | # Comment the next line if you don't want to use dynamic frameworks 6 | use_frameworks! 7 | 8 | pod 'Reveal-SDK' 9 | pod 'TinyConstraints' 10 | pod 'TextureSwiftSupport' 11 | # Pods for PresentationViewController 12 | 13 | end 14 | -------------------------------------------------------------------------------- /Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - Reveal-SDK (28) 3 | - Texture/Core (3.0.0) 4 | - TextureSwiftSupport (3.4.0): 5 | - Texture/Core (~> 3) 6 | - TinyConstraints (4.0.1) 7 | 8 | DEPENDENCIES: 9 | - Reveal-SDK 10 | - TextureSwiftSupport 11 | - TinyConstraints 12 | 13 | SPEC REPOS: 14 | trunk: 15 | - Reveal-SDK 16 | - Texture 17 | - TextureSwiftSupport 18 | - TinyConstraints 19 | 20 | SPEC CHECKSUMS: 21 | Reveal-SDK: 1a2a678648fc4d277bad71c86d15530424324288 22 | Texture: 2f109e937850d94d1d07232041c9c7313ccddb81 23 | TextureSwiftSupport: 9f630cd5056e90991bc0d473a3d77f05a891831c 24 | TinyConstraints: 8dd91295e56797648c7bc335dd20e1d91ec4c192 25 | 26 | PODFILE CHECKSUM: d54f4a2840672f40d827351459d7901cc0e4f46c 27 | 28 | COCOAPODS: 1.10.1 29 | -------------------------------------------------------------------------------- /PresentationViewController.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /PresentationViewController.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FluidPresentation - no more handling presented or pushed in view controller 2 | 3 | A view controller that supports the interactive dismissal by edge pan gesture or screen pan gesture from modal presentation. 4 | 5 | | Using Presentation | Using Presentation | 6 | |---|---| 7 | |![CleanShot 2021-04-24 at 00 26 38](https://user-images.githubusercontent.com/1888355/115894190-f2778900-a493-11eb-8795-3dcaddc6f380.gif)|![CleanShot 2021-04-24 at 00 26 14](https://user-images.githubusercontent.com/1888355/115894209-f7d4d380-a493-11eb-89a7-fad3eddf0433.gif)| 8 | 9 | 10 | ## Motivation - Using UINavigationController make the app complicated. 11 | 12 | ### 🤷🏻‍♂️ View Controller would be displayed in push and modal. 13 | 14 | Against the small application, a big application abolutely have a tons of transitions between view controllers. 15 | In addition, the view controller migth be presented in navigation controller or modal presentation. 16 | Which means that should support the both of presentations. 17 | 18 | And also `self.navigationController?.push` is not safe operation. (under the dependencies the context.) 19 | 20 | ### 🙅‍♂️ What about Coordinator pattern? 21 | 22 | Coordinator-pattern's purpose is solving those problems from complex transitions. 23 | However, even this pattern, it's hard to manage pushing and presenting view controller corresponding to the context. 24 | 25 | ### 🙋‍♂️ So, FluidPresentation stop us to use `push`, instead only uses `present`. 26 | 27 | FluidPresentation provides `FluidViewController` as a primitive component. 28 | It does animation like push or presentation in modal presentation transitioning. 29 | 30 | And we can create hierarchy by using **presentation context** in UIKit. 31 | For example, `fullScreen` or `currentContext`. 32 | 33 | ## Usage 34 | 35 | ### Create a view controller by subclassing from `FluidViewController`. 36 | 37 | ```swift 38 | final class YourViewController: FluidViewController { 39 | 40 | } 41 | ``` 42 | 43 | **Presentation style** 44 | 45 | ```swift 46 | let viewController: YourViewController 47 | present(viewController, animated: true, completion: nil) 48 | ``` 49 | 50 | **Navigation style** 51 | 52 | ```swift 53 | let viewController: YourViewController 54 | 55 | viewController.setIdiom(.navigationPush()) 56 | 57 | present(viewController, animated: true, completion: nil) 58 | ``` 59 | 60 | ### Present your own view controller by wraping with `FluidViewController`. 61 | 62 | **Presentation style** 63 | 64 | ```swift 65 | let viewController: YourViewController 66 | 67 | let fluidViewController = FluidViewController(idiom: .presentation, bodyViewController: viewController) 68 | 69 | present(fluidViewController, animated: true, completion: nil) 70 | ``` 71 | 72 | **Navigation style** 73 | 74 | ```swift 75 | let viewController: YourViewController 76 | 77 | let fluidViewController = FluidViewController(idiom: .navigationPush(), bodyViewController: viewController) 78 | 79 | present(fluidViewController, animated: true, completion: nil) 80 | ``` 81 | 82 | ### Using NavigationBar for view controller's navigationItem 83 | 84 | `NavigatedFluidViewController` displays `UINavigationBar` as standalone using the view controller's `navigationItem`. 85 | 86 | > ☝️ NavigationBar transitions as cross-dissolve if previous or next view controller is the type of `NavigatedFluidViewController`. 87 | > That makes the user can feel like using basic navigation system. 88 | 89 | ```swift 90 | let viewController: YourViewController 91 | 92 | let fluidViewController = NavigatedFluidViewController(idiom: .navigationPush(), bodyViewController: viewController) 93 | 94 | present(fluidViewController, animated: true, completion: nil) 95 | ``` 96 | 97 | ## Technical information 98 | 99 | https://developer.apple.com/videos/play/wwdc2013/218/ 100 | https://developer.apple.com/videos/play/tech-talks/10869/ 101 | 102 | ## License 103 | 104 | FluidPresentation is released under the MIT license. 105 | 106 | --------------------------------------------------------------------------------