├── .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 | |||
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 |
--------------------------------------------------------------------------------