├── .github
├── release-drafter.yml
└── workflows
│ ├── ci.yml
│ ├── publish-release-note.yml
│ └── release.yml
├── .gitignore
├── .spi.yml
├── FloatingBottomSheet.podspec
├── LICENSE
├── Package.swift
├── README.md
├── Sample
├── Sample.xcodeproj
│ ├── project.pbxproj
│ ├── project.xcworkspace
│ │ ├── contents.xcworkspacedata
│ │ └── xcshareddata
│ │ │ └── IDEWorkspaceChecks.plist
│ └── xcshareddata
│ │ └── xcschemes
│ │ └── Sample.xcscheme
├── Sample
│ ├── AppDelegate.swift
│ ├── Assets.xcassets
│ │ ├── AccentColor.colorset
│ │ │ └── Contents.json
│ │ ├── AppIcon.appiconset
│ │ │ └── Contents.json
│ │ └── Contents.json
│ ├── Base.lproj
│ │ └── LaunchScreen.storyboard
│ ├── Info.plist
│ ├── Root
│ │ ├── SampleModels.swift
│ │ └── SampleViewController.swift
│ ├── SceneDelegate.swift
│ └── ViewControllers
│ │ └── PlainViewController.swift
├── SampleTests
│ └── SampleTests.swift
└── SampleUITests
│ ├── SampleUITests.swift
│ └── SampleUITestsLaunchTests.swift
├── Sources
└── FloatingBottomSheet
│ ├── Animator
│ ├── FloatingBottomSheetAnimator.swift
│ └── FloatingBottomSheetPresentationAnimator.swift
│ ├── Presentable
│ ├── FloatingBottomSheet.swift
│ ├── FloatingBottomSheetPresentable+Default.swift
│ ├── FloatingBottomSheetPresentable+Internal.swift
│ ├── FloatingBottomSheetPresentable+UIViewController.swift
│ └── FloatingBottomSheetPresentable.swift
│ ├── Presentation
│ ├── FloatingBottomSheetContainerView.swift
│ ├── FloatingBottomSheetHandleMetric.swift
│ ├── FloatingBottomSheetPresentationController.swift
│ └── FloatingBottomSheetPresentationDelegate.swift
│ ├── Presenter
│ ├── FloatingBottomSheetPresenter.swift
│ └── UIViewController+FloatingBottomSheetPresenter.swift
│ └── Utils
│ └── UIColor+Init.swift
├── Tests
└── FloatingBottomSheetTests
│ └── FloatingBottomSheetTests.swift
└── assets
├── floatingbottomsheet.gif
└── logo.png
/.github/release-drafter.yml:
--------------------------------------------------------------------------------
1 | name-template: "🚀 $RESOLVED_VERSION"
2 | tag-template: "$RESOLVED_VERSION"
3 | categories:
4 | - title: "✨ Features"
5 | label: "feature"
6 |
7 | - title: "😎 Update"
8 | label: "update"
9 |
10 | - title: "🐛 Bug Fixes"
11 | label: "bug"
12 |
13 | - title: "🤓 Improvements"
14 | label: "improvement"
15 |
16 | - title: "📚 Documentation"
17 | label: "docs"
18 |
19 | - title: "🧰 Maintenance"
20 | labels:
21 | - "chore"
22 |
23 | change-template: "- $TITLE @$AUTHOR (#$NUMBER)"
24 | change-title-escapes: '\<*_&'
25 | template: |
26 | ## What’s Changed
27 |
28 | $CHANGES
29 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | pull_request:
8 |
9 | concurrency:
10 | group: ${{ github.head_ref || github.run_id }}
11 | cancel-in-progress: true
12 |
13 | jobs:
14 | build_and_test:
15 | runs-on: macos-14
16 |
17 | env:
18 | SCHEME: FloatingBottomSheet
19 | SDK: iphonesimulator
20 | DESTINATION: platform=iOS Simulator,name=iPhone 15,OS=17.2
21 |
22 | steps:
23 | - uses: actions/checkout@v4
24 |
25 | - name: Setup Xcode Version
26 | run: sudo xcode-select --switch /Applications/Xcode_15.2.app
27 | shell: bash
28 |
29 | - name: Run `build`
30 | run: |
31 | set -o pipefail && xcodebuild build \
32 | -scheme "$SCHEME" \
33 | -sdk "$SDK" \
34 | -destination "$DESTINATION" \
35 | -configuration Debug \
36 | CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO \
37 | | xcbeautify --renderer github-actions
38 |
--------------------------------------------------------------------------------
/.github/workflows/publish-release-note.yml:
--------------------------------------------------------------------------------
1 | name: Publish release note
2 | on:
3 | push:
4 | tags:
5 | - "[0-9]+.[0-9]+.[0-9]+"
6 |
7 | permissions:
8 | contents: write
9 | pull-requests: read
10 |
11 | jobs:
12 | publish-release-note:
13 | runs-on: ubuntu-latest
14 |
15 | steps:
16 | - uses: actions/checkout@v4
17 |
18 | - uses: release-drafter/release-drafter@v6
19 | with:
20 | version: ${{ github.ref_name }}
21 |
22 | env:
23 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
24 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Release
2 | on:
3 | release:
4 | types: [published]
5 |
6 | jobs:
7 | release:
8 | runs-on: macos-14
9 |
10 | steps:
11 | - uses: actions/checkout@v4
12 |
13 | - name: extract_version_info
14 | run: echo "version=$(git describe --tags $(git rev-list --tags --max-count=1))" >> $GITHUB_OUTPUT
15 | id: extract_version_name
16 |
17 | - name: Deploy to Cocoapods
18 | run: |
19 | set -eo pipefail
20 | pod lib lint --allow-warnings
21 | pod trunk push --allow-warnings
22 | env:
23 | LIB_VERSION: ${{ steps.extract_version_name.outputs.version }}
24 | COCOAPODS_TRUNK_TOKEN: ${{ secrets.COCOAPODS_TRUNK_TOKEN }}
25 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | /.build
3 | /Packages
4 | xcuserdata/
5 | DerivedData/
6 | .swiftpm/configuration/registries.json
7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
8 | .netrc
9 |
--------------------------------------------------------------------------------
/.spi.yml:
--------------------------------------------------------------------------------
1 | version: 1
2 | builder:
3 | configs:
4 | - platform: ios
5 | documentation_targets:
6 | - FloatingBottomSheet
7 |
--------------------------------------------------------------------------------
/FloatingBottomSheet.podspec:
--------------------------------------------------------------------------------
1 | Pod::Spec.new do |spec|
2 | spec.name = 'FloatingBottomSheet'
3 | spec.version = ENV['LIB_VERSION'] || '0.1.0'
4 | spec.summary = 'An easy way to present a bottom sheet with a floating effect.'
5 | spec.homepage = 'https://github.com/OhKanghoon/FloatingBottomSheet'
6 | spec.license = { type: 'MIT', file: 'LICENSE' }
7 | spec.author = { 'OhKanghoon' => 'ggaa96@naver.com' }
8 | spec.source = { git: 'https://github.com/OhKanghoon/FloatingBottomSheet.git', tag: spec.version.to_s }
9 |
10 | spec.source_files = 'Sources/**/*.{swift,h,m}'
11 | spec.frameworks = 'UIKit'
12 | spec.swift_version = '5.0'
13 |
14 | spec.ios.deployment_target = '11.0'
15 | end
16 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Ray (Kanghoon Oh)
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.
22 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:5.1
2 |
3 | import PackageDescription
4 |
5 | let package = Package(
6 | name: "FloatingBottomSheet",
7 | platforms: [.iOS(.v11)],
8 | products: [
9 | .library(
10 | name: "FloatingBottomSheet",
11 | targets: ["FloatingBottomSheet"]
12 | ),
13 | ],
14 | targets: [
15 | .target(
16 | name: "FloatingBottomSheet"
17 | ),
18 | .testTarget(
19 | name: "FloatingBottomSheetTests",
20 | dependencies: ["FloatingBottomSheet"]
21 | ),
22 | ]
23 | )
24 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # FloatingBottomSheet
2 |
3 |
4 |
5 | [](https://github.com/OhKanghoon/FloatingBottomSheet/actions?query=branch%3Amain+workflow%3ACI)
6 | [](https://github.com/apple/swift-package-manager)
7 | [](https://swiftpackageindex.com/OhKanghoon/FloatingBottomSheet)
8 | [](https://swiftpackageindex.com/OhKanghoon/FloatingBottomSheet)
9 |
10 | A library that displays floating bottom sheet.
11 |
12 | See the [FloatingBottomSheet DocC documentation](https://swiftpackageindex.com/OhKanghoon/FloatingBottomSheet/main/documentation/floatingbottomsheet) hosted on the [Swift Package Index](https://swiftpackageindex.com/).
13 |
14 | ## Compatibility
15 |
16 | FloatingBottomSheet requires **iOS 11+** and is compatible with **Swift 5** projects.
17 |
18 | ## Installation
19 |
20 | ### [Swift Package Manager](https://swift.org/package-manager).
21 |
22 | The preferred way of installing FloatingBottomSheet is via the Swift Package Manager
23 |
24 | 1. In Xcode, open your project and navigate to **File** → **Add Packages**
25 | 2. Paste the repository URL (`https://github.com/OhKanghoon/FloatingBottomSheet`) and click **Next**.
26 | 3. For **Rules**, select **Up to Next Major Version**.
27 | 4. Click **Add Package**.
28 |
29 |
30 | ### [CocoaPods](https://guides.cocoapods.org/using/using-cocoapods.html)
31 |
32 | ```ruby
33 | # Podfile
34 | use_frameworks!
35 |
36 | target 'YOUR_TARGET_NAME' do
37 | pod 'FloatingBottomSheet'
38 | end
39 | ```
40 |
41 | Replace `YOUR_TARGET_NAME` and then, in the `Podfile` directory, type:
42 |
43 | ```bash
44 | $ pod install
45 | ```
46 |
47 | ## Usage
48 |
49 |
50 |
51 | ### Configuration
52 |
53 | To use the FloatingBottomSheet, your ViewController must conform to the `FloatingBottomSheetPresentable` protocol.
54 |
55 | Start by implementing the `bottomSheetScrollable` and `bottomSheetHeight` properties.
56 |
57 | ```swift
58 | final class ViewController: UIViewController, FloatingBottomSheetPresentable {
59 |
60 | var bottomSheetScrollable: UIScrollView? {
61 | // Return a scrollable view
62 | }
63 |
64 | var bottomSheetHeight: CGFloat {
65 | // Set the height of the bottom sheet
66 | }
67 | }
68 | ```
69 |
70 | ### Present bottom sheet
71 |
72 | Simply present the floating bottom sheet using the presentFloatingBottomSheet function like this:
73 |
74 | ```swift
75 | let viewController = ViewController()
76 |
77 | presentFloatingBottomSheet(viewController)
78 | ```
79 |
80 | ### Updates bottom sheet height at runtime
81 |
82 | To update the bottom sheet's height dynamically during runtime, use the following code:
83 |
84 | ```swift
85 | bottomSheetHeight = 400.0
86 | bottomSheetPerformLayout(animated: true)
87 | ```
88 |
89 | You can change the value of bottomSheetHeight to your desired height
90 | and then call `bottomSheetPerformLayout` function to update the bottom sheet's height with optional animation.
91 |
92 | If you don't want animation, set `animated` to false.
93 |
94 | ## License
95 |
96 | FloatingBottomSheet is under MIT license. See the [LICENSE](https://github.com/OhKanghoon/FloatingBottomSheet/blob/main/LICENSE) for more info.
97 |
--------------------------------------------------------------------------------
/Sample/Sample.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 60;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | 711A09302AC971CD0013EB44 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 711A092F2AC971CD0013EB44 /* AppDelegate.swift */; };
11 | 711A09322AC971CD0013EB44 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 711A09312AC971CD0013EB44 /* SceneDelegate.swift */; };
12 | 711A09392AC971CE0013EB44 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 711A09382AC971CE0013EB44 /* Assets.xcassets */; };
13 | 711A093C2AC971CE0013EB44 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 711A093A2AC971CE0013EB44 /* LaunchScreen.storyboard */; };
14 | 711A09472AC971CE0013EB44 /* SampleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 711A09462AC971CE0013EB44 /* SampleTests.swift */; };
15 | 711A09512AC971CE0013EB44 /* SampleUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 711A09502AC971CE0013EB44 /* SampleUITests.swift */; };
16 | 711A09532AC971CE0013EB44 /* SampleUITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 711A09522AC971CE0013EB44 /* SampleUITestsLaunchTests.swift */; };
17 | 711A09612AC971DC0013EB44 /* FloatingBottomSheet in Frameworks */ = {isa = PBXBuildFile; productRef = 711A09602AC971DC0013EB44 /* FloatingBottomSheet */; };
18 | 711A09642AC971FF0013EB44 /* PlainViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 711A09632AC971FF0013EB44 /* PlainViewController.swift */; };
19 | 711A09662AC972510013EB44 /* SampleViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 711A09652AC972510013EB44 /* SampleViewController.swift */; };
20 | 711A096C2AC9BAD50013EB44 /* SampleModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 711A096B2AC9BAD50013EB44 /* SampleModels.swift */; };
21 | /* End PBXBuildFile section */
22 |
23 | /* Begin PBXContainerItemProxy section */
24 | 711A09432AC971CE0013EB44 /* PBXContainerItemProxy */ = {
25 | isa = PBXContainerItemProxy;
26 | containerPortal = 711A09242AC971CD0013EB44 /* Project object */;
27 | proxyType = 1;
28 | remoteGlobalIDString = 711A092B2AC971CD0013EB44;
29 | remoteInfo = Sample;
30 | };
31 | 711A094D2AC971CE0013EB44 /* PBXContainerItemProxy */ = {
32 | isa = PBXContainerItemProxy;
33 | containerPortal = 711A09242AC971CD0013EB44 /* Project object */;
34 | proxyType = 1;
35 | remoteGlobalIDString = 711A092B2AC971CD0013EB44;
36 | remoteInfo = Sample;
37 | };
38 | /* End PBXContainerItemProxy section */
39 |
40 | /* Begin PBXFileReference section */
41 | 711A092C2AC971CD0013EB44 /* Sample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Sample.app; sourceTree = BUILT_PRODUCTS_DIR; };
42 | 711A092F2AC971CD0013EB44 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
43 | 711A09312AC971CD0013EB44 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; };
44 | 711A09382AC971CE0013EB44 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
45 | 711A093B2AC971CE0013EB44 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; };
46 | 711A093D2AC971CE0013EB44 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
47 | 711A09422AC971CE0013EB44 /* SampleTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SampleTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
48 | 711A09462AC971CE0013EB44 /* SampleTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SampleTests.swift; sourceTree = ""; };
49 | 711A094C2AC971CE0013EB44 /* SampleUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SampleUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
50 | 711A09502AC971CE0013EB44 /* SampleUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SampleUITests.swift; sourceTree = ""; };
51 | 711A09522AC971CE0013EB44 /* SampleUITestsLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SampleUITestsLaunchTests.swift; sourceTree = ""; };
52 | 711A09632AC971FF0013EB44 /* PlainViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlainViewController.swift; sourceTree = ""; };
53 | 711A09652AC972510013EB44 /* SampleViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SampleViewController.swift; sourceTree = ""; };
54 | 711A096B2AC9BAD50013EB44 /* SampleModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SampleModels.swift; sourceTree = ""; };
55 | /* End PBXFileReference section */
56 |
57 | /* Begin PBXFrameworksBuildPhase section */
58 | 711A09292AC971CD0013EB44 /* Frameworks */ = {
59 | isa = PBXFrameworksBuildPhase;
60 | buildActionMask = 2147483647;
61 | files = (
62 | 711A09612AC971DC0013EB44 /* FloatingBottomSheet in Frameworks */,
63 | );
64 | runOnlyForDeploymentPostprocessing = 0;
65 | };
66 | 711A093F2AC971CE0013EB44 /* Frameworks */ = {
67 | isa = PBXFrameworksBuildPhase;
68 | buildActionMask = 2147483647;
69 | files = (
70 | );
71 | runOnlyForDeploymentPostprocessing = 0;
72 | };
73 | 711A09492AC971CE0013EB44 /* Frameworks */ = {
74 | isa = PBXFrameworksBuildPhase;
75 | buildActionMask = 2147483647;
76 | files = (
77 | );
78 | runOnlyForDeploymentPostprocessing = 0;
79 | };
80 | /* End PBXFrameworksBuildPhase section */
81 |
82 | /* Begin PBXGroup section */
83 | 711A09232AC971CD0013EB44 = {
84 | isa = PBXGroup;
85 | children = (
86 | 711A092E2AC971CD0013EB44 /* Sample */,
87 | 711A09452AC971CE0013EB44 /* SampleTests */,
88 | 711A094F2AC971CE0013EB44 /* SampleUITests */,
89 | 711A092D2AC971CD0013EB44 /* Products */,
90 | );
91 | sourceTree = "";
92 | };
93 | 711A092D2AC971CD0013EB44 /* Products */ = {
94 | isa = PBXGroup;
95 | children = (
96 | 711A092C2AC971CD0013EB44 /* Sample.app */,
97 | 711A09422AC971CE0013EB44 /* SampleTests.xctest */,
98 | 711A094C2AC971CE0013EB44 /* SampleUITests.xctest */,
99 | );
100 | name = Products;
101 | sourceTree = "";
102 | };
103 | 711A092E2AC971CD0013EB44 /* Sample */ = {
104 | isa = PBXGroup;
105 | children = (
106 | 711A096A2AC9BAC70013EB44 /* Root */,
107 | 711A09622AC971F70013EB44 /* ViewControllers */,
108 | 711A092F2AC971CD0013EB44 /* AppDelegate.swift */,
109 | 711A09312AC971CD0013EB44 /* SceneDelegate.swift */,
110 | 711A09382AC971CE0013EB44 /* Assets.xcassets */,
111 | 711A093A2AC971CE0013EB44 /* LaunchScreen.storyboard */,
112 | 711A093D2AC971CE0013EB44 /* Info.plist */,
113 | );
114 | path = Sample;
115 | sourceTree = "";
116 | };
117 | 711A09452AC971CE0013EB44 /* SampleTests */ = {
118 | isa = PBXGroup;
119 | children = (
120 | 711A09462AC971CE0013EB44 /* SampleTests.swift */,
121 | );
122 | path = SampleTests;
123 | sourceTree = "";
124 | };
125 | 711A094F2AC971CE0013EB44 /* SampleUITests */ = {
126 | isa = PBXGroup;
127 | children = (
128 | 711A09502AC971CE0013EB44 /* SampleUITests.swift */,
129 | 711A09522AC971CE0013EB44 /* SampleUITestsLaunchTests.swift */,
130 | );
131 | path = SampleUITests;
132 | sourceTree = "";
133 | };
134 | 711A09622AC971F70013EB44 /* ViewControllers */ = {
135 | isa = PBXGroup;
136 | children = (
137 | 711A09632AC971FF0013EB44 /* PlainViewController.swift */,
138 | );
139 | path = ViewControllers;
140 | sourceTree = "";
141 | };
142 | 711A096A2AC9BAC70013EB44 /* Root */ = {
143 | isa = PBXGroup;
144 | children = (
145 | 711A09652AC972510013EB44 /* SampleViewController.swift */,
146 | 711A096B2AC9BAD50013EB44 /* SampleModels.swift */,
147 | );
148 | path = Root;
149 | sourceTree = "";
150 | };
151 | /* End PBXGroup section */
152 |
153 | /* Begin PBXNativeTarget section */
154 | 711A092B2AC971CD0013EB44 /* Sample */ = {
155 | isa = PBXNativeTarget;
156 | buildConfigurationList = 711A09562AC971CE0013EB44 /* Build configuration list for PBXNativeTarget "Sample" */;
157 | buildPhases = (
158 | 711A09282AC971CD0013EB44 /* Sources */,
159 | 711A09292AC971CD0013EB44 /* Frameworks */,
160 | 711A092A2AC971CD0013EB44 /* Resources */,
161 | );
162 | buildRules = (
163 | );
164 | dependencies = (
165 | );
166 | name = Sample;
167 | packageProductDependencies = (
168 | 711A09602AC971DC0013EB44 /* FloatingBottomSheet */,
169 | );
170 | productName = Sample;
171 | productReference = 711A092C2AC971CD0013EB44 /* Sample.app */;
172 | productType = "com.apple.product-type.application";
173 | };
174 | 711A09412AC971CE0013EB44 /* SampleTests */ = {
175 | isa = PBXNativeTarget;
176 | buildConfigurationList = 711A09592AC971CE0013EB44 /* Build configuration list for PBXNativeTarget "SampleTests" */;
177 | buildPhases = (
178 | 711A093E2AC971CE0013EB44 /* Sources */,
179 | 711A093F2AC971CE0013EB44 /* Frameworks */,
180 | 711A09402AC971CE0013EB44 /* Resources */,
181 | );
182 | buildRules = (
183 | );
184 | dependencies = (
185 | 711A09442AC971CE0013EB44 /* PBXTargetDependency */,
186 | );
187 | name = SampleTests;
188 | productName = SampleTests;
189 | productReference = 711A09422AC971CE0013EB44 /* SampleTests.xctest */;
190 | productType = "com.apple.product-type.bundle.unit-test";
191 | };
192 | 711A094B2AC971CE0013EB44 /* SampleUITests */ = {
193 | isa = PBXNativeTarget;
194 | buildConfigurationList = 711A095C2AC971CE0013EB44 /* Build configuration list for PBXNativeTarget "SampleUITests" */;
195 | buildPhases = (
196 | 711A09482AC971CE0013EB44 /* Sources */,
197 | 711A09492AC971CE0013EB44 /* Frameworks */,
198 | 711A094A2AC971CE0013EB44 /* Resources */,
199 | );
200 | buildRules = (
201 | );
202 | dependencies = (
203 | 711A094E2AC971CE0013EB44 /* PBXTargetDependency */,
204 | );
205 | name = SampleUITests;
206 | productName = SampleUITests;
207 | productReference = 711A094C2AC971CE0013EB44 /* SampleUITests.xctest */;
208 | productType = "com.apple.product-type.bundle.ui-testing";
209 | };
210 | /* End PBXNativeTarget section */
211 |
212 | /* Begin PBXProject section */
213 | 711A09242AC971CD0013EB44 /* Project object */ = {
214 | isa = PBXProject;
215 | attributes = {
216 | BuildIndependentTargetsInParallel = 1;
217 | LastSwiftUpdateCheck = 1500;
218 | LastUpgradeCheck = 1500;
219 | TargetAttributes = {
220 | 711A092B2AC971CD0013EB44 = {
221 | CreatedOnToolsVersion = 15.0;
222 | };
223 | 711A09412AC971CE0013EB44 = {
224 | CreatedOnToolsVersion = 15.0;
225 | TestTargetID = 711A092B2AC971CD0013EB44;
226 | };
227 | 711A094B2AC971CE0013EB44 = {
228 | CreatedOnToolsVersion = 15.0;
229 | TestTargetID = 711A092B2AC971CD0013EB44;
230 | };
231 | };
232 | };
233 | buildConfigurationList = 711A09272AC971CD0013EB44 /* Build configuration list for PBXProject "Sample" */;
234 | compatibilityVersion = "Xcode 14.0";
235 | developmentRegion = en;
236 | hasScannedForEncodings = 0;
237 | knownRegions = (
238 | en,
239 | Base,
240 | );
241 | mainGroup = 711A09232AC971CD0013EB44;
242 | packageReferences = (
243 | 711A095F2AC971DC0013EB44 /* XCLocalSwiftPackageReference ".." */,
244 | );
245 | productRefGroup = 711A092D2AC971CD0013EB44 /* Products */;
246 | projectDirPath = "";
247 | projectRoot = "";
248 | targets = (
249 | 711A092B2AC971CD0013EB44 /* Sample */,
250 | 711A09412AC971CE0013EB44 /* SampleTests */,
251 | 711A094B2AC971CE0013EB44 /* SampleUITests */,
252 | );
253 | };
254 | /* End PBXProject section */
255 |
256 | /* Begin PBXResourcesBuildPhase section */
257 | 711A092A2AC971CD0013EB44 /* Resources */ = {
258 | isa = PBXResourcesBuildPhase;
259 | buildActionMask = 2147483647;
260 | files = (
261 | 711A093C2AC971CE0013EB44 /* LaunchScreen.storyboard in Resources */,
262 | 711A09392AC971CE0013EB44 /* Assets.xcassets in Resources */,
263 | );
264 | runOnlyForDeploymentPostprocessing = 0;
265 | };
266 | 711A09402AC971CE0013EB44 /* Resources */ = {
267 | isa = PBXResourcesBuildPhase;
268 | buildActionMask = 2147483647;
269 | files = (
270 | );
271 | runOnlyForDeploymentPostprocessing = 0;
272 | };
273 | 711A094A2AC971CE0013EB44 /* Resources */ = {
274 | isa = PBXResourcesBuildPhase;
275 | buildActionMask = 2147483647;
276 | files = (
277 | );
278 | runOnlyForDeploymentPostprocessing = 0;
279 | };
280 | /* End PBXResourcesBuildPhase section */
281 |
282 | /* Begin PBXSourcesBuildPhase section */
283 | 711A09282AC971CD0013EB44 /* Sources */ = {
284 | isa = PBXSourcesBuildPhase;
285 | buildActionMask = 2147483647;
286 | files = (
287 | 711A09662AC972510013EB44 /* SampleViewController.swift in Sources */,
288 | 711A09642AC971FF0013EB44 /* PlainViewController.swift in Sources */,
289 | 711A09302AC971CD0013EB44 /* AppDelegate.swift in Sources */,
290 | 711A096C2AC9BAD50013EB44 /* SampleModels.swift in Sources */,
291 | 711A09322AC971CD0013EB44 /* SceneDelegate.swift in Sources */,
292 | );
293 | runOnlyForDeploymentPostprocessing = 0;
294 | };
295 | 711A093E2AC971CE0013EB44 /* Sources */ = {
296 | isa = PBXSourcesBuildPhase;
297 | buildActionMask = 2147483647;
298 | files = (
299 | 711A09472AC971CE0013EB44 /* SampleTests.swift in Sources */,
300 | );
301 | runOnlyForDeploymentPostprocessing = 0;
302 | };
303 | 711A09482AC971CE0013EB44 /* Sources */ = {
304 | isa = PBXSourcesBuildPhase;
305 | buildActionMask = 2147483647;
306 | files = (
307 | 711A09532AC971CE0013EB44 /* SampleUITestsLaunchTests.swift in Sources */,
308 | 711A09512AC971CE0013EB44 /* SampleUITests.swift in Sources */,
309 | );
310 | runOnlyForDeploymentPostprocessing = 0;
311 | };
312 | /* End PBXSourcesBuildPhase section */
313 |
314 | /* Begin PBXTargetDependency section */
315 | 711A09442AC971CE0013EB44 /* PBXTargetDependency */ = {
316 | isa = PBXTargetDependency;
317 | target = 711A092B2AC971CD0013EB44 /* Sample */;
318 | targetProxy = 711A09432AC971CE0013EB44 /* PBXContainerItemProxy */;
319 | };
320 | 711A094E2AC971CE0013EB44 /* PBXTargetDependency */ = {
321 | isa = PBXTargetDependency;
322 | target = 711A092B2AC971CD0013EB44 /* Sample */;
323 | targetProxy = 711A094D2AC971CE0013EB44 /* PBXContainerItemProxy */;
324 | };
325 | /* End PBXTargetDependency section */
326 |
327 | /* Begin PBXVariantGroup section */
328 | 711A093A2AC971CE0013EB44 /* LaunchScreen.storyboard */ = {
329 | isa = PBXVariantGroup;
330 | children = (
331 | 711A093B2AC971CE0013EB44 /* Base */,
332 | );
333 | name = LaunchScreen.storyboard;
334 | sourceTree = "";
335 | };
336 | /* End PBXVariantGroup section */
337 |
338 | /* Begin XCBuildConfiguration section */
339 | 711A09542AC971CE0013EB44 /* Debug */ = {
340 | isa = XCBuildConfiguration;
341 | buildSettings = {
342 | ALWAYS_SEARCH_USER_PATHS = NO;
343 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
344 | CLANG_ANALYZER_NONNULL = YES;
345 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
346 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
347 | CLANG_ENABLE_MODULES = YES;
348 | CLANG_ENABLE_OBJC_ARC = YES;
349 | CLANG_ENABLE_OBJC_WEAK = YES;
350 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
351 | CLANG_WARN_BOOL_CONVERSION = YES;
352 | CLANG_WARN_COMMA = YES;
353 | CLANG_WARN_CONSTANT_CONVERSION = YES;
354 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
355 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
356 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
357 | CLANG_WARN_EMPTY_BODY = YES;
358 | CLANG_WARN_ENUM_CONVERSION = YES;
359 | CLANG_WARN_INFINITE_RECURSION = YES;
360 | CLANG_WARN_INT_CONVERSION = YES;
361 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
362 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
363 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
364 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
365 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
366 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
367 | CLANG_WARN_STRICT_PROTOTYPES = YES;
368 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
369 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
370 | CLANG_WARN_UNREACHABLE_CODE = YES;
371 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
372 | COPY_PHASE_STRIP = NO;
373 | DEBUG_INFORMATION_FORMAT = dwarf;
374 | ENABLE_STRICT_OBJC_MSGSEND = YES;
375 | ENABLE_TESTABILITY = YES;
376 | ENABLE_USER_SCRIPT_SANDBOXING = YES;
377 | GCC_C_LANGUAGE_STANDARD = gnu17;
378 | GCC_DYNAMIC_NO_PIC = NO;
379 | GCC_NO_COMMON_BLOCKS = YES;
380 | GCC_OPTIMIZATION_LEVEL = 0;
381 | GCC_PREPROCESSOR_DEFINITIONS = (
382 | "DEBUG=1",
383 | "$(inherited)",
384 | );
385 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
386 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
387 | GCC_WARN_UNDECLARED_SELECTOR = YES;
388 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
389 | GCC_WARN_UNUSED_FUNCTION = YES;
390 | GCC_WARN_UNUSED_VARIABLE = YES;
391 | IPHONEOS_DEPLOYMENT_TARGET = 17.0;
392 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
393 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
394 | MTL_FAST_MATH = YES;
395 | ONLY_ACTIVE_ARCH = YES;
396 | SDKROOT = iphoneos;
397 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
398 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
399 | };
400 | name = Debug;
401 | };
402 | 711A09552AC971CE0013EB44 /* Release */ = {
403 | isa = XCBuildConfiguration;
404 | buildSettings = {
405 | ALWAYS_SEARCH_USER_PATHS = NO;
406 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
407 | CLANG_ANALYZER_NONNULL = YES;
408 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
409 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
410 | CLANG_ENABLE_MODULES = YES;
411 | CLANG_ENABLE_OBJC_ARC = YES;
412 | CLANG_ENABLE_OBJC_WEAK = YES;
413 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
414 | CLANG_WARN_BOOL_CONVERSION = YES;
415 | CLANG_WARN_COMMA = YES;
416 | CLANG_WARN_CONSTANT_CONVERSION = YES;
417 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
418 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
419 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
420 | CLANG_WARN_EMPTY_BODY = YES;
421 | CLANG_WARN_ENUM_CONVERSION = YES;
422 | CLANG_WARN_INFINITE_RECURSION = YES;
423 | CLANG_WARN_INT_CONVERSION = YES;
424 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
425 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
426 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
427 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
428 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
429 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
430 | CLANG_WARN_STRICT_PROTOTYPES = YES;
431 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
432 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
433 | CLANG_WARN_UNREACHABLE_CODE = YES;
434 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
435 | COPY_PHASE_STRIP = NO;
436 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
437 | ENABLE_NS_ASSERTIONS = NO;
438 | ENABLE_STRICT_OBJC_MSGSEND = YES;
439 | ENABLE_USER_SCRIPT_SANDBOXING = YES;
440 | GCC_C_LANGUAGE_STANDARD = gnu17;
441 | GCC_NO_COMMON_BLOCKS = YES;
442 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
443 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
444 | GCC_WARN_UNDECLARED_SELECTOR = YES;
445 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
446 | GCC_WARN_UNUSED_FUNCTION = YES;
447 | GCC_WARN_UNUSED_VARIABLE = YES;
448 | IPHONEOS_DEPLOYMENT_TARGET = 17.0;
449 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
450 | MTL_ENABLE_DEBUG_INFO = NO;
451 | MTL_FAST_MATH = YES;
452 | SDKROOT = iphoneos;
453 | SWIFT_COMPILATION_MODE = wholemodule;
454 | VALIDATE_PRODUCT = YES;
455 | };
456 | name = Release;
457 | };
458 | 711A09572AC971CE0013EB44 /* Debug */ = {
459 | isa = XCBuildConfiguration;
460 | buildSettings = {
461 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
462 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
463 | CODE_SIGN_STYLE = Automatic;
464 | CURRENT_PROJECT_VERSION = 1;
465 | GENERATE_INFOPLIST_FILE = YES;
466 | INFOPLIST_FILE = Sample/Info.plist;
467 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
468 | INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen.storyboard;
469 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
470 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
471 | LD_RUNPATH_SEARCH_PATHS = (
472 | "$(inherited)",
473 | "@executable_path/Frameworks",
474 | );
475 | MARKETING_VERSION = 1.0;
476 | PRODUCT_BUNDLE_IDENTIFIER = com.kanghoon.Sample;
477 | PRODUCT_NAME = "$(TARGET_NAME)";
478 | SWIFT_EMIT_LOC_STRINGS = YES;
479 | SWIFT_VERSION = 5.0;
480 | TARGETED_DEVICE_FAMILY = "1,2";
481 | };
482 | name = Debug;
483 | };
484 | 711A09582AC971CE0013EB44 /* Release */ = {
485 | isa = XCBuildConfiguration;
486 | buildSettings = {
487 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
488 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
489 | CODE_SIGN_STYLE = Automatic;
490 | CURRENT_PROJECT_VERSION = 1;
491 | GENERATE_INFOPLIST_FILE = YES;
492 | INFOPLIST_FILE = Sample/Info.plist;
493 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
494 | INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen.storyboard;
495 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
496 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
497 | LD_RUNPATH_SEARCH_PATHS = (
498 | "$(inherited)",
499 | "@executable_path/Frameworks",
500 | );
501 | MARKETING_VERSION = 1.0;
502 | PRODUCT_BUNDLE_IDENTIFIER = com.kanghoon.Sample;
503 | PRODUCT_NAME = "$(TARGET_NAME)";
504 | SWIFT_EMIT_LOC_STRINGS = YES;
505 | SWIFT_VERSION = 5.0;
506 | TARGETED_DEVICE_FAMILY = "1,2";
507 | };
508 | name = Release;
509 | };
510 | 711A095A2AC971CE0013EB44 /* Debug */ = {
511 | isa = XCBuildConfiguration;
512 | buildSettings = {
513 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
514 | BUNDLE_LOADER = "$(TEST_HOST)";
515 | CODE_SIGN_STYLE = Automatic;
516 | CURRENT_PROJECT_VERSION = 1;
517 | GENERATE_INFOPLIST_FILE = YES;
518 | IPHONEOS_DEPLOYMENT_TARGET = 17.0;
519 | MARKETING_VERSION = 1.0;
520 | PRODUCT_BUNDLE_IDENTIFIER = com.kanghoon.SampleTests;
521 | PRODUCT_NAME = "$(TARGET_NAME)";
522 | SWIFT_EMIT_LOC_STRINGS = NO;
523 | SWIFT_VERSION = 5.0;
524 | TARGETED_DEVICE_FAMILY = "1,2";
525 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Sample.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Sample";
526 | };
527 | name = Debug;
528 | };
529 | 711A095B2AC971CE0013EB44 /* Release */ = {
530 | isa = XCBuildConfiguration;
531 | buildSettings = {
532 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
533 | BUNDLE_LOADER = "$(TEST_HOST)";
534 | CODE_SIGN_STYLE = Automatic;
535 | CURRENT_PROJECT_VERSION = 1;
536 | GENERATE_INFOPLIST_FILE = YES;
537 | IPHONEOS_DEPLOYMENT_TARGET = 17.0;
538 | MARKETING_VERSION = 1.0;
539 | PRODUCT_BUNDLE_IDENTIFIER = com.kanghoon.SampleTests;
540 | PRODUCT_NAME = "$(TARGET_NAME)";
541 | SWIFT_EMIT_LOC_STRINGS = NO;
542 | SWIFT_VERSION = 5.0;
543 | TARGETED_DEVICE_FAMILY = "1,2";
544 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Sample.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Sample";
545 | };
546 | name = Release;
547 | };
548 | 711A095D2AC971CE0013EB44 /* Debug */ = {
549 | isa = XCBuildConfiguration;
550 | buildSettings = {
551 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
552 | CODE_SIGN_STYLE = Automatic;
553 | CURRENT_PROJECT_VERSION = 1;
554 | GENERATE_INFOPLIST_FILE = YES;
555 | MARKETING_VERSION = 1.0;
556 | PRODUCT_BUNDLE_IDENTIFIER = com.kanghoon.SampleUITests;
557 | PRODUCT_NAME = "$(TARGET_NAME)";
558 | SWIFT_EMIT_LOC_STRINGS = NO;
559 | SWIFT_VERSION = 5.0;
560 | TARGETED_DEVICE_FAMILY = "1,2";
561 | TEST_TARGET_NAME = Sample;
562 | };
563 | name = Debug;
564 | };
565 | 711A095E2AC971CE0013EB44 /* Release */ = {
566 | isa = XCBuildConfiguration;
567 | buildSettings = {
568 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
569 | CODE_SIGN_STYLE = Automatic;
570 | CURRENT_PROJECT_VERSION = 1;
571 | GENERATE_INFOPLIST_FILE = YES;
572 | MARKETING_VERSION = 1.0;
573 | PRODUCT_BUNDLE_IDENTIFIER = com.kanghoon.SampleUITests;
574 | PRODUCT_NAME = "$(TARGET_NAME)";
575 | SWIFT_EMIT_LOC_STRINGS = NO;
576 | SWIFT_VERSION = 5.0;
577 | TARGETED_DEVICE_FAMILY = "1,2";
578 | TEST_TARGET_NAME = Sample;
579 | };
580 | name = Release;
581 | };
582 | /* End XCBuildConfiguration section */
583 |
584 | /* Begin XCConfigurationList section */
585 | 711A09272AC971CD0013EB44 /* Build configuration list for PBXProject "Sample" */ = {
586 | isa = XCConfigurationList;
587 | buildConfigurations = (
588 | 711A09542AC971CE0013EB44 /* Debug */,
589 | 711A09552AC971CE0013EB44 /* Release */,
590 | );
591 | defaultConfigurationIsVisible = 0;
592 | defaultConfigurationName = Release;
593 | };
594 | 711A09562AC971CE0013EB44 /* Build configuration list for PBXNativeTarget "Sample" */ = {
595 | isa = XCConfigurationList;
596 | buildConfigurations = (
597 | 711A09572AC971CE0013EB44 /* Debug */,
598 | 711A09582AC971CE0013EB44 /* Release */,
599 | );
600 | defaultConfigurationIsVisible = 0;
601 | defaultConfigurationName = Release;
602 | };
603 | 711A09592AC971CE0013EB44 /* Build configuration list for PBXNativeTarget "SampleTests" */ = {
604 | isa = XCConfigurationList;
605 | buildConfigurations = (
606 | 711A095A2AC971CE0013EB44 /* Debug */,
607 | 711A095B2AC971CE0013EB44 /* Release */,
608 | );
609 | defaultConfigurationIsVisible = 0;
610 | defaultConfigurationName = Release;
611 | };
612 | 711A095C2AC971CE0013EB44 /* Build configuration list for PBXNativeTarget "SampleUITests" */ = {
613 | isa = XCConfigurationList;
614 | buildConfigurations = (
615 | 711A095D2AC971CE0013EB44 /* Debug */,
616 | 711A095E2AC971CE0013EB44 /* Release */,
617 | );
618 | defaultConfigurationIsVisible = 0;
619 | defaultConfigurationName = Release;
620 | };
621 | /* End XCConfigurationList section */
622 |
623 | /* Begin XCLocalSwiftPackageReference section */
624 | 711A095F2AC971DC0013EB44 /* XCLocalSwiftPackageReference ".." */ = {
625 | isa = XCLocalSwiftPackageReference;
626 | relativePath = ..;
627 | };
628 | /* End XCLocalSwiftPackageReference section */
629 |
630 | /* Begin XCSwiftPackageProductDependency section */
631 | 711A09602AC971DC0013EB44 /* FloatingBottomSheet */ = {
632 | isa = XCSwiftPackageProductDependency;
633 | productName = FloatingBottomSheet;
634 | };
635 | /* End XCSwiftPackageProductDependency section */
636 | };
637 | rootObject = 711A09242AC971CD0013EB44 /* Project object */;
638 | }
639 |
--------------------------------------------------------------------------------
/Sample/Sample.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Sample/Sample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Sample/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
31 |
32 |
35 |
41 |
42 |
43 |
46 |
52 |
53 |
54 |
55 |
56 |
66 |
68 |
74 |
75 |
76 |
77 |
83 |
85 |
91 |
92 |
93 |
94 |
96 |
97 |
100 |
101 |
102 |
--------------------------------------------------------------------------------
/Sample/Sample/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.swift
3 | // Sample
4 | //
5 | // Created by Kanghoon Oh on 2023/10/01.
6 | //
7 |
8 | import UIKit
9 |
10 | @main
11 | final class AppDelegate: UIResponder, UIApplicationDelegate {
12 |
13 | func application(
14 | _ application: UIApplication,
15 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
16 | ) -> Bool {
17 | // Override point for customization after application launch.
18 | true
19 | }
20 |
21 | // MARK: UISceneSession Lifecycle
22 |
23 | func application(
24 | _ application: UIApplication,
25 | configurationForConnecting connectingSceneSession: UISceneSession,
26 | options: UIScene.ConnectionOptions
27 | ) -> UISceneConfiguration {
28 | // Called when a new scene session is being created.
29 | // Use this method to select a configuration to create the new scene with.
30 | UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/Sample/Sample/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 |
--------------------------------------------------------------------------------
/Sample/Sample/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "platform" : "ios",
6 | "size" : "1024x1024"
7 | }
8 | ],
9 | "info" : {
10 | "author" : "xcode",
11 | "version" : 1
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/Sample/Sample/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Sample/Sample/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 |
--------------------------------------------------------------------------------
/Sample/Sample/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | UIApplicationSceneManifest
6 |
7 | UIApplicationSupportsMultipleScenes
8 |
9 | UISceneConfigurations
10 |
11 | UIWindowSceneSessionRoleApplication
12 |
13 |
14 | UISceneConfigurationName
15 | Default Configuration
16 | UISceneDelegateClassName
17 | $(PRODUCT_MODULE_NAME).SceneDelegate
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/Sample/Sample/Root/SampleModels.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SampleModels.swift
3 | // Sample
4 | //
5 | // Created by Kanghoon Oh on 2023/10/01.
6 | //
7 |
8 | import Foundation
9 |
10 | import FloatingBottomSheet
11 |
12 | protocol SampleViewModel {
13 | var title: String { get }
14 | var rowViewController: FloatingBottomSheet { get }
15 | }
16 |
17 | enum SampleRow: Int, CaseIterable {
18 | case plain
19 |
20 | var viewModel: SampleViewModel {
21 | switch self {
22 | case .plain: return PlainViewModel()
23 | }
24 | }
25 | }
26 |
27 | extension SampleRow {
28 |
29 | struct PlainViewModel: SampleViewModel {
30 | let title = "Plain"
31 | let rowViewController: FloatingBottomSheet = PlainViewController()
32 | }
33 | }
34 |
35 |
--------------------------------------------------------------------------------
/Sample/Sample/Root/SampleViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SampleViewController.swift
3 | // Sample
4 | //
5 | // Created by Kanghoon Oh on 2023/10/01.
6 | //
7 |
8 | import UIKit
9 |
10 | import FloatingBottomSheet
11 |
12 | final class SampleViewController: UITableViewController {
13 |
14 | override func viewDidLoad() {
15 | super.viewDidLoad()
16 | setupView()
17 | }
18 |
19 | private func setupView() {
20 | title = "FloatingBottomSheet"
21 |
22 | tableView.register(UITableViewCell.self, forCellReuseIdentifier: String(describing: UITableViewCell.self))
23 | tableView.tableFooterView = UIView()
24 | tableView.separatorInset = .zero
25 | }
26 |
27 | override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
28 | 60.0
29 | }
30 |
31 | override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
32 | SampleRow.allCases.count
33 | }
34 |
35 | override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
36 | let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: UITableViewCell.self), for: indexPath)
37 |
38 | guard let rowType = SampleRow(rawValue: indexPath.row) else {
39 | return cell
40 | }
41 | cell.textLabel?.textAlignment = .center
42 | cell.textLabel?.text = rowType.viewModel.title
43 | cell.textLabel?.font = UIFont.preferredFont(forTextStyle: .body)
44 | return cell
45 | }
46 |
47 | override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
48 | tableView.deselectRow(at: indexPath, animated: true)
49 |
50 | guard let rowType = SampleRow(rawValue: indexPath.row) else {
51 | return
52 | }
53 | dismiss(animated: true, completion: nil)
54 | presentFloatingBottomSheet(rowType.viewModel.rowViewController, completion: nil)
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/Sample/Sample/SceneDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SceneDelegate.swift
3 | // Sample
4 | //
5 | // Created by Kanghoon Oh on 2023/10/01.
6 | //
7 |
8 | import UIKit
9 |
10 | final class SceneDelegate: UIResponder, UIWindowSceneDelegate {
11 |
12 | var window: UIWindow?
13 |
14 | func scene(
15 | _ scene: UIScene,
16 | willConnectTo session: UISceneSession,
17 | options connectionOptions: UIScene.ConnectionOptions
18 | ) {
19 | guard let windowScene = (scene as? UIWindowScene) else { return }
20 |
21 | window = UIWindow(windowScene: windowScene)
22 | window?.rootViewController = UINavigationController(rootViewController: SampleViewController())
23 | window?.makeKeyAndVisible()
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/Sample/Sample/ViewControllers/PlainViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PlainViewController.swift
3 | // Sample
4 | //
5 | // Created by Kanghoon Oh on 2023/10/01.
6 | //
7 |
8 | import UIKit
9 |
10 | import FloatingBottomSheet
11 |
12 | final class PlainViewController: UIViewController, FloatingBottomSheetPresentable {
13 |
14 | private let scrollView = UIScrollView()
15 |
16 | private let stackView: UIStackView = {
17 | let stackView = UIStackView()
18 | stackView.axis = .vertical
19 | stackView.spacing = 10.0
20 | stackView.distribution = .fill
21 | return stackView
22 | }()
23 |
24 | var bottomSheetHeight: CGFloat = 200
25 |
26 | var bottomSheetScrollable: UIScrollView? {
27 | scrollView
28 | }
29 |
30 | override func viewDidLoad() {
31 | super.viewDidLoad()
32 | view.backgroundColor = .systemBackground
33 |
34 | addSubviews()
35 | configureConstraint()
36 | }
37 |
38 | private func addSubviews() {
39 | view.addSubview(scrollView)
40 | scrollView.addSubview(stackView)
41 |
42 | let increaseButton = makeButton(title: "+ Increase Height", color: .systemBlue) { [weak self] in
43 | self?.bottomSheetHeight += 100
44 | self?.bottomSheetPerformLayout(animated: true)
45 | }
46 | let decreaseButton = makeButton(title: "- Decrease Height", color: .systemRed) { [weak self] in
47 | self?.bottomSheetHeight -= 100
48 | self?.bottomSheetPerformLayout(animated: true)
49 | }
50 | stackView.addArrangedSubview(increaseButton)
51 | stackView.addArrangedSubview(decreaseButton)
52 |
53 | (0...3).forEach { _ in
54 | stackView.addArrangedSubview(makeLabel())
55 | }
56 | }
57 |
58 | private func makeButton(title: String, color: UIColor, handler: @escaping () -> Void) -> UIButton {
59 | let button = UIButton(configuration: .filled())
60 | button.configuration?.baseBackgroundColor = color
61 | button.titleLabel?.font = UIFont.preferredFont(forTextStyle: .body)
62 | button.titleLabel?.adjustsFontForContentSizeCategory = true
63 | button.setTitleColor(.white, for: .normal)
64 | button.setTitle(title, for: .normal)
65 | button.addAction(UIAction(handler: { _ in handler() }), for: .touchUpInside)
66 | return button
67 | }
68 |
69 | private func makeLabel() -> UILabel {
70 | let label = UILabel()
71 | label.font = UIFont.preferredFont(forTextStyle: .body)
72 | label.adjustsFontForContentSizeCategory = true
73 | label.numberOfLines = 0
74 | label.text = """
75 | Lorem ipsum dolor sit amet consectetur adipisicing elit. Laborum minima voluptas officia eaque eveniet cupiditate dolores exercitationem soluta consequuntur rem blanditiis, odit delectus assumenda, beatae aliquam quidem voluptate nemo veniam.
76 | """
77 | label.backgroundColor = .systemGray6
78 | return label
79 | }
80 |
81 | private func configureConstraint() {
82 | scrollView.translatesAutoresizingMaskIntoConstraints = false
83 | NSLayoutConstraint.activate([
84 | scrollView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
85 | scrollView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
86 | scrollView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
87 | scrollView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
88 | ])
89 |
90 | stackView.translatesAutoresizingMaskIntoConstraints = false
91 | NSLayoutConstraint.activate([
92 | stackView.topAnchor.constraint(equalTo: scrollView.topAnchor, constant: 16),
93 | stackView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor, constant: 16),
94 | stackView.trailingAnchor .constraint(equalTo: scrollView.trailingAnchor, constant: -16),
95 | stackView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor, constant: -24),
96 | stackView.widthAnchor.constraint(equalTo: scrollView.frameLayoutGuide.widthAnchor, constant: -32),
97 | ])
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/Sample/SampleTests/SampleTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SampleTests.swift
3 | // SampleTests
4 | //
5 | // Created by Kanghoon Oh on 2023/10/01.
6 | //
7 |
8 | import XCTest
9 | @testable import Sample
10 |
11 | final class SampleTests: XCTestCase {
12 |
13 | override func setUpWithError() throws {
14 | // Put setup code here. This method is called before the invocation of each test method in the class.
15 | }
16 |
17 | override func tearDownWithError() throws {
18 | // Put teardown code here. This method is called after the invocation of each test method in the class.
19 | }
20 |
21 | func testExample() throws {
22 | // This is an example of a functional test case.
23 | // Use XCTAssert and related functions to verify your tests produce the correct results.
24 | // Any test you write for XCTest can be annotated as throws and async.
25 | // Mark your test throws to produce an unexpected failure when your test encounters an uncaught error.
26 | // Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards.
27 | }
28 |
29 | func testPerformanceExample() throws {
30 | // This is an example of a performance test case.
31 | measure {
32 | // Put the code you want to measure the time of here.
33 | }
34 | }
35 |
36 | }
37 |
--------------------------------------------------------------------------------
/Sample/SampleUITests/SampleUITests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SampleUITests.swift
3 | // SampleUITests
4 | //
5 | // Created by Kanghoon Oh on 2023/10/01.
6 | //
7 |
8 | import XCTest
9 |
10 | final class SampleUITests: 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 | // In UI tests it is usually best to stop immediately when a failure occurs.
16 | continueAfterFailure = false
17 |
18 | // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this.
19 | }
20 |
21 | override func tearDownWithError() throws {
22 | // Put teardown code here. This method is called after the invocation of each test method in the class.
23 | }
24 |
25 | func testExample() throws {
26 | // UI tests must launch the application that they test.
27 | let app = XCUIApplication()
28 | app.launch()
29 |
30 | // Use XCTAssert and related functions to verify your tests produce the correct results.
31 | }
32 |
33 | func testLaunchPerformance() throws {
34 | if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) {
35 | // This measures how long it takes to launch your application.
36 | measure(metrics: [XCTApplicationLaunchMetric()]) {
37 | XCUIApplication().launch()
38 | }
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/Sample/SampleUITests/SampleUITestsLaunchTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SampleUITestsLaunchTests.swift
3 | // SampleUITests
4 | //
5 | // Created by Kanghoon Oh on 2023/10/01.
6 | //
7 |
8 | import XCTest
9 |
10 | final class SampleUITestsLaunchTests: XCTestCase {
11 |
12 | override class var runsForEachTargetApplicationUIConfiguration: Bool {
13 | true
14 | }
15 |
16 | override func setUpWithError() throws {
17 | continueAfterFailure = false
18 | }
19 |
20 | func testLaunch() throws {
21 | let app = XCUIApplication()
22 | app.launch()
23 |
24 | // Insert steps here to perform after app launch but before taking a screenshot,
25 | // such as logging into a test account or navigating somewhere in the app
26 |
27 | let attachment = XCTAttachment(screenshot: app.screenshot())
28 | attachment.name = "Launch Screen"
29 | attachment.lifetime = .keepAlways
30 | add(attachment)
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/Sources/FloatingBottomSheet/Animator/FloatingBottomSheetAnimator.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FloatingBottomSheetAnimator.swift
3 | //
4 | //
5 | // Created by Kanghoon Oh on 2023/10/01.
6 | //
7 |
8 | import UIKit
9 |
10 | enum FloatingBottomSheetAnimator {
11 |
12 | static func animate(
13 | _ animations: @escaping () -> Void,
14 | _ completion: ((Bool) -> Void)? = nil
15 | ) {
16 | UIView.animate(
17 | withDuration: 0.5,
18 | delay: 0,
19 | usingSpringWithDamping: 0.8,
20 | initialSpringVelocity: 0,
21 | options: [.curveEaseInOut, .allowUserInteraction, .beginFromCurrentState],
22 | animations: animations,
23 | completion: completion
24 | )
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/Sources/FloatingBottomSheet/Animator/FloatingBottomSheetPresentationAnimator.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FloatingBottomSheetPresentationAnimator.swift
3 | //
4 | //
5 | // Created by Kanghoon Oh on 2023/10/01.
6 | //
7 |
8 | import UIKit
9 |
10 | public final class FloatingBottomSheetPresentationAnimator: NSObject {
11 |
12 | public enum TransitionDirection {
13 | case present
14 | case dismiss
15 | }
16 |
17 |
18 | // MARK: Properties
19 |
20 | private let transitionDirection: TransitionDirection
21 |
22 | private var feedbackGenerator: UISelectionFeedbackGenerator?
23 |
24 |
25 | // MARK: Initializing
26 |
27 | public init(transitionDirection: TransitionDirection) {
28 | self.transitionDirection = transitionDirection
29 | super.init()
30 |
31 | if case .present = transitionDirection {
32 | self.feedbackGenerator = UISelectionFeedbackGenerator()
33 | feedbackGenerator?.prepare()
34 | }
35 | }
36 | }
37 |
38 |
39 | // MARK: - UIViewControllerAnimatedTransitioning
40 |
41 | extension FloatingBottomSheetPresentationAnimator: UIViewControllerAnimatedTransitioning {
42 |
43 | public func transitionDuration(
44 | using transitionContext: UIViewControllerContextTransitioning?
45 | ) -> TimeInterval {
46 | 0.5
47 | }
48 |
49 | public func animateTransition(
50 | using transitionContext: UIViewControllerContextTransitioning
51 | ) {
52 | switch transitionDirection {
53 | case .present:
54 | presentTransition(transitionContext: transitionContext)
55 | case .dismiss:
56 | dismissTransition(transitionContext: transitionContext)
57 | }
58 | }
59 | }
60 |
61 |
62 | // MARK: - Private
63 |
64 | extension FloatingBottomSheetPresentationAnimator {
65 |
66 | private func presentTransition(transitionContext: UIViewControllerContextTransitioning) {
67 | guard let toViewController = transitionContext.viewController(forKey: .to) else {
68 | return
69 | }
70 |
71 | let presentable = toViewController as? FloatingBottomSheet
72 |
73 |
74 | let yOffset = presentable?.topYPosition ?? 0.0
75 |
76 | let bottomSheetContainerView: UIView = transitionContext.containerView.bottomSheetContainerView
77 | ?? toViewController.view
78 |
79 | bottomSheetContainerView.frame = transitionContext.finalFrame(for: toViewController)
80 | bottomSheetContainerView.frame.origin.y = transitionContext.containerView.frame.height
81 |
82 | feedbackGenerator?.selectionChanged()
83 |
84 | FloatingBottomSheetAnimator.animate({
85 | bottomSheetContainerView.frame.origin.y = yOffset
86 | }) { [weak self] isCompleted in
87 | transitionContext.completeTransition(isCompleted)
88 | self?.feedbackGenerator = nil
89 | }
90 | }
91 |
92 | private func dismissTransition(transitionContext: UIViewControllerContextTransitioning) {
93 | guard let fromViewController = transitionContext.viewController(forKey: .from) else {
94 | return
95 | }
96 |
97 | let bottomSheetContainerView: UIView = transitionContext.containerView.bottomSheetContainerView
98 | ?? fromViewController.view
99 |
100 | FloatingBottomSheetAnimator.animate({
101 | bottomSheetContainerView.frame.origin.y = transitionContext.containerView.frame.height
102 | }) { isCompleted in
103 | fromViewController.view.removeFromSuperview()
104 | transitionContext.completeTransition(isCompleted)
105 | }
106 | }
107 | }
108 |
109 | extension UIView {
110 |
111 | fileprivate var bottomSheetContainerView: FloatingBottomSheetContainerView? {
112 | subviews.first(where: { view -> Bool in
113 | view is FloatingBottomSheetContainerView
114 | }) as? FloatingBottomSheetContainerView
115 | }
116 | }
117 |
118 |
--------------------------------------------------------------------------------
/Sources/FloatingBottomSheet/Presentable/FloatingBottomSheet.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FloatingBottomSheet.swift
3 | //
4 | //
5 | // Created by Kanghoon Oh on 2023/10/01.
6 | //
7 |
8 | import UIKit
9 |
10 | /// A type that can be presented as a floating bottom sheet.
11 | public typealias FloatingBottomSheet = FloatingBottomSheetPresentable & UIViewController
12 |
--------------------------------------------------------------------------------
/Sources/FloatingBottomSheet/Presentable/FloatingBottomSheetPresentable+Default.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FloatingBottomSheetPresentable+Default.swift
3 | //
4 | //
5 | // Created by Kanghoon Oh on 2023/10/01.
6 | //
7 |
8 | import UIKit
9 |
10 | extension FloatingBottomSheetPresentable where Self: UIViewController {
11 |
12 | public var bottomSheetInsets: NSDirectionalEdgeInsets {
13 | .init(
14 | top: safeAreaInsets.top + 42.0,
15 | leading: 16.0,
16 | bottom: safeAreaInsets.bottom + 8.0,
17 | trailing: 16.0
18 | )
19 | }
20 |
21 | public var bottomSheetHeight: CGFloat {
22 | guard let scrollView = bottomSheetScrollable else { return 100 }
23 | scrollView.layoutIfNeeded()
24 | return scrollView.contentSize.height
25 | }
26 |
27 | public var bottomSheetCornerRadius: CGFloat {
28 | 20
29 | }
30 |
31 | public var bottomSheetDimColor: UIColor {
32 | UIColor.black.withAlphaComponent(0.5)
33 | }
34 |
35 | public var bottomSheetHandleColor: UIColor {
36 | UIColor(lightHex: "#EAEBEE", darkHex: "#34373D")
37 | }
38 |
39 | public var allowsDragToDismiss: Bool { true }
40 |
41 | public var allowsTapToDismiss: Bool { true }
42 |
43 | public func shouldRespond(to panGestureRecognizer: UIPanGestureRecognizer) -> Bool {
44 | true
45 | }
46 |
47 | public func willRespond(to panGestureRecognizer: UIPanGestureRecognizer) {}
48 |
49 | public func shouldPrioritize(panGestureRecognizer: UIPanGestureRecognizer) -> Bool {
50 | false
51 | }
52 |
53 | public func bottomSheetWillDismiss() {}
54 |
55 | public func bottomSheetDidDismiss() {}
56 | }
57 |
58 |
59 | // MARK: - Private
60 |
61 | extension FloatingBottomSheetPresentable {
62 |
63 | var safeAreaInsets: UIEdgeInsets {
64 | guard let rootViewController else { return .zero }
65 | return rootViewController.view.safeAreaInsets
66 | }
67 |
68 | private var keyWindow: UIWindow? {
69 | if #available(iOS 15.0, *) {
70 | return UIApplication.shared.connectedScenes
71 | .compactMap { ($0 as? UIWindowScene)?.keyWindow }
72 | .first
73 | } else {
74 | return UIApplication.shared.windows.first { $0.isKeyWindow }
75 | }
76 | }
77 |
78 | private var rootViewController: UIViewController? {
79 | keyWindow?.rootViewController
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/Sources/FloatingBottomSheet/Presentable/FloatingBottomSheetPresentable+Internal.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FloatingBottomSheetPresentable+Internal.swift
3 | //
4 | //
5 | // Created by Kanghoon Oh on 2023/10/01.
6 | //
7 |
8 | import UIKit
9 |
10 | extension FloatingBottomSheetPresentable where Self: UIViewController {
11 |
12 | var bottomSheetPresentationController: FloatingBottomSheetPresentationController? {
13 | presentationController as? FloatingBottomSheetPresentationController
14 | }
15 |
16 | var topYPosition: CGFloat {
17 | max(topMargin(from: containerViewHeight), 0) + bottomSheetInsets.top
18 | }
19 |
20 | private var containerViewHeight: CGFloat {
21 | bottomSheetHeight
22 | + FloatingBottomSheetHandleMetric.verticalMargin * 2
23 | + FloatingBottomSheetHandleMetric.size.height
24 | }
25 |
26 | private var bottomYPosition: CGFloat {
27 | guard let containerView = bottomSheetPresentationController?.containerView
28 | else { return view.bounds.height }
29 |
30 | return containerView.bounds.size.height - bottomSheetInsets.bottom - bottomSheetInsets.top
31 | }
32 |
33 | private func topMargin(from height: CGFloat) -> CGFloat {
34 | bottomYPosition - height
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/Sources/FloatingBottomSheet/Presentable/FloatingBottomSheetPresentable+UIViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FloatingBottomSheetPresentable+UIViewController.swift
3 | //
4 | //
5 | // Created by Kanghoon Oh on 2023/10/01.
6 | //
7 |
8 | import UIKit
9 |
10 | extension FloatingBottomSheetPresentable where Self: UIViewController {
11 |
12 | /// Triggers layout changes for the bottom sheet presentation controller.
13 | ///
14 | /// This method can be called to initiate layout changes for the bottom sheet presentation controller.
15 | /// The `animated` parameter determines whether the layout updates should be performed with animation.
16 | ///
17 | /// - Parameter animated: A Boolean value indicating whether the layout changes should be performed with animation.
18 | public func bottomSheetPerformLayout(animated: Bool) {
19 | bottomSheetPresentationController?.performLayout(animated: animated)
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Sources/FloatingBottomSheet/Presentable/FloatingBottomSheetPresentable.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FloatingBottomSheetPresentable.swift
3 | //
4 | //
5 | // Created by Kanghoon Oh on 2023/10/01.
6 | //
7 |
8 | import UIKit
9 |
10 | /// It is the configuration object for a view controller
11 | /// that will be presented using transition
12 | ///
13 | /// ```
14 | /// extension ViewController: FloatingBottomSheetPresentable {
15 | ///
16 | /// var bottomSheetScrollable: UIScrollView? {
17 | /// scrollView
18 | /// }
19 | ///
20 | /// var bottomSheetHeight: CGFloat {
21 | /// scrollView.contentSize.height
22 | /// }
23 | /// }
24 | /// ```
25 | public protocol FloatingBottomSheetPresentable: AnyObject {
26 |
27 | /// The scroll view embedded in the view controller.
28 | /// it allows for seamless transition between the embedded scroll view and bottom sheet container view.
29 | var bottomSheetScrollable: UIScrollView? { get }
30 |
31 | /// The insets between the screen and the container view for a bottom sheet.
32 | ///
33 | /// The default value for `bottomSheetInsets` is
34 | /// - top: view.safeAreaInsets.top + 42.0
35 | /// - leading: 16.0
36 | /// - trailing: 16.0
37 | /// - bottom: view.safeAreaInsets.bottom + 8.0
38 | var bottomSheetInsets: NSDirectionalEdgeInsets { get }
39 |
40 | /// The `bottomSheetHeight` property represents the height of the presented view for a bottom sheet.
41 | /// Height of the bottom sheet container view is `bottomSheetHeight` + 24 (handle view area)
42 | ///
43 | /// This property is adjusted to the maximum possible height within the screen limits.
44 | ///
45 | /// The default value for `bottomSheetHeight` is 100.
46 | var bottomSheetHeight: CGFloat { get }
47 |
48 | /// The bottom sheet corner radius
49 | ///
50 | /// The default value for `bottomSheetCornerRadius` is 20.
51 | var bottomSheetCornerRadius: CGFloat { get }
52 |
53 | /// The bottom sheet dim color
54 | ///
55 | /// The default value for `bottomSheetDimColor` is black with alpha component 0.8
56 | var bottomSheetDimColor: UIColor { get }
57 |
58 | /// The bottom sheet handle color
59 | ///
60 | /// The default value for `bottomSheetHandleColor` is `UIColor(lightHex: "#EAEBEE", darkHex: "#34373D")`
61 | var bottomSheetHandleColor: UIColor { get }
62 |
63 | /// The `allowsDragToDismiss` property determines whether the user can swipe down to dismiss the bottom sheet.
64 | ///
65 | /// If `allowsDragToDismiss` is set to `true`, the user can dismiss the bottom sheet by swiping it down.
66 | /// If set to `false`, this behavior is disabled.
67 | ///
68 | /// The default value for `allowsDragToDismiss` is `true`.
69 | var allowsDragToDismiss: Bool { get }
70 |
71 | /// The `allowsTapToDismiss` property determines whether the user can tap the dimmed background view to dismiss the bottom sheet.
72 | ///
73 | /// When `allowsTapToDismiss` is set to `true`, tapping on the dimmed background view will dismiss the bottom sheet.
74 | /// If set to `false`, this interaction is disabled.
75 | ///
76 | /// The default value for `allowsTapToDismiss` is `true`.
77 | var allowsTapToDismiss: Bool { get }
78 |
79 | /// This method is used to query the delegate about whether the bottom sheet should respond to the bottom sheet gesture recognizer.
80 | ///
81 | /// Return `false` to disable the bottom sheet's movement while keeping other gestures on the presented view intact.
82 | ///
83 | /// The default value is `true`.
84 | ///
85 | /// - Parameters:
86 | /// - panGestureRecognizer: The gesture recognizer used for bottom sheet interaction.
87 | /// - Returns: A Boolean value indicating whether the bottom sheet should respond to the bottom sheet gesture recognizer.
88 | func shouldRespond(to panGestureRecognizer: UIPanGestureRecognizer) -> Bool
89 |
90 | /// This method notifies the delegate when the bottom sheet gesture recognizer's state transitions to either `began` or `changed`.
91 | /// It provides an opportunity for the delegate to prepare for changes in the gesture recognizer's state, such as when the bottom sheet view is about to scroll.
92 |
93 | /// The default implementation is an empty method.
94 |
95 | /// - Parameter panGestureRecognizer: The gesture recognizer used for bottom sheet interaction.
96 | func willRespond(to panGestureRecognizer: UIPanGestureRecognizer)
97 |
98 | /// Asks the delegate whether the bottom sheet gesture recognizer should be given priority for the bottom sheet.
99 | ///
100 | /// For example, you can use this method to define a region where you want to restrict the starting location of the pan gesture.
101 | ///
102 | /// If you return `false`, the decision to succeed or fail the pan gesture relies solely on internal conditions, such as whether the scrollView is actively scrolling.
103 | ///
104 | /// The default return value is `false`.
105 | ///
106 | /// - Parameter panGestureRecognizer: The gesture recognizer used for bottom sheet interaction.
107 | /// - Returns: A Boolean value indicating whether the bottom sheet gesture recognizer should be prioritized.
108 | func shouldPrioritize(panGestureRecognizer: UIPanGestureRecognizer) -> Bool
109 |
110 | /// Informs the delegate that the bottom sheet is about to be dismissed.
111 | ///
112 | /// This method is called just before the bottom sheet is dismissed, giving the delegate an opportunity to perform any necessary tasks.
113 | ///
114 | /// The default behavior is an empty implementation.
115 | func bottomSheetWillDismiss()
116 |
117 | /// Informs the delegate that the bottom sheet has been dismissed.
118 | ///
119 | /// This method is called after the bottom sheet has been dismissed, allowing the delegate to perform any post-dismissal actions.
120 | ///
121 | /// The default behavior is an empty implementation.
122 | func bottomSheetDidDismiss()
123 | }
124 |
--------------------------------------------------------------------------------
/Sources/FloatingBottomSheet/Presentation/FloatingBottomSheetContainerView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FloatingBottomSheetContainerView.swift
3 | //
4 | //
5 | // Created by Kanghoon Oh on 2023/10/01.
6 | //
7 |
8 | import UIKit
9 |
10 | final class FloatingBottomSheetContainerView: UIView {
11 |
12 | init(presentedView: UIView, frame: CGRect) {
13 | super.init(frame: frame)
14 | addSubview(presentedView)
15 | }
16 |
17 | required init?(coder: NSCoder) {
18 | fatalError("init(coder:) has not been implemented")
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/Sources/FloatingBottomSheet/Presentation/FloatingBottomSheetHandleMetric.swift:
--------------------------------------------------------------------------------
1 | //
2 | // BottomSheetHandleMetric.swift
3 | //
4 | //
5 | // Created by Ray on 3/24/24.
6 | //
7 |
8 | import UIKit
9 |
10 | enum FloatingBottomSheetHandleMetric {
11 | static let size = CGSize(width: 40, height: 4)
12 | static let verticalMargin: CGFloat = 10
13 | }
14 |
--------------------------------------------------------------------------------
/Sources/FloatingBottomSheet/Presentation/FloatingBottomSheetPresentationController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FloatingBottomSheetPresentationController.swift
3 | //
4 | //
5 | // Created by Kanghoon Oh on 2023/10/01.
6 | //
7 |
8 | import UIKit
9 |
10 | public final class FloatingBottomSheetPresentationController: UIPresentationController {
11 |
12 | // MARK: Constants
13 |
14 | private enum Metric {
15 |
16 | enum PresentedView {
17 | static let cornerRadius: CGFloat = 20
18 | }
19 | }
20 |
21 | private enum Const {
22 | static let snapMovementSensitivity: CGFloat = 0.5
23 | }
24 |
25 |
26 | // MARK: UI
27 |
28 | private var presentable: FloatingBottomSheetPresentable? {
29 | presentedViewController as? FloatingBottomSheetPresentable
30 | }
31 |
32 | private lazy var dimmingView: UIView = {
33 | let dimmingView = UIView()
34 | dimmingView.alpha = 0
35 | dimmingView.addGestureRecognizer(tapGesture)
36 | return dimmingView
37 | }()
38 |
39 | private lazy var bottomSheetContainerView = FloatingBottomSheetContainerView(
40 | presentedView: presentedViewController.view,
41 | frame: containerView?.frame ?? .zero
42 | )
43 |
44 | private lazy var handleView: UIView = {
45 | let view = UIView()
46 | view.backgroundColor = presentable?.bottomSheetHandleColor
47 | view.layer.cornerRadius = FloatingBottomSheetHandleMetric.size.height * 0.5
48 | return view
49 | }()
50 |
51 | public override var presentedView: UIView {
52 | bottomSheetContainerView.bringSubviewToFront(handleView)
53 | return bottomSheetContainerView
54 | }
55 |
56 | public override var frameOfPresentedViewInContainerView: CGRect {
57 | guard let containerViewFrame = containerView?.frame else { return .zero }
58 | let adjustedSize = CGSize(
59 | width: containerViewFrame.size.width - bottomSheetInsets.leading - bottomSheetInsets.trailing,
60 | height: containerViewFrame.size.height - bottomSheetInsets.bottom - topYPosition
61 | )
62 |
63 | return CGRect(
64 | x: bottomSheetInsets.leading,
65 | y: topYPosition,
66 | width: adjustedSize.width,
67 | height: adjustedSize.height
68 | )
69 | }
70 |
71 | // MARK: Gesture
72 |
73 | private lazy var tapGesture = UITapGestureRecognizer(
74 | target: self,
75 | action: #selector(didTapDimmingView)
76 | )
77 |
78 | private lazy var panGestureRecognizer: UIPanGestureRecognizer = {
79 | let gesture = UIPanGestureRecognizer(target: self, action: #selector(didPanOnPresentedView(_ :)))
80 | gesture.minimumNumberOfTouches = 1
81 | gesture.maximumNumberOfTouches = 1
82 | gesture.delegate = self
83 | return gesture
84 | }()
85 |
86 |
87 | // MARK: Properties
88 |
89 | private var scrollObserver: NSKeyValueObservation?
90 |
91 | private var topYPosition: CGFloat = 0.0
92 |
93 | private var bottomSheetInsets: NSDirectionalEdgeInsets = .zero
94 |
95 | private var isPresentedViewAnimating = false
96 |
97 | private var scrollViewYOffset: CGFloat = 0.0
98 |
99 |
100 | // MARK: Initializing
101 |
102 | deinit {
103 | scrollObserver?.invalidate()
104 | }
105 |
106 |
107 | // MARK: View Life Cycle
108 |
109 | public override func containerViewWillLayoutSubviews() {
110 | super.containerViewWillLayoutSubviews()
111 | configureViewLayout()
112 | presentedView.frame = frameOfPresentedViewInContainerView
113 | presentedViewController.view.frame.size = frameOfPresentedViewInContainerView.size
114 | }
115 |
116 | public override func containerViewDidLayoutSubviews() {
117 | super.containerViewDidLayoutSubviews()
118 | adjustHandleViewFrame()
119 | adjustPresentedViewControllerTopInset()
120 | addRoundedCorners(to: bottomSheetContainerView)
121 | }
122 |
123 |
124 | // MARK: Presentation Transition
125 |
126 | public override func presentationTransitionWillBegin() {
127 | guard let containerView else { return }
128 |
129 | bottomSheetContainerView.addSubview(handleView)
130 | bottomSheetContainerView.addGestureRecognizer(panGestureRecognizer)
131 |
132 | containerView.addSubview(dimmingView)
133 | containerView.addSubview(bottomSheetContainerView)
134 |
135 | layoutDimmingView(in: containerView)
136 | performLayout(animated: false)
137 | adjustBackgroundColors()
138 |
139 | guard let coordinator = presentedViewController.transitionCoordinator else {
140 | // Calls viewWillAppear and viewWillDisappear
141 | presentingViewController.beginAppearanceTransition(false, animated: false)
142 | dimmingView.alpha = 1.0
143 | return
144 | }
145 |
146 | // Calls viewWillAppear and viewWillDisappear
147 | presentingViewController.beginAppearanceTransition(false, animated: coordinator.isAnimated)
148 |
149 | coordinator.animate(alongsideTransition: { [weak self] _ in
150 | self?.dimmingView.alpha = 1.0
151 | self?.presentedViewController.setNeedsStatusBarAppearanceUpdate()
152 | })
153 | }
154 |
155 | public override func presentationTransitionDidEnd(_ completed: Bool) {
156 | // Calls viewDidAppear and viewDidDisappear
157 | presentingViewController.endAppearanceTransition()
158 |
159 | if !completed {
160 | dimmingView.removeFromSuperview()
161 | }
162 | }
163 |
164 |
165 | // MARK: Dismissal Transition
166 |
167 | public override func dismissalTransitionWillBegin() {
168 | presentable?.bottomSheetWillDismiss()
169 |
170 | guard let coordinator = presentedViewController.transitionCoordinator else {
171 | // Calls viewWillAppear and viewWillDisappear
172 | presentingViewController.beginAppearanceTransition(true, animated: false)
173 | dimmingView.alpha = 0.0
174 | return
175 | }
176 |
177 | // Calls viewWillAppear and viewWillDisappear
178 | presentingViewController.beginAppearanceTransition(true, animated: coordinator.isAnimated)
179 |
180 | coordinator.animate { [weak self] context in
181 | self?.dimmingView.alpha = 0.0
182 | if !context.isAnimated {
183 | self?.bottomSheetContainerView.alpha = 0.0
184 | }
185 | self?.presentingViewController.setNeedsStatusBarAppearanceUpdate()
186 | }
187 | }
188 |
189 | public override func dismissalTransitionDidEnd(_ completed: Bool) {
190 | // Calls viewDidAppear and viewDidDisappear
191 | presentingViewController.endAppearanceTransition()
192 |
193 | if completed {
194 | presentable?.bottomSheetDidDismiss()
195 | }
196 | }
197 | }
198 |
199 |
200 | // MARK: - Presented View Layout
201 |
202 | extension FloatingBottomSheetPresentationController {
203 |
204 | var isPresentedViewAnchored: Bool {
205 | if !isPresentedViewAnimating,
206 | bottomSheetContainerView.frame.minY.rounded() <= topYPosition.rounded() {
207 | return true
208 | }
209 |
210 | return false
211 | }
212 |
213 | func performLayout(animated: Bool) {
214 | if animated {
215 | FloatingBottomSheetAnimator.animate({ [weak self] in
216 | guard let self else { return }
217 | isPresentedViewAnimating = true
218 | configureViewLayout()
219 | observe(scrollView: presentable?.bottomSheetScrollable)
220 | configureScrollViewInsets()
221 | containerView?.setNeedsLayout()
222 | containerView?.layoutIfNeeded()
223 | }) { [weak self] isCompleted in
224 | self?.isPresentedViewAnimating = !isCompleted
225 | }
226 | } else {
227 | configureViewLayout()
228 | observe(scrollView: presentable?.bottomSheetScrollable)
229 | configureScrollViewInsets()
230 | containerView?.setNeedsLayout()
231 | }
232 | }
233 |
234 | private func adjustHandleViewFrame() {
235 | handleView.frame.origin.y = FloatingBottomSheetHandleMetric.verticalMargin
236 | handleView.frame.size = FloatingBottomSheetHandleMetric.size
237 | handleView.center.x = CGRectGetMidX(bottomSheetContainerView.bounds)
238 | }
239 |
240 | private func adjustPresentedViewControllerTopInset() {
241 | let topInset = handleView.frame.maxY + FloatingBottomSheetHandleMetric.verticalMargin
242 | presentedViewController.additionalSafeAreaInsets.top = topInset
243 | }
244 |
245 | private func adjustBackgroundColors() {
246 | dimmingView.backgroundColor = presentable?.bottomSheetDimColor
247 |
248 | handleView.backgroundColor = presentable?.bottomSheetHandleColor
249 |
250 | bottomSheetContainerView.backgroundColor = presentedViewController.view.backgroundColor
251 | ?? presentable?.bottomSheetScrollable?.backgroundColor
252 | }
253 |
254 | private func layoutDimmingView(in containerView: UIView) {
255 | dimmingView.translatesAutoresizingMaskIntoConstraints = false
256 | NSLayoutConstraint.activate([
257 | dimmingView.topAnchor.constraint(equalTo: containerView.topAnchor),
258 | dimmingView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor),
259 | dimmingView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor),
260 | dimmingView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor),
261 | ])
262 | }
263 |
264 | private func configureViewLayout() {
265 | guard let viewControllable = presentedViewController as? FloatingBottomSheet
266 | else { return }
267 |
268 | topYPosition = viewControllable.topYPosition
269 | bottomSheetInsets = viewControllable.bottomSheetInsets
270 | }
271 |
272 | private func configureScrollViewInsets() {
273 | guard let scrollView = presentable?.bottomSheetScrollable,
274 | !scrollView.isScrolling
275 | else { return }
276 |
277 | scrollView.showsVerticalScrollIndicator = false
278 | scrollView.contentInsetAdjustmentBehavior = .never
279 | }
280 |
281 | private func addRoundedCorners(to view: UIView) {
282 | view.layer.cornerRadius = presentable?.bottomSheetCornerRadius ?? Metric.PresentedView.cornerRadius
283 | view.layer.masksToBounds = true
284 | }
285 | }
286 |
287 |
288 | // MARK: - Gesture
289 |
290 | extension FloatingBottomSheetPresentationController {
291 |
292 | @objc
293 | private func didTapDimmingView() {
294 | guard presentable?.allowsTapToDismiss == true else { return }
295 | presentedViewController.dismiss(animated: true)
296 | }
297 |
298 | @objc
299 | private func didPanOnPresentedView(_ recognizer: UIPanGestureRecognizer) {
300 | guard shouldRespond(to: recognizer),
301 | let containerView
302 | else {
303 | recognizer.setTranslation(.zero, in: recognizer.view)
304 | return
305 | }
306 |
307 | switch recognizer.state {
308 | case .began, .changed:
309 | respond(to: recognizer)
310 |
311 | default:
312 | let velocity = recognizer.velocity(in: bottomSheetContainerView)
313 |
314 | if isVelocityWithinSensitivityRange(velocity.y) {
315 | if bottomSheetContainerView.frame.minY < topYPosition || presentable?.allowsDragToDismiss == false {
316 | snap(toYPosition: topYPosition)
317 | } else {
318 | presentedViewController.dismiss(animated: true)
319 | }
320 |
321 | } else {
322 | let position = nearest(
323 | to: bottomSheetContainerView.frame.minY,
324 | inValues: [containerView.bounds.height, topYPosition]
325 | )
326 |
327 | if position == topYPosition || presentable?.allowsDragToDismiss == false {
328 | snap(toYPosition: topYPosition)
329 | } else {
330 | presentedViewController.dismiss(animated: true)
331 | }
332 | }
333 | }
334 | }
335 |
336 | func shouldRespond(to panGestureRecognizer: UIPanGestureRecognizer) -> Bool {
337 | guard presentable?.shouldRespond(to: panGestureRecognizer) == true ||
338 | !(panGestureRecognizer.state == .began || panGestureRecognizer.state == .cancelled)
339 | else {
340 | panGestureRecognizer.isEnabled = false
341 | panGestureRecognizer.isEnabled = true
342 | return false
343 | }
344 | return !shouldFail(panGestureRecognizer: panGestureRecognizer)
345 | }
346 |
347 | func respond(to panGestureRecognizer: UIPanGestureRecognizer) {
348 | presentable?.willRespond(to: panGestureRecognizer)
349 |
350 | var yDisplacement = panGestureRecognizer.translation(in: bottomSheetContainerView).y
351 |
352 | if bottomSheetContainerView.frame.origin.y < topYPosition {
353 | yDisplacement /= 2.0
354 | }
355 | adjust(toYPosition: bottomSheetContainerView.frame.origin.y + yDisplacement)
356 |
357 | panGestureRecognizer.setTranslation(.zero, in: bottomSheetContainerView)
358 | }
359 |
360 | func shouldFail(panGestureRecognizer: UIPanGestureRecognizer) -> Bool {
361 | guard !shouldPrioritize(panGestureRecognizer: panGestureRecognizer)
362 | else {
363 | presentable?.bottomSheetScrollable?.panGestureRecognizer.isEnabled = false
364 | presentable?.bottomSheetScrollable?.panGestureRecognizer.isEnabled = true
365 | return false
366 | }
367 |
368 | guard isPresentedViewAnchored,
369 | let scrollView = presentable?.bottomSheetScrollable,
370 | scrollView.contentOffset.y > 0
371 | else {
372 | return false
373 | }
374 |
375 | let location = panGestureRecognizer.location(in: bottomSheetContainerView)
376 | return scrollView.frame.contains(location) || scrollView.isScrolling
377 | }
378 |
379 | func shouldPrioritize(panGestureRecognizer: UIPanGestureRecognizer) -> Bool {
380 | panGestureRecognizer.state == .began &&
381 | presentable?.shouldPrioritize(panGestureRecognizer: panGestureRecognizer) == true
382 | }
383 |
384 | func isVelocityWithinSensitivityRange(_ velocity: CGFloat) -> Bool {
385 | (velocity - (1000 * (1 - Const.snapMovementSensitivity))) > 0
386 | }
387 |
388 | func snap(toYPosition yPosition: CGFloat) {
389 | FloatingBottomSheetAnimator.animate({ [weak self] in
390 | self?.adjust(toYPosition: yPosition)
391 | self?.isPresentedViewAnimating = true
392 | }) { [weak self] isCompleted in
393 | self?.isPresentedViewAnimating = !isCompleted
394 | }
395 | }
396 |
397 | func adjust(toYPosition yPosition: CGFloat) {
398 | bottomSheetContainerView.frame.origin.y = max(yPosition, topYPosition)
399 |
400 | guard bottomSheetContainerView.frame.origin.y > topYPosition else {
401 | dimmingView.alpha = 1.0
402 | return
403 | }
404 |
405 | let yDisplacementFromShortForm = bottomSheetContainerView.frame.origin.y - topYPosition
406 |
407 | dimmingView.alpha = 1.0 - (yDisplacementFromShortForm / bottomSheetContainerView.frame.height)
408 | }
409 |
410 | func nearest(to number: CGFloat, inValues values: [CGFloat]) -> CGFloat {
411 | guard let nearestVal = values.min(by: { abs(number - $0) < abs(number - $1) })
412 | else { return number }
413 | return nearestVal
414 | }
415 | }
416 |
417 |
418 | // MARK: - UIGestureRecognizerDelegate
419 |
420 | extension FloatingBottomSheetPresentationController: UIGestureRecognizerDelegate {
421 |
422 | public func gestureRecognizer(
423 | _ gestureRecognizer: UIGestureRecognizer,
424 | shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer
425 | ) -> Bool {
426 | false
427 | }
428 |
429 | public func gestureRecognizer(
430 | _ gestureRecognizer: UIGestureRecognizer,
431 | shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer
432 | ) -> Bool {
433 | otherGestureRecognizer.view == presentable?.bottomSheetScrollable
434 | }
435 | }
436 |
437 |
438 | // MARK: - UIScrollView Observer
439 |
440 | extension FloatingBottomSheetPresentationController {
441 |
442 | private func observe(scrollView: UIScrollView?) {
443 | scrollObserver?.invalidate()
444 | scrollObserver = scrollView?.observe(\.contentOffset, options: .old) { [weak self] scrollView, change in
445 | guard self?.containerView != nil else { return }
446 |
447 | self?.didPanOnScrollView(scrollView, change: change)
448 | }
449 | }
450 |
451 | private func didPanOnScrollView(
452 | _ scrollView: UIScrollView,
453 | change: NSKeyValueObservedChange
454 | ) {
455 | guard !presentedViewController.isBeingDismissed,
456 | !presentedViewController.isBeingPresented
457 | else { return }
458 |
459 | if !isPresentedViewAnchored, scrollView.contentOffset.y > 0 {
460 | haltScrolling(scrollView)
461 | return
462 | }
463 |
464 | if scrollView.isScrolling || isPresentedViewAnimating {
465 | if isPresentedViewAnchored {
466 | trackScrolling(scrollView)
467 | } else {
468 | haltScrolling(scrollView)
469 | }
470 | return
471 | }
472 |
473 | if presentedViewController.view.isKind(of: UIScrollView.self),
474 | !isPresentedViewAnimating, scrollView.contentOffset.y <= 0 {
475 | handleScrollViewTopBounce(scrollView: scrollView, change: change)
476 | return
477 | }
478 |
479 | trackScrolling(scrollView)
480 | }
481 |
482 | private func haltScrolling(_ scrollView: UIScrollView) {
483 | scrollView.setContentOffset(CGPoint(x: 0, y: scrollViewYOffset), animated: false)
484 | scrollView.showsVerticalScrollIndicator = false
485 | }
486 |
487 | private func trackScrolling(_ scrollView: UIScrollView) {
488 | scrollViewYOffset = max(scrollView.contentOffset.y, 0)
489 | scrollView.showsVerticalScrollIndicator = true
490 | }
491 |
492 | private func handleScrollViewTopBounce(scrollView: UIScrollView, change: NSKeyValueObservedChange) {
493 | guard let oldYValue = change.oldValue?.y, scrollView.isDecelerating else { return }
494 |
495 | let yOffset = scrollView.contentOffset.y
496 | let presentedSize = containerView?.frame.size ?? .zero
497 |
498 | bottomSheetContainerView.bounds.size = CGSize(width: presentedSize.width, height: presentedSize.height + yOffset)
499 |
500 | if oldYValue > yOffset {
501 | bottomSheetContainerView.frame.origin.y = topYPosition - yOffset
502 | } else {
503 | scrollViewYOffset = 0
504 | snap(toYPosition: topYPosition)
505 | }
506 |
507 | scrollView.showsVerticalScrollIndicator = false
508 | }
509 | }
510 |
511 | extension UIScrollView {
512 |
513 | fileprivate var isScrolling: Bool {
514 | isDragging && !isDecelerating || isTracking
515 | }
516 | }
517 |
--------------------------------------------------------------------------------
/Sources/FloatingBottomSheet/Presentation/FloatingBottomSheetPresentationDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FloatingBottomSheetPresentationDelegate.swift
3 | //
4 | //
5 | // Created by Kanghoon Oh on 2023/10/01.
6 | //
7 |
8 | import UIKit
9 |
10 | public final class FloatingBottomSheetPresentationDelegate: NSObject {
11 |
12 | public static var `default` = FloatingBottomSheetPresentationDelegate()
13 | }
14 |
15 |
16 | // MARK: - UIViewControllerTransitioningDelegate
17 |
18 | extension FloatingBottomSheetPresentationDelegate: UIViewControllerTransitioningDelegate {
19 |
20 | public func animationController(
21 | forPresented presented: UIViewController,
22 | presenting: UIViewController,
23 | source: UIViewController
24 | ) -> UIViewControllerAnimatedTransitioning? {
25 | FloatingBottomSheetPresentationAnimator(transitionDirection: .present)
26 | }
27 |
28 | public func animationController(
29 | forDismissed dismissed: UIViewController
30 | ) -> UIViewControllerAnimatedTransitioning? {
31 | FloatingBottomSheetPresentationAnimator(transitionDirection: .dismiss)
32 | }
33 |
34 | public func presentationController(
35 | forPresented presented: UIViewController,
36 | presenting: UIViewController?,
37 | source: UIViewController
38 | ) -> UIPresentationController? {
39 | let controller = FloatingBottomSheetPresentationController(
40 | presentedViewController: presented,
41 | presenting: presenting
42 | )
43 | controller.delegate = self
44 | return controller
45 | }
46 | }
47 |
48 |
49 | // MARK: - UIAdaptivePresentationControllerDelegate
50 |
51 | extension FloatingBottomSheetPresentationDelegate: UIAdaptivePresentationControllerDelegate {
52 |
53 | public func adaptivePresentationStyle(
54 | for controller: UIPresentationController,
55 | traitCollection: UITraitCollection
56 | ) -> UIModalPresentationStyle {
57 | .none
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/Sources/FloatingBottomSheet/Presenter/FloatingBottomSheetPresenter.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FloatingBottomSheetPresenter.swift
3 | //
4 | //
5 | // Created by Kanghoon Oh on 2023/10/01.
6 | //
7 |
8 | import UIKit
9 |
10 | /// Protocol for presenting a floating bottom sheet.
11 | protocol FloatingBottomSheetPresenter: AnyObject {
12 |
13 | /// A boolean property indicating whether a floating bottom sheet is currently presented.
14 | var isFloatingBottomSheetPresented: Bool { get }
15 |
16 | /// Presents a floating bottom sheet.
17 | ///
18 | /// - Parameters:
19 | /// - viewControllerToPresent: The FloatingBottomSheet view controller to present.
20 | /// - completion: A closure to be executed after the presentation is complete.
21 | func presentFloatingBottomSheet(
22 | _ viewControllerToPresent: FloatingBottomSheet,
23 | completion: (() -> Void)?
24 | )
25 | }
26 |
--------------------------------------------------------------------------------
/Sources/FloatingBottomSheet/Presenter/UIViewController+FloatingBottomSheetPresenter.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UIViewController+FloatingBottomSheetPresenter.swift
3 | //
4 | //
5 | // Created by Kanghoon Oh on 2023/10/01.
6 | //
7 |
8 | import UIKit
9 |
10 | extension UIViewController: FloatingBottomSheetPresenter {
11 |
12 | /// A flag that returns true if the topmost view controller in the navigation stack
13 | /// was presented using the custom FloatingBottomSheet transition.
14 | ///
15 | /// - Warning: ⚠️ Calling `transitioningDelegate` in this function may cause a memory leak. ⚠️
16 | ///
17 | /// In most cases, this check will be used early in the view lifecycle and unfortunately,
18 | /// there's a potential issue that causes a memory leak if the `transitioningDelegate` is
19 | /// referenced here and called too early, resulting in a strong reference to this view controller.
20 | public var isFloatingBottomSheetPresented: Bool {
21 | (transitioningDelegate as? FloatingBottomSheetPresentationDelegate) != nil
22 | }
23 |
24 | /// Configures a view controller for presentation using the FloatingBottomSheet transition.
25 | ///
26 | /// - Parameters:
27 | /// - viewControllerToPresent: The view controller to be presented.
28 | /// - completion: The block to execute after the presentation finishes. You may specify nil for this parameter.
29 | ///
30 | /// The function sets the modal presentation style and related properties for the specified view controller
31 | /// to achieve a FloatingBottomSheet presentation. It also assigns the appropriate transitioning delegate
32 | /// for the presentation style.
33 | public func presentFloatingBottomSheet(
34 | _ viewControllerToPresent: FloatingBottomSheet,
35 | completion: (() -> Void)? = nil
36 | ) {
37 | viewControllerToPresent.modalPresentationStyle = .custom
38 | viewControllerToPresent.modalPresentationCapturesStatusBarAppearance = true
39 | viewControllerToPresent.transitioningDelegate = FloatingBottomSheetPresentationDelegate.default
40 |
41 | present(viewControllerToPresent, animated: true, completion: completion)
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/Sources/FloatingBottomSheet/Utils/UIColor+Init.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UIColor+Init.swift
3 | //
4 | //
5 | // Created by Kanghoon Oh on 2023/10/01.
6 | //
7 |
8 | import UIKit
9 |
10 | extension UIColor {
11 |
12 | convenience init(
13 | r: Int,
14 | g: Int,
15 | b: Int,
16 | a: Int? = nil
17 | ) {
18 | self.init(
19 | red: CGFloat(r) / 255,
20 | green: CGFloat(g) / 255,
21 | blue: CGFloat(b) / 255,
22 | alpha: CGFloat(a ?? 255) / 255
23 | )
24 | }
25 |
26 | convenience init(hex: String) {
27 | let hexSanitized = hex
28 | .trimmingCharacters(in: .whitespacesAndNewlines)
29 | .replacingOccurrences(of: "#", with: "")
30 |
31 | var rgb: UInt64 = 0
32 |
33 | var r: CGFloat = 0.0
34 | var g: CGFloat = 0.0
35 | var b: CGFloat = 0.0
36 | var a: CGFloat = 1.0
37 |
38 | let length = hexSanitized.count
39 |
40 | if Scanner(string: hexSanitized).scanHexInt64(&rgb) == true {
41 | if length == 6 {
42 | r = CGFloat((rgb & 0xFF0000) >> 16) / 255.0
43 | g = CGFloat((rgb & 0x00FF00) >> 8) / 255.0
44 | b = CGFloat(rgb & 0x0000FF) / 255.0
45 |
46 | } else if length == 8 {
47 | r = CGFloat((rgb & 0xFF000000) >> 24) / 255.0
48 | g = CGFloat((rgb & 0x00FF0000) >> 16) / 255.0
49 | b = CGFloat((rgb & 0x0000FF00) >> 8) / 255.0
50 | a = CGFloat(rgb & 0x000000FF) / 255.0
51 | }
52 | }
53 |
54 | self.init(red: r, green: g, blue: b, alpha: a)
55 | }
56 |
57 | convenience init(light: UIColor, dark: UIColor) {
58 | if #available(iOS 13.0, *) {
59 | self.init(dynamicProvider: {
60 | switch $0.userInterfaceStyle {
61 | case .light, .unspecified:
62 | return light
63 | case .dark:
64 | return dark
65 | @unknown default:
66 | assertionFailure("Unknown userInterfaceStyle: \($0.userInterfaceStyle)")
67 | return light
68 | }
69 | })
70 | } else {
71 | self.init(cgColor: light.cgColor)
72 | }
73 | }
74 |
75 | convenience init(lightHex: String, darkHex: String) {
76 | if #available(iOS 13.0, *) {
77 | self.init(dynamicProvider: {
78 | switch $0.userInterfaceStyle {
79 | case .light, .unspecified:
80 | return UIColor(hex: lightHex)
81 | case .dark:
82 | return UIColor(hex: darkHex)
83 | @unknown default:
84 | assertionFailure("Unknown userInterfaceStyle: \($0.userInterfaceStyle)")
85 | return UIColor(hex: lightHex)
86 | }
87 | })
88 | } else {
89 | self.init(hex: lightHex)
90 | }
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/Tests/FloatingBottomSheetTests/FloatingBottomSheetTests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 | @testable import FloatingBottomSheet
3 |
4 | final class FloatingBottomSheetTests: XCTestCase {
5 | func testExample() throws {
6 | // XCTest Documentation
7 | // https://developer.apple.com/documentation/xctest
8 |
9 | // Defining Test Cases and Test Methods
10 | // https://developer.apple.com/documentation/xctest/defining_test_cases_and_test_methods
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/assets/floatingbottomsheet.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/OhKanghoon/FloatingBottomSheet/34f7d86712b19c894b76759796aa638cb09facbf/assets/floatingbottomsheet.gif
--------------------------------------------------------------------------------
/assets/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/OhKanghoon/FloatingBottomSheet/34f7d86712b19c894b76759796aa638cb09facbf/assets/logo.png
--------------------------------------------------------------------------------