├── .gitignore ├── Assets └── feature-graphic.png ├── LICENSE.md ├── Package.swift ├── README.md ├── Sources └── BottomSheet │ ├── Modifiers │ └── BottomSheetModifier.swift │ └── View Controllers │ └── BottomSheetViewController.swift └── Tests └── BottomSheetTests └── BottomSheetTests.swift /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.toptal.com/developers/gitignore/api/swift,swiftpm,xcode 3 | # Edit at https://www.toptal.com/developers/gitignore?templates=swift,swiftpm,xcode 4 | 5 | ### macOS ### 6 | # General 7 | .DS_Store 8 | .AppleDouble 9 | .LSOverride 10 | 11 | # Icon must end with two \r 12 | Icon 13 | 14 | 15 | # Thumbnails 16 | ._* 17 | 18 | # Files that might appear in the root of a volume 19 | .DocumentRevisions-V100 20 | .fseventsd 21 | .Spotlight-V100 22 | .TemporaryItems 23 | .Trashes 24 | .VolumeIcon.icns 25 | .com.apple.timemachine.donotpresent 26 | 27 | # Directories potentially created on remote AFP share 28 | .AppleDB 29 | .AppleDesktop 30 | Network Trash Folder 31 | Temporary Items 32 | .apdisk 33 | 34 | ### OSX ### 35 | # General 36 | 37 | # Icon must end with two \r 38 | 39 | # Thumbnails 40 | 41 | # Files that might appear in the root of a volume 42 | 43 | # Directories potentially created on remote AFP share 44 | 45 | 46 | ### Swift ### 47 | # Xcode 48 | # 49 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 50 | 51 | ## User settings 52 | xcuserdata/ 53 | *.xcuserdata 54 | *.xcuserstate 55 | 56 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 57 | *.xcscmblueprint 58 | *.xccheckout 59 | 60 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 61 | build/ 62 | DerivedData/ 63 | *.moved-aside 64 | *.pbxuser 65 | !default.pbxuser 66 | *.mode1v3 67 | !default.mode1v3 68 | *.mode2v3 69 | !default.mode2v3 70 | *.perspectivev3 71 | !default.perspectivev3 72 | 73 | ## Obj-C/Swift specific 74 | *.hmap 75 | 76 | ## App packaging 77 | *.ipa 78 | *.dSYM.zip 79 | *.dSYM 80 | 81 | ## Playgrounds 82 | timeline.xctimeline 83 | playground.xcworkspace 84 | 85 | # Swift Package Manager 86 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 87 | # Packages/ 88 | # Package.pins 89 | # Package.resolved 90 | # *.xcodeproj 91 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 92 | # hence it is not needed unless you have added a package configuration file to your project 93 | .swiftpm 94 | 95 | .build/ 96 | 97 | # CocoaPods 98 | # We recommend against adding the Pods directory to your .gitignore. However 99 | # you should judge for yourself, the pros and cons are mentioned at: 100 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 101 | # Pods/ 102 | # Add this line if you want to avoid checking in source code from the Xcode workspace 103 | # *.xcworkspace 104 | 105 | # Carthage 106 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 107 | # Carthage/Checkouts 108 | 109 | Carthage/Build/ 110 | 111 | # Add this lines if you are using Accio dependency management (Deprecated since Xcode 12) 112 | # Dependencies/ 113 | # .accio/ 114 | 115 | # fastlane 116 | # It is recommended to not store the screenshots in the git repo. 117 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 118 | # For more information about the recommended setup visit: 119 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 120 | 121 | fastlane/report.xml 122 | fastlane/Preview.html 123 | fastlane/screenshots/**/*.png 124 | fastlane/test_output 125 | 126 | # Code Injection 127 | # After new code Injection tools there's a generated folder /iOSInjectionProject 128 | # https://github.com/johnno1962/injectionforxcode 129 | 130 | iOSInjectionProject/ 131 | 132 | ### SwiftPM ### 133 | Packages 134 | xcuserdata 135 | *.xcodeproj 136 | 137 | 138 | ### Xcode ### 139 | # Xcode 140 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 141 | 142 | 143 | ## Gcc Patch 144 | /*.gcno 145 | 146 | ### Xcode Patch ### 147 | *.xcodeproj/* 148 | !*.xcodeproj/project.pbxproj 149 | !*.xcodeproj/xcshareddata/ 150 | !*.xcworkspace/contents.xcworkspacedata 151 | **/xcshareddata/WorkspaceSettings.xcsettings 152 | 153 | # End of https://www.toptal.com/developers/gitignore/api/swift,swiftpm,xcode -------------------------------------------------------------------------------- /Assets/feature-graphic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamfootdev/BottomSheet/0e6db002eeb5e3ba402e94dfe6a120f78b5a64df/Assets/feature-graphic.png -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Adam Foot 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.5 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "BottomSheet", 8 | platforms: [ 9 | .iOS("13.0"), 10 | .macCatalyst("13.0") 11 | ], 12 | products: [ 13 | // Products define the executables and libraries a package produces, and make them visible to other packages. 14 | .library( 15 | name: "BottomSheet", 16 | targets: ["BottomSheet"]), 17 | ], 18 | dependencies: [ 19 | // Dependencies declare other packages that this package depends on. 20 | // .package(url: /* package url */, from: "1.0.0"), 21 | ], 22 | targets: [ 23 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 24 | // Targets can depend on other targets in this package, and on products in packages this package depends on. 25 | .target( 26 | name: "BottomSheet", 27 | dependencies: []), 28 | .testTarget( 29 | name: "BottomSheetTests", 30 | dependencies: ["BottomSheet"]), 31 | ] 32 | ) 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BottomSheet 2 | 3 | ![Feature Graphic](https://github.com/adamfootdev/BottomSheet/blob/main/Assets/feature-graphic.png?raw=true) 4 | ![Platform](https://img.shields.io/badge/platforms-iOS%2015.0-F28D00.svg) 5 | 6 | BottomSheet makes it easy to take advantage of the new UISheetPresentationController in SwiftUI with a simple .bottomSheet modifier on existing views. 7 | 8 | 1. [Requirements](#requirements) 9 | 2. [Integration](#integration) 10 | 3. [Usage](#usage) 11 | - [Presenting the Sheet](#presenting-the-sheet) 12 | - [Customizing the Sheet](#customizing-the-sheet) 13 | 14 | ## Requirements 15 | 16 | - iOS 15+ 17 | - Xcode 13+ 18 | 19 | ## Integration 20 | 21 | ### Swift Package Manager 22 | 23 | BottomSheet can be added to your app via Swift Package Manager in Xcode using the following configuration: 24 | 25 | ```swift 26 | dependencies: [ 27 | .package(url: "https://github.com/adamfootdev/BottomSheet.git", from: "0.1.3") 28 | ] 29 | ``` 30 | 31 | ## Usage 32 | 33 | To get started with BottomSheet, you'll need to import the framework first: 34 | 35 | ```swift 36 | import BottomSheet 37 | ``` 38 | 39 | ### Presenting the Sheet 40 | 41 | You can then apply the .bottomSheet modifier to any SwiftUI view, ensuring you attach a binding to the isPresented property - just like the standard .sheet modifier: 42 | 43 | ```swift 44 | .bottomSheet(isPresented: $isPresented) { 45 | Text("Hello, world!") 46 | } 47 | ``` 48 | 49 | BottomSheet also supports passing an Optional item to it, displaying the sheet if the item is not nil: 50 | 51 | ```swift 52 | .bottomSheet(item: $item) { 53 | Text("Hello, world!") 54 | } 55 | ``` 56 | 57 | ### Customizing the Sheet 58 | 59 | BottomSheet can be customized in the same way a UISheetPresentationController can be customized in UIKit. This is done by specifying additional properties in the modifier: 60 | 61 | ```swift 62 | .bottomSheet( 63 | isPresented: $isPresented, 64 | detents: [.medium(), .large()], 65 | largestUndimmedDetentIdentifier: .large, 66 | prefersGrabberVisible: true, 67 | prefersScrollingExpandsWhenScrolledToEdge: true, 68 | prefersEdgeAttachedInCompactHeight: false, 69 | widthFollowsPreferredContentSizeWhenEdgeAttached: false, 70 | onDismiss: { print("Dismissed") } 71 | ) { 72 | Text("Hello, world!") 73 | } 74 | ``` 75 | 76 | For more information on UISheetPresentationController, read Apple's documentation: https://developer.apple.com/documentation/uikit/uisheetpresentationcontroller 77 | 78 | 79 | Please note BottomSheet is currently missing the ability to be resized programmatically as the delegate doesn't work in iOS 15 beta 1. You may also run into issues when used on an iPad with multi-window support and have multiple instances of the same app side-by-side running. 80 | -------------------------------------------------------------------------------- /Sources/BottomSheet/Modifiers/BottomSheetModifier.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BottomSheetModifier.swift 3 | // BottomSheet 4 | // 5 | // Created by Adam Foot on 16/06/2021. 6 | // 7 | 8 | import SwiftUI 9 | 10 | @available(iOS 15, *) 11 | struct BottomSheet: ViewModifier { 12 | @Binding private var isPresented: Bool 13 | 14 | private let detents: [UISheetPresentationController.Detent] 15 | private let largestUndimmedDetentIdentifier: UISheetPresentationController.Detent.Identifier? 16 | private let prefersGrabberVisible: Bool 17 | private let prefersScrollingExpandsWhenScrolledToEdge: Bool 18 | private let prefersEdgeAttachedInCompactHeight: Bool 19 | @Binding private var selectedDetentIdentifier: UISheetPresentationController.Detent.Identifier? 20 | private let widthFollowsPreferredContentSizeWhenEdgeAttached: Bool 21 | private let isModalInPresentation: Bool 22 | private var onDismiss: (() -> Void)? 23 | private let contentView: () -> ContentView 24 | 25 | @State private var bottomSheetViewController: BottomSheetViewController? 26 | 27 | init( 28 | isPresented: Binding, 29 | detents: [UISheetPresentationController.Detent] = [.medium(), .large()], 30 | largestUndimmedDetentIdentifier: UISheetPresentationController.Detent.Identifier? = nil, 31 | prefersGrabberVisible: Bool = false, 32 | prefersScrollingExpandsWhenScrolledToEdge: Bool = true, 33 | prefersEdgeAttachedInCompactHeight: Bool = false, 34 | selectedDetentIdentifier: Binding = Binding.constant(nil), 35 | widthFollowsPreferredContentSizeWhenEdgeAttached: Bool = false, 36 | isModalInPresentation: Bool = false, 37 | onDismiss: (() -> Void)? = nil, 38 | @ViewBuilder contentView: @escaping () -> ContentView 39 | ) { 40 | _isPresented = isPresented 41 | self.detents = detents 42 | self.largestUndimmedDetentIdentifier = largestUndimmedDetentIdentifier 43 | self.prefersGrabberVisible = prefersGrabberVisible 44 | self.prefersScrollingExpandsWhenScrolledToEdge = prefersScrollingExpandsWhenScrolledToEdge 45 | self.prefersEdgeAttachedInCompactHeight = prefersEdgeAttachedInCompactHeight 46 | self._selectedDetentIdentifier = selectedDetentIdentifier 47 | self.widthFollowsPreferredContentSizeWhenEdgeAttached = widthFollowsPreferredContentSizeWhenEdgeAttached 48 | self.isModalInPresentation = isModalInPresentation 49 | self.contentView = contentView 50 | self.onDismiss = onDismiss 51 | } 52 | 53 | init( 54 | item: Binding, 55 | detents: [UISheetPresentationController.Detent] = [.medium(), .large()], 56 | largestUndimmedDetentIdentifier: UISheetPresentationController.Detent.Identifier? = nil, 57 | prefersGrabberVisible: Bool = false, 58 | prefersScrollingExpandsWhenScrolledToEdge: Bool = true, 59 | prefersEdgeAttachedInCompactHeight: Bool = false, 60 | selectedDetentIdentifier: Binding = Binding.constant(nil), 61 | widthFollowsPreferredContentSizeWhenEdgeAttached: Bool = false, 62 | isModalInPresentation: Bool = false, 63 | onDismiss: (() -> Void)? = nil, 64 | @ViewBuilder contentView: @escaping () -> ContentView 65 | ) { 66 | self._isPresented = Binding(get: { 67 | item.wrappedValue != nil 68 | }, set: { newValue in 69 | item.wrappedValue = nil 70 | }) 71 | self.detents = detents 72 | self.largestUndimmedDetentIdentifier = largestUndimmedDetentIdentifier 73 | self.prefersGrabberVisible = prefersGrabberVisible 74 | self.prefersScrollingExpandsWhenScrolledToEdge = prefersScrollingExpandsWhenScrolledToEdge 75 | self.prefersEdgeAttachedInCompactHeight = prefersEdgeAttachedInCompactHeight 76 | self._selectedDetentIdentifier = selectedDetentIdentifier 77 | self.widthFollowsPreferredContentSizeWhenEdgeAttached = widthFollowsPreferredContentSizeWhenEdgeAttached 78 | self.isModalInPresentation = isModalInPresentation 79 | self.contentView = contentView 80 | } 81 | 82 | func body(content: Content) -> some View { 83 | content 84 | .onChange(of: isPresented, perform: updatePresentation) 85 | .onChange(of: selectedDetentIdentifier, perform: updateSelectedDetentIdentifier) 86 | } 87 | 88 | private func updatePresentation(_ isPresented: Bool) { 89 | guard let windowScene = UIApplication.shared.connectedScenes.first(where: { 90 | $0.activationState == .foregroundActive 91 | }) as? UIWindowScene else { return } 92 | 93 | 94 | guard let root = windowScene.keyWindow?.rootViewController else { return } 95 | var controllerToPresentFrom = root 96 | while let presented = controllerToPresentFrom.presentedViewController { 97 | controllerToPresentFrom = presented 98 | } 99 | 100 | if isPresented { 101 | bottomSheetViewController = BottomSheetViewController( 102 | isPresented: $isPresented, 103 | detents: detents, 104 | largestUndimmedDetentIdentifier: largestUndimmedDetentIdentifier, 105 | prefersGrabberVisible: prefersGrabberVisible, 106 | prefersScrollingExpandsWhenScrolledToEdge: prefersScrollingExpandsWhenScrolledToEdge, 107 | prefersEdgeAttachedInCompactHeight: prefersEdgeAttachedInCompactHeight, 108 | selectedDetentIdentifier: $selectedDetentIdentifier, 109 | widthFollowsPreferredContentSizeWhenEdgeAttached: widthFollowsPreferredContentSizeWhenEdgeAttached, 110 | isModalInPresentation: isModalInPresentation, 111 | content: contentView() 112 | ) 113 | 114 | controllerToPresentFrom.present(bottomSheetViewController!, animated: true) 115 | 116 | } else { 117 | onDismiss?() 118 | bottomSheetViewController?.dismiss(animated: true) 119 | } 120 | } 121 | 122 | private func updateSelectedDetentIdentifier(_ selectedDetentIdentifier: UISheetPresentationController.Detent.Identifier?) { 123 | bottomSheetViewController?.updateSelectedDetentIdentifier(selectedDetentIdentifier) 124 | } 125 | } 126 | 127 | @available(iOS 15, *) 128 | extension View { 129 | 130 | /// Presents a bottom sheet when the binding to a Boolean value you provide is true. The bottom sheet 131 | /// can also be customised in the same way as a UISheetPresentationController can be. 132 | /// - Parameters: 133 | /// - isPresented: A binding to a Boolean value that determines whether to present the sheet that you create in the modifier’s content closure. 134 | /// - detents: An array containing all of the possible sizes for the sheet. This array must contain at least one element. When you set this value, specify detents in order from smallest to largest height. 135 | /// - largestUndimmedDetentIdentifier: The largest detent that doesn't dim the view underneath the sheet. 136 | /// - prefersGrabberVisible: A Boolean value that determines whether the sheet shows a grabber at the top. 137 | /// - prefersScrollingExpandsWhenScrolledToEdge: A Boolean value that determines whether scrolling expands the sheet to a larger detent. 138 | /// - prefersEdgeAttachedInCompactHeight: A Boolean value that determines whether the sheet attaches to the bottom edge of the screen in a compact-height size class. 139 | /// - selectedDetentIdentifier: A binding to a identifier of the most recent detent that the user selected or that you set programmatically. 140 | /// - widthFollowsPreferredContentSizeWhenEdgeAttached: A Boolean value that determines whether the sheet's width matches its view controller's preferred content size. 141 | /// - isModalInPresentation: A Boolean value indicating whether the view controller enforces a modal behavior. 142 | /// - onDismiss: The closure to execute when dismissing the sheet. 143 | /// - contentView: A closure that returns the content of the sheet. 144 | public func bottomSheet( 145 | isPresented: Binding, 146 | detents: [UISheetPresentationController.Detent] = [.medium(), .large()], 147 | largestUndimmedDetentIdentifier: UISheetPresentationController.Detent.Identifier? = nil, 148 | prefersGrabberVisible: Bool = false, 149 | prefersScrollingExpandsWhenScrolledToEdge: Bool = true, 150 | prefersEdgeAttachedInCompactHeight: Bool = false, 151 | selectedDetentIdentifier: Binding = Binding.constant(nil), 152 | widthFollowsPreferredContentSizeWhenEdgeAttached: Bool = false, 153 | isModalInPresentation: Bool = false, 154 | onDismiss: (() -> Void)? = nil, 155 | @ViewBuilder contentView: @escaping () -> ContentView 156 | ) -> some View { 157 | self.modifier( 158 | BottomSheet( 159 | isPresented: isPresented, 160 | detents: detents, 161 | largestUndimmedDetentIdentifier: largestUndimmedDetentIdentifier, prefersGrabberVisible: prefersGrabberVisible, 162 | prefersScrollingExpandsWhenScrolledToEdge: prefersScrollingExpandsWhenScrolledToEdge, 163 | prefersEdgeAttachedInCompactHeight: prefersEdgeAttachedInCompactHeight, 164 | selectedDetentIdentifier: selectedDetentIdentifier, 165 | widthFollowsPreferredContentSizeWhenEdgeAttached: widthFollowsPreferredContentSizeWhenEdgeAttached, 166 | isModalInPresentation: isModalInPresentation, 167 | onDismiss: onDismiss, 168 | contentView: contentView 169 | ) 170 | ) 171 | } 172 | 173 | /// Presents a bottom sheet when the binding to an Optinal item you pass to it is not nil. The bottom sheet 174 | /// can also be customised in the same way as a UISheetPresentationController can be. 175 | /// - Parameters: 176 | /// - item: A binding to an Optional item that determines whether to present the sheet that you create in the modifier’s content closure. 177 | /// - detents: An array containing all of the possible sizes for the sheet. This array must contain at least one element. When you set this value, specify detents in order from smallest to largest height. 178 | /// - largestUndimmedDetentIdentifier: The largest detent that doesn't dim the view underneath the sheet. 179 | /// - prefersGrabberVisible: A Boolean value that determines whether the sheet shows a grabber at the top. 180 | /// - prefersScrollingExpandsWhenScrolledToEdge: A Boolean value that determines whether scrolling expands the sheet to a larger detent. 181 | /// - prefersEdgeAttachedInCompactHeight: A Boolean value that determines whether the sheet attaches to the bottom edge of the screen in a compact-height size class. 182 | /// - selectedDetentIdentifier: A binding to a identifier of the most recent detent that the user selected or that you set programmatically. 183 | /// - widthFollowsPreferredContentSizeWhenEdgeAttached: A Boolean value that determines whether the sheet's width matches its view controller's preferred content size. 184 | /// - isModalInPresentation: A Boolean value indicating whether the view controller enforces a modal behavior. 185 | /// - onDismiss: The closure to execute when dismissing the sheet. 186 | /// - contentView: A closure that returns the content of the sheet. 187 | public func bottomSheet( 188 | item: Binding, 189 | detents: [UISheetPresentationController.Detent] = [.medium(), .large()], 190 | largestUndimmedDetentIdentifier: UISheetPresentationController.Detent.Identifier? = nil, 191 | prefersGrabberVisible: Bool = false, 192 | prefersScrollingExpandsWhenScrolledToEdge: Bool = true, 193 | prefersEdgeAttachedInCompactHeight: Bool = false, 194 | selectedDetentIdentifier: Binding = Binding.constant(nil), 195 | widthFollowsPreferredContentSizeWhenEdgeAttached: Bool = false, 196 | isModalInPresentation: Bool = false, 197 | onDismiss: (() -> Void)? = nil, 198 | @ViewBuilder contentView: @escaping () -> ContentView 199 | ) -> some View { 200 | self.modifier( 201 | BottomSheet( 202 | item: item, 203 | detents: detents, 204 | largestUndimmedDetentIdentifier: largestUndimmedDetentIdentifier, prefersGrabberVisible: prefersGrabberVisible, 205 | prefersScrollingExpandsWhenScrolledToEdge: prefersScrollingExpandsWhenScrolledToEdge, 206 | prefersEdgeAttachedInCompactHeight: prefersEdgeAttachedInCompactHeight, 207 | selectedDetentIdentifier: selectedDetentIdentifier, 208 | widthFollowsPreferredContentSizeWhenEdgeAttached: widthFollowsPreferredContentSizeWhenEdgeAttached, 209 | isModalInPresentation: isModalInPresentation, 210 | onDismiss: onDismiss, 211 | contentView: contentView 212 | ) 213 | ) 214 | } 215 | } 216 | 217 | -------------------------------------------------------------------------------- /Sources/BottomSheet/View Controllers/BottomSheetViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BottomSheetViewController.swift 3 | // BottomSheet 4 | // 5 | // Created by Adam Foot on 16/06/2021. 6 | // 7 | 8 | import UIKit 9 | import SwiftUI 10 | import Combine 11 | 12 | @available(iOS 15, *) 13 | class BottomSheetViewController: UIViewController, UISheetPresentationControllerDelegate { 14 | @Binding private var isPresented: Bool 15 | 16 | private let detents: [UISheetPresentationController.Detent] 17 | private let largestUndimmedDetentIdentifier: UISheetPresentationController.Detent.Identifier? 18 | private let prefersGrabberVisible: Bool 19 | private let prefersScrollingExpandsWhenScrolledToEdge: Bool 20 | private let prefersEdgeAttachedInCompactHeight: Bool 21 | @Binding private var selectedDetentIdentifier: UISheetPresentationController.Detent.Identifier? 22 | private let widthFollowsPreferredContentSizeWhenEdgeAttached: Bool 23 | 24 | private let contentView: UIHostingController 25 | 26 | init( 27 | isPresented: Binding, 28 | detents: [UISheetPresentationController.Detent] = [.medium(), .large()], 29 | largestUndimmedDetentIdentifier: UISheetPresentationController.Detent.Identifier? = nil, 30 | prefersGrabberVisible: Bool = false, 31 | prefersScrollingExpandsWhenScrolledToEdge: Bool = true, 32 | prefersEdgeAttachedInCompactHeight: Bool = false, 33 | selectedDetentIdentifier: Binding = Binding.constant(nil), 34 | widthFollowsPreferredContentSizeWhenEdgeAttached: Bool = false, 35 | isModalInPresentation: Bool = false, 36 | content: Content 37 | ) { 38 | _isPresented = isPresented 39 | 40 | self.detents = detents 41 | self.largestUndimmedDetentIdentifier = largestUndimmedDetentIdentifier 42 | self.prefersGrabberVisible = prefersGrabberVisible 43 | self.prefersScrollingExpandsWhenScrolledToEdge = prefersScrollingExpandsWhenScrolledToEdge 44 | self.prefersEdgeAttachedInCompactHeight = prefersEdgeAttachedInCompactHeight 45 | self._selectedDetentIdentifier = selectedDetentIdentifier 46 | self.widthFollowsPreferredContentSizeWhenEdgeAttached = widthFollowsPreferredContentSizeWhenEdgeAttached 47 | 48 | self.contentView = UIHostingController(rootView: content) 49 | 50 | super.init(nibName: nil, bundle: nil) 51 | self.isModalInPresentation = isModalInPresentation 52 | } 53 | 54 | required init?(coder: NSCoder) { 55 | fatalError("init(coder:) has not been implemented") 56 | } 57 | 58 | override func viewDidLoad() { 59 | super.viewDidLoad() 60 | 61 | addChild(contentView) 62 | view.addSubview(contentView.view) 63 | 64 | contentView.view.translatesAutoresizingMaskIntoConstraints = false 65 | 66 | NSLayoutConstraint.activate([ 67 | contentView.view.topAnchor.constraint(equalTo: view.topAnchor), 68 | contentView.view.bottomAnchor.constraint(equalTo: view.bottomAnchor), 69 | contentView.view.leadingAnchor.constraint(equalTo: view.leadingAnchor), 70 | contentView.view.trailingAnchor.constraint(equalTo: view.trailingAnchor) 71 | ]) 72 | 73 | if let presentationController = presentationController as? UISheetPresentationController { 74 | presentationController.detents = detents 75 | presentationController.largestUndimmedDetentIdentifier = largestUndimmedDetentIdentifier 76 | presentationController.prefersGrabberVisible = prefersGrabberVisible 77 | presentationController.prefersScrollingExpandsWhenScrolledToEdge = prefersScrollingExpandsWhenScrolledToEdge 78 | presentationController.prefersEdgeAttachedInCompactHeight = prefersEdgeAttachedInCompactHeight 79 | presentationController.selectedDetentIdentifier = selectedDetentIdentifier 80 | presentationController.widthFollowsPreferredContentSizeWhenEdgeAttached = widthFollowsPreferredContentSizeWhenEdgeAttached 81 | presentationController.delegate = self 82 | } 83 | } 84 | 85 | override func viewDidDisappear(_ animated: Bool) { 86 | super.viewDidDisappear(animated) 87 | 88 | isPresented = false 89 | } 90 | 91 | func updateSelectedDetentIdentifier(_ selectedDetentIdentifier: UISheetPresentationController.Detent.Identifier?) { 92 | self.sheetPresentationController?.animateChanges { 93 | self.sheetPresentationController?.selectedDetentIdentifier = selectedDetentIdentifier 94 | } 95 | } 96 | 97 | func sheetPresentationControllerDidChangeSelectedDetentIdentifier(_ sheetPresentationController: UISheetPresentationController) { 98 | self.selectedDetentIdentifier = sheetPresentationController.selectedDetentIdentifier 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /Tests/BottomSheetTests/BottomSheetTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import BottomSheet 3 | 4 | final class BottomSheetTests: XCTestCase { 5 | } 6 | --------------------------------------------------------------------------------