├── .gitignore ├── LICENSE ├── Sheet.xcodeproj ├── project.pbxproj └── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ └── IDEWorkspaceChecks.plist └── Sheet ├── Animation └── FluidTimingCurve.swift ├── AppDelegate.swift ├── Assets.xcassets ├── AppIcon.appiconset │ └── Contents.json └── Contents.json ├── Base.lproj ├── LaunchScreen.storyboard └── Main.storyboard ├── Bottom Sheet ├── Chrome │ └── SheetViewController.swift └── Presentation │ ├── SheetContainerViewController.swift │ ├── SheetPresentationWindow.swift │ └── SheetPresenter.swift ├── Info.plist └── ViewController.swift /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata/ 19 | 20 | ## Other 21 | *.moved-aside 22 | *.xcuserstate 23 | .DS_Store 24 | *.xcscmblueprint 25 | 26 | ## Obj-C/Swift specific 27 | *.hmap 28 | *.ipa 29 | *.dSYM.zip 30 | *.dSYM 31 | 32 | # CocoaPods 33 | # 34 | # We recommend against adding the Pods directory to your .gitignore. However 35 | # you should judge for yourself, the pros and cons are mentioned at: 36 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 37 | # 38 | Pods/ 39 | 40 | # Carthage 41 | # 42 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 43 | # Carthage/Checkouts 44 | 45 | Carthage/Build 46 | 47 | # fastlane 48 | # 49 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 50 | # screenshots whenever they are needed. 51 | # For more information about the recommended setup visit: 52 | # https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Gitignore.md 53 | 54 | fastlane/report.xml 55 | fastlane/screenshots 56 | fastlane/Preview.html 57 | fastlane/test_output 58 | 59 | # Code Injection 60 | # 61 | # After new code Injection tools there's a generated folder /iOSInjectionProject 62 | # https://github.com/johnno1962/injectionforxcode 63 | 64 | iOSInjectionProject/ 65 | 66 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 67 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 68 | 69 | # User-specific stuff: 70 | .idea/ 71 | 72 | # Gradle: 73 | .idea/gradle.xml 74 | .idea/libraries 75 | 76 | # Mongo Explorer plugin: 77 | .idea/mongoSettings.xml 78 | 79 | ## File-based project format: 80 | *.iws 81 | 82 | ## Plugin-specific files: 83 | 84 | # IntelliJ 85 | /out/ 86 | 87 | # mpeltonen/sbt-idea plugin 88 | .idea_modules/ 89 | 90 | # JIRA plugin 91 | atlassian-ide-plugin.xml 92 | 93 | # Crashlytics plugin (for Android Studio and IntelliJ) 94 | com_crashlytics_export_strings.xml 95 | crashlytics.properties 96 | crashlytics-build.properties 97 | fabric.properties 98 | 99 | # SVN 100 | .svn/ 101 | 102 | *.xcbkptlist 103 | 104 | Vendor/Dependencies 105 | 106 | *.todo 107 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019 Guilherme Rambo 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are met: 5 | 6 | - Redistributions of source code must retain the above copyright notice, this 7 | list of conditions and the following disclaimer. 8 | 9 | - Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation 11 | and/or other materials provided with the distribution. 12 | 13 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 14 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 15 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 16 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 17 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 18 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 19 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 20 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 21 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 22 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /Sheet.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 50; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | DDF6E75A2316CA9000251A21 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDF6E7592316CA9000251A21 /* AppDelegate.swift */; }; 11 | DDF6E75C2316CA9000251A21 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDF6E75B2316CA9000251A21 /* ViewController.swift */; }; 12 | DDF6E75F2316CA9000251A21 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = DDF6E75D2316CA9000251A21 /* Main.storyboard */; }; 13 | DDF6E7612316CA9100251A21 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DDF6E7602316CA9100251A21 /* Assets.xcassets */; }; 14 | DDF6E7642316CA9100251A21 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = DDF6E7622316CA9100251A21 /* LaunchScreen.storyboard */; }; 15 | DDF6E7742316CAA300251A21 /* SheetViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDF6E76E2316CAA300251A21 /* SheetViewController.swift */; }; 16 | DDF6E7752316CAA300251A21 /* SheetPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDF6E7702316CAA300251A21 /* SheetPresenter.swift */; }; 17 | DDF6E7762316CAA300251A21 /* SheetPresentationWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDF6E7712316CAA300251A21 /* SheetPresentationWindow.swift */; }; 18 | DDF6E7772316CAA300251A21 /* SheetContainerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDF6E7722316CAA300251A21 /* SheetContainerViewController.swift */; }; 19 | DDF6E77F2316CAEE00251A21 /* FluidTimingCurve.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDF6E77B2316CAEE00251A21 /* FluidTimingCurve.swift */; }; 20 | /* End PBXBuildFile section */ 21 | 22 | /* Begin PBXFileReference section */ 23 | DDF6E7562316CA9000251A21 /* Sheet.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Sheet.app; sourceTree = BUILT_PRODUCTS_DIR; }; 24 | DDF6E7592316CA9000251A21 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 25 | DDF6E75B2316CA9000251A21 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 26 | DDF6E75E2316CA9000251A21 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 27 | DDF6E7602316CA9100251A21 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 28 | DDF6E7632316CA9100251A21 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 29 | DDF6E7652316CA9100251A21 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 30 | DDF6E76E2316CAA300251A21 /* SheetViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SheetViewController.swift; sourceTree = ""; }; 31 | DDF6E7702316CAA300251A21 /* SheetPresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SheetPresenter.swift; sourceTree = ""; }; 32 | DDF6E7712316CAA300251A21 /* SheetPresentationWindow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SheetPresentationWindow.swift; sourceTree = ""; }; 33 | DDF6E7722316CAA300251A21 /* SheetContainerViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SheetContainerViewController.swift; sourceTree = ""; }; 34 | DDF6E77B2316CAEE00251A21 /* FluidTimingCurve.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FluidTimingCurve.swift; sourceTree = ""; }; 35 | /* End PBXFileReference section */ 36 | 37 | /* Begin PBXFrameworksBuildPhase section */ 38 | DDF6E7532316CA9000251A21 /* Frameworks */ = { 39 | isa = PBXFrameworksBuildPhase; 40 | buildActionMask = 2147483647; 41 | files = ( 42 | ); 43 | runOnlyForDeploymentPostprocessing = 0; 44 | }; 45 | /* End PBXFrameworksBuildPhase section */ 46 | 47 | /* Begin PBXGroup section */ 48 | DDF6E74D2316CA8F00251A21 = { 49 | isa = PBXGroup; 50 | children = ( 51 | DDF6E7582316CA9000251A21 /* Sheet */, 52 | DDF6E7572316CA9000251A21 /* Products */, 53 | ); 54 | sourceTree = ""; 55 | }; 56 | DDF6E7572316CA9000251A21 /* Products */ = { 57 | isa = PBXGroup; 58 | children = ( 59 | DDF6E7562316CA9000251A21 /* Sheet.app */, 60 | ); 61 | name = Products; 62 | sourceTree = ""; 63 | }; 64 | DDF6E7582316CA9000251A21 /* Sheet */ = { 65 | isa = PBXGroup; 66 | children = ( 67 | DDF6E7782316CAEE00251A21 /* Animation */, 68 | DDF6E76B2316CAA300251A21 /* Bottom Sheet */, 69 | DDF6E7592316CA9000251A21 /* AppDelegate.swift */, 70 | DDF6E75B2316CA9000251A21 /* ViewController.swift */, 71 | DDF6E75D2316CA9000251A21 /* Main.storyboard */, 72 | DDF6E7602316CA9100251A21 /* Assets.xcassets */, 73 | DDF6E7622316CA9100251A21 /* LaunchScreen.storyboard */, 74 | DDF6E7652316CA9100251A21 /* Info.plist */, 75 | ); 76 | path = Sheet; 77 | sourceTree = ""; 78 | }; 79 | DDF6E76B2316CAA300251A21 /* Bottom Sheet */ = { 80 | isa = PBXGroup; 81 | children = ( 82 | DDF6E76C2316CAA300251A21 /* Chrome */, 83 | DDF6E76F2316CAA300251A21 /* Presentation */, 84 | ); 85 | path = "Bottom Sheet"; 86 | sourceTree = ""; 87 | }; 88 | DDF6E76C2316CAA300251A21 /* Chrome */ = { 89 | isa = PBXGroup; 90 | children = ( 91 | DDF6E76E2316CAA300251A21 /* SheetViewController.swift */, 92 | ); 93 | path = Chrome; 94 | sourceTree = ""; 95 | }; 96 | DDF6E76F2316CAA300251A21 /* Presentation */ = { 97 | isa = PBXGroup; 98 | children = ( 99 | DDF6E7702316CAA300251A21 /* SheetPresenter.swift */, 100 | DDF6E7712316CAA300251A21 /* SheetPresentationWindow.swift */, 101 | DDF6E7722316CAA300251A21 /* SheetContainerViewController.swift */, 102 | ); 103 | path = Presentation; 104 | sourceTree = ""; 105 | }; 106 | DDF6E7782316CAEE00251A21 /* Animation */ = { 107 | isa = PBXGroup; 108 | children = ( 109 | DDF6E77B2316CAEE00251A21 /* FluidTimingCurve.swift */, 110 | ); 111 | path = Animation; 112 | sourceTree = ""; 113 | }; 114 | /* End PBXGroup section */ 115 | 116 | /* Begin PBXNativeTarget section */ 117 | DDF6E7552316CA9000251A21 /* Sheet */ = { 118 | isa = PBXNativeTarget; 119 | buildConfigurationList = DDF6E7682316CA9100251A21 /* Build configuration list for PBXNativeTarget "Sheet" */; 120 | buildPhases = ( 121 | DDF6E7522316CA9000251A21 /* Sources */, 122 | DDF6E7532316CA9000251A21 /* Frameworks */, 123 | DDF6E7542316CA9000251A21 /* Resources */, 124 | ); 125 | buildRules = ( 126 | ); 127 | dependencies = ( 128 | ); 129 | name = Sheet; 130 | productName = Sheet; 131 | productReference = DDF6E7562316CA9000251A21 /* Sheet.app */; 132 | productType = "com.apple.product-type.application"; 133 | }; 134 | /* End PBXNativeTarget section */ 135 | 136 | /* Begin PBXProject section */ 137 | DDF6E74E2316CA8F00251A21 /* Project object */ = { 138 | isa = PBXProject; 139 | attributes = { 140 | LastSwiftUpdateCheck = 1030; 141 | LastUpgradeCheck = 1030; 142 | ORGANIZATIONNAME = "Guilherme Rambo"; 143 | TargetAttributes = { 144 | DDF6E7552316CA9000251A21 = { 145 | CreatedOnToolsVersion = 10.3; 146 | }; 147 | }; 148 | }; 149 | buildConfigurationList = DDF6E7512316CA8F00251A21 /* Build configuration list for PBXProject "Sheet" */; 150 | compatibilityVersion = "Xcode 9.3"; 151 | developmentRegion = en; 152 | hasScannedForEncodings = 0; 153 | knownRegions = ( 154 | en, 155 | Base, 156 | ); 157 | mainGroup = DDF6E74D2316CA8F00251A21; 158 | productRefGroup = DDF6E7572316CA9000251A21 /* Products */; 159 | projectDirPath = ""; 160 | projectRoot = ""; 161 | targets = ( 162 | DDF6E7552316CA9000251A21 /* Sheet */, 163 | ); 164 | }; 165 | /* End PBXProject section */ 166 | 167 | /* Begin PBXResourcesBuildPhase section */ 168 | DDF6E7542316CA9000251A21 /* Resources */ = { 169 | isa = PBXResourcesBuildPhase; 170 | buildActionMask = 2147483647; 171 | files = ( 172 | DDF6E7642316CA9100251A21 /* LaunchScreen.storyboard in Resources */, 173 | DDF6E7612316CA9100251A21 /* Assets.xcassets in Resources */, 174 | DDF6E75F2316CA9000251A21 /* Main.storyboard in Resources */, 175 | ); 176 | runOnlyForDeploymentPostprocessing = 0; 177 | }; 178 | /* End PBXResourcesBuildPhase section */ 179 | 180 | /* Begin PBXSourcesBuildPhase section */ 181 | DDF6E7522316CA9000251A21 /* Sources */ = { 182 | isa = PBXSourcesBuildPhase; 183 | buildActionMask = 2147483647; 184 | files = ( 185 | DDF6E7742316CAA300251A21 /* SheetViewController.swift in Sources */, 186 | DDF6E7752316CAA300251A21 /* SheetPresenter.swift in Sources */, 187 | DDF6E7762316CAA300251A21 /* SheetPresentationWindow.swift in Sources */, 188 | DDF6E77F2316CAEE00251A21 /* FluidTimingCurve.swift in Sources */, 189 | DDF6E7772316CAA300251A21 /* SheetContainerViewController.swift in Sources */, 190 | DDF6E75C2316CA9000251A21 /* ViewController.swift in Sources */, 191 | DDF6E75A2316CA9000251A21 /* AppDelegate.swift in Sources */, 192 | ); 193 | runOnlyForDeploymentPostprocessing = 0; 194 | }; 195 | /* End PBXSourcesBuildPhase section */ 196 | 197 | /* Begin PBXVariantGroup section */ 198 | DDF6E75D2316CA9000251A21 /* Main.storyboard */ = { 199 | isa = PBXVariantGroup; 200 | children = ( 201 | DDF6E75E2316CA9000251A21 /* Base */, 202 | ); 203 | name = Main.storyboard; 204 | sourceTree = ""; 205 | }; 206 | DDF6E7622316CA9100251A21 /* LaunchScreen.storyboard */ = { 207 | isa = PBXVariantGroup; 208 | children = ( 209 | DDF6E7632316CA9100251A21 /* Base */, 210 | ); 211 | name = LaunchScreen.storyboard; 212 | sourceTree = ""; 213 | }; 214 | /* End PBXVariantGroup section */ 215 | 216 | /* Begin XCBuildConfiguration section */ 217 | DDF6E7662316CA9100251A21 /* Debug */ = { 218 | isa = XCBuildConfiguration; 219 | buildSettings = { 220 | ALWAYS_SEARCH_USER_PATHS = NO; 221 | CLANG_ANALYZER_NONNULL = YES; 222 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 223 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 224 | CLANG_CXX_LIBRARY = "libc++"; 225 | CLANG_ENABLE_MODULES = YES; 226 | CLANG_ENABLE_OBJC_ARC = YES; 227 | CLANG_ENABLE_OBJC_WEAK = YES; 228 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 229 | CLANG_WARN_BOOL_CONVERSION = YES; 230 | CLANG_WARN_COMMA = YES; 231 | CLANG_WARN_CONSTANT_CONVERSION = YES; 232 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 233 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 234 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 235 | CLANG_WARN_EMPTY_BODY = YES; 236 | CLANG_WARN_ENUM_CONVERSION = YES; 237 | CLANG_WARN_INFINITE_RECURSION = YES; 238 | CLANG_WARN_INT_CONVERSION = YES; 239 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 240 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 241 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 242 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 243 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 244 | CLANG_WARN_STRICT_PROTOTYPES = YES; 245 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 246 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 247 | CLANG_WARN_UNREACHABLE_CODE = YES; 248 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 249 | CODE_SIGN_IDENTITY = "iPhone Developer"; 250 | COPY_PHASE_STRIP = NO; 251 | DEBUG_INFORMATION_FORMAT = dwarf; 252 | ENABLE_STRICT_OBJC_MSGSEND = YES; 253 | ENABLE_TESTABILITY = YES; 254 | GCC_C_LANGUAGE_STANDARD = gnu11; 255 | GCC_DYNAMIC_NO_PIC = NO; 256 | GCC_NO_COMMON_BLOCKS = YES; 257 | GCC_OPTIMIZATION_LEVEL = 0; 258 | GCC_PREPROCESSOR_DEFINITIONS = ( 259 | "DEBUG=1", 260 | "$(inherited)", 261 | ); 262 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 263 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 264 | GCC_WARN_UNDECLARED_SELECTOR = YES; 265 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 266 | GCC_WARN_UNUSED_FUNCTION = YES; 267 | GCC_WARN_UNUSED_VARIABLE = YES; 268 | IPHONEOS_DEPLOYMENT_TARGET = 12.4; 269 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 270 | MTL_FAST_MATH = YES; 271 | ONLY_ACTIVE_ARCH = YES; 272 | SDKROOT = iphoneos; 273 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 274 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 275 | }; 276 | name = Debug; 277 | }; 278 | DDF6E7672316CA9100251A21 /* Release */ = { 279 | isa = XCBuildConfiguration; 280 | buildSettings = { 281 | ALWAYS_SEARCH_USER_PATHS = NO; 282 | CLANG_ANALYZER_NONNULL = YES; 283 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 284 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 285 | CLANG_CXX_LIBRARY = "libc++"; 286 | CLANG_ENABLE_MODULES = YES; 287 | CLANG_ENABLE_OBJC_ARC = YES; 288 | CLANG_ENABLE_OBJC_WEAK = YES; 289 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 290 | CLANG_WARN_BOOL_CONVERSION = YES; 291 | CLANG_WARN_COMMA = YES; 292 | CLANG_WARN_CONSTANT_CONVERSION = YES; 293 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 294 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 295 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 296 | CLANG_WARN_EMPTY_BODY = YES; 297 | CLANG_WARN_ENUM_CONVERSION = YES; 298 | CLANG_WARN_INFINITE_RECURSION = YES; 299 | CLANG_WARN_INT_CONVERSION = YES; 300 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 301 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 302 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 303 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 304 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 305 | CLANG_WARN_STRICT_PROTOTYPES = YES; 306 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 307 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 308 | CLANG_WARN_UNREACHABLE_CODE = YES; 309 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 310 | CODE_SIGN_IDENTITY = "iPhone Developer"; 311 | COPY_PHASE_STRIP = NO; 312 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 313 | ENABLE_NS_ASSERTIONS = NO; 314 | ENABLE_STRICT_OBJC_MSGSEND = YES; 315 | GCC_C_LANGUAGE_STANDARD = gnu11; 316 | GCC_NO_COMMON_BLOCKS = YES; 317 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 318 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 319 | GCC_WARN_UNDECLARED_SELECTOR = YES; 320 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 321 | GCC_WARN_UNUSED_FUNCTION = YES; 322 | GCC_WARN_UNUSED_VARIABLE = YES; 323 | IPHONEOS_DEPLOYMENT_TARGET = 12.4; 324 | MTL_ENABLE_DEBUG_INFO = NO; 325 | MTL_FAST_MATH = YES; 326 | SDKROOT = iphoneos; 327 | SWIFT_COMPILATION_MODE = wholemodule; 328 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 329 | VALIDATE_PRODUCT = YES; 330 | }; 331 | name = Release; 332 | }; 333 | DDF6E7692316CA9100251A21 /* Debug */ = { 334 | isa = XCBuildConfiguration; 335 | buildSettings = { 336 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 337 | CODE_SIGN_STYLE = Automatic; 338 | DEVELOPMENT_TEAM = 6ABEVHK7CE; 339 | INFOPLIST_FILE = Sheet/Info.plist; 340 | IPHONEOS_DEPLOYMENT_TARGET = 12.0; 341 | LD_RUNPATH_SEARCH_PATHS = ( 342 | "$(inherited)", 343 | "@executable_path/Frameworks", 344 | ); 345 | PRODUCT_BUNDLE_IDENTIFIER = codes.rambo.Sheet; 346 | PRODUCT_NAME = "$(TARGET_NAME)"; 347 | SWIFT_VERSION = 5.0; 348 | TARGETED_DEVICE_FAMILY = "1,2"; 349 | }; 350 | name = Debug; 351 | }; 352 | DDF6E76A2316CA9100251A21 /* Release */ = { 353 | isa = XCBuildConfiguration; 354 | buildSettings = { 355 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 356 | CODE_SIGN_STYLE = Automatic; 357 | DEVELOPMENT_TEAM = 6ABEVHK7CE; 358 | INFOPLIST_FILE = Sheet/Info.plist; 359 | IPHONEOS_DEPLOYMENT_TARGET = 12.0; 360 | LD_RUNPATH_SEARCH_PATHS = ( 361 | "$(inherited)", 362 | "@executable_path/Frameworks", 363 | ); 364 | PRODUCT_BUNDLE_IDENTIFIER = codes.rambo.Sheet; 365 | PRODUCT_NAME = "$(TARGET_NAME)"; 366 | SWIFT_VERSION = 5.0; 367 | TARGETED_DEVICE_FAMILY = "1,2"; 368 | }; 369 | name = Release; 370 | }; 371 | /* End XCBuildConfiguration section */ 372 | 373 | /* Begin XCConfigurationList section */ 374 | DDF6E7512316CA8F00251A21 /* Build configuration list for PBXProject "Sheet" */ = { 375 | isa = XCConfigurationList; 376 | buildConfigurations = ( 377 | DDF6E7662316CA9100251A21 /* Debug */, 378 | DDF6E7672316CA9100251A21 /* Release */, 379 | ); 380 | defaultConfigurationIsVisible = 0; 381 | defaultConfigurationName = Release; 382 | }; 383 | DDF6E7682316CA9100251A21 /* Build configuration list for PBXNativeTarget "Sheet" */ = { 384 | isa = XCConfigurationList; 385 | buildConfigurations = ( 386 | DDF6E7692316CA9100251A21 /* Debug */, 387 | DDF6E76A2316CA9100251A21 /* Release */, 388 | ); 389 | defaultConfigurationIsVisible = 0; 390 | defaultConfigurationName = Release; 391 | }; 392 | /* End XCConfigurationList section */ 393 | }; 394 | rootObject = DDF6E74E2316CA8F00251A21 /* Project object */; 395 | } 396 | -------------------------------------------------------------------------------- /Sheet.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Sheet.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Sheet/Animation/FluidTimingCurve.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FluidTimingCurve.swift 3 | // Sheet 4 | // 5 | // Created by Guilherme Rambo on 05/08/19. 6 | // Copyright © 2019 Guilherme Rambo. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public final class FluidTimingCurve: NSObject, UITimingCurveProvider { 12 | 13 | public let initialVelocity: CGVector 14 | let mass: CGFloat 15 | let stiffness: CGFloat 16 | let damping: CGFloat 17 | 18 | public init(velocity: CGVector, stiffness: CGFloat = 400, damping: CGFloat = 30, mass: CGFloat = 1.0) { 19 | self.initialVelocity = velocity 20 | self.stiffness = stiffness 21 | self.damping = damping 22 | self.mass = mass 23 | 24 | super.init() 25 | } 26 | 27 | public func encode(with aCoder: NSCoder) { 28 | fatalError("Not supported") 29 | } 30 | 31 | public init?(coder aDecoder: NSCoder) { 32 | fatalError("Not supported") 33 | } 34 | 35 | public func copy(with zone: NSZone? = nil) -> Any { 36 | return FluidTimingCurve(velocity: initialVelocity) 37 | } 38 | 39 | public var timingCurveType: UITimingCurveType { 40 | return .composed 41 | } 42 | 43 | public var cubicTimingParameters: UICubicTimingParameters? { 44 | return .init(animationCurve: .easeIn) 45 | } 46 | 47 | public var springTimingParameters: UISpringTimingParameters? { 48 | return UISpringTimingParameters(mass: mass, stiffness: stiffness, damping: damping, initialVelocity: initialVelocity) 49 | } 50 | 51 | } 52 | 53 | public extension UISpringTimingParameters { 54 | 55 | /// A design-friendly way to create a spring timing curve. 56 | /// 57 | /// - Parameters: 58 | /// - damping: The 'bounciness' of the animation. Value must be between 0 and 1. 59 | /// - response: The 'speed' of the animation. 60 | /// - initialVelocity: The vector describing the starting motion of the property. Optional, default is `.zero`. 61 | convenience init(damping: CGFloat, response: CGFloat, initialVelocity: CGVector = .zero) { 62 | let stiffness = pow(2 * .pi / response, 2) 63 | let damp = 4 * .pi * damping / response 64 | self.init(mass: 1, stiffness: stiffness, damping: damp, initialVelocity: initialVelocity) 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /Sheet/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // Sheet 4 | // 5 | // Created by Guilherme Rambo on 28/08/19. 6 | // Copyright © 2019 Guilherme Rambo. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | 17 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 18 | // Override point for customization after application launch. 19 | return true 20 | } 21 | 22 | func applicationWillResignActive(_ application: UIApplication) { 23 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 24 | // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. 25 | } 26 | 27 | func applicationDidEnterBackground(_ application: UIApplication) { 28 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 29 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 30 | } 31 | 32 | func applicationWillEnterForeground(_ application: UIApplication) { 33 | // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. 34 | } 35 | 36 | func applicationDidBecomeActive(_ application: UIApplication) { 37 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 38 | } 39 | 40 | func applicationWillTerminate(_ application: UIApplication) { 41 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 42 | } 43 | 44 | 45 | } 46 | 47 | -------------------------------------------------------------------------------- /Sheet/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /Sheet/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Sheet/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 | -------------------------------------------------------------------------------- /Sheet/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /Sheet/Bottom Sheet/Chrome/SheetViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SheetViewController.swift 3 | // Sheet 4 | // 5 | // Created by Guilherme Rambo on 05/08/19. 6 | // Copyright © 2019 Guilherme Rambo. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class SheetViewController: UIViewController { 12 | 13 | let metrics: SheetMetrics 14 | 15 | init(metrics: SheetMetrics) { 16 | self.metrics = metrics 17 | 18 | super.init(nibName: nil, bundle: nil) 19 | } 20 | 21 | required init?(coder: NSCoder) { 22 | fatalError() 23 | } 24 | 25 | private var container: SheetContainerViewController? { 26 | return parent as? SheetContainerViewController 27 | } 28 | 29 | var rubberBandingStartHandler: (() -> Void)? 30 | var rubberBandingUpdateHandler: ((CGFloat) -> Void)? 31 | var rubberBandingFinishedHandler: (() -> Void)? 32 | 33 | var isScrollingEnabled = true 34 | 35 | private let scrollViewAtTheTopDeltaThreshold: CGFloat = 3 36 | 37 | var isScrollViewAtTheTop: Bool { 38 | return abs(scrollView.contentOffset.y - scrollView.contentInset.top * -1) < scrollViewAtTheTopDeltaThreshold 39 | } 40 | 41 | private lazy var contentView: SheetContentView = { 42 | let v = SheetContentView(metrics: self.metrics) 43 | 44 | v.layer.cornerRadius = view.layer.cornerRadius 45 | v.layer.maskedCorners = view.layer.maskedCorners 46 | v.autoresizingMask = [.flexibleWidth, .flexibleHeight] 47 | v.clipsToBounds = true 48 | 49 | return v 50 | }() 51 | 52 | private(set) lazy var scrollView: UIScrollView = { 53 | let v = UIScrollView() 54 | 55 | v.translatesAutoresizingMaskIntoConstraints = false 56 | v.delegate = self 57 | 58 | return v 59 | }() 60 | 61 | override func loadView() { 62 | view = UIView() 63 | 64 | view.backgroundColor = #colorLiteral(red: 0.9411764706, green: 0.9411764706, blue: 0.9411764706, alpha: 1) 65 | view.layer.cornerRadius = metrics.cornerRadius 66 | view.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] 67 | view.translatesAutoresizingMaskIntoConstraints = false 68 | view.layer.shadowColor = UIColor.black.cgColor 69 | view.layer.shadowOpacity = Float(metrics.shadowOpacity) 70 | view.layer.shadowRadius = metrics.shadowRadius 71 | view.layer.shadowOffset = CGSize(width: 0, height: -1) 72 | 73 | contentView.frame = view.bounds 74 | view.addSubview(contentView) 75 | 76 | contentView.addSubview(scrollView) 77 | 78 | NSLayoutConstraint.activate([ 79 | scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor), 80 | scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor), 81 | scrollView.topAnchor.constraint(equalTo: view.topAnchor), 82 | scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor) 83 | ]) 84 | } 85 | 86 | private weak var contentController: UIViewController? 87 | 88 | func installContent(_ content: UIViewController) { 89 | contentController = content 90 | 91 | addChild(content) 92 | content.view.translatesAutoresizingMaskIntoConstraints = false 93 | scrollView.addSubview(content.view) 94 | 95 | content.didMove(toParent: self) 96 | 97 | NSLayoutConstraint.activate([ 98 | content.view.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor), 99 | content.view.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor), 100 | content.view.topAnchor.constraint(equalTo: scrollView.topAnchor), 101 | content.view.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor), 102 | content.view.widthAnchor.constraint(equalTo: scrollView.widthAnchor), 103 | content.view.heightAnchor.constraint(equalTo: scrollView.heightAnchor) 104 | ]) 105 | } 106 | 107 | var availableHeight: CGFloat { 108 | guard let parentView = parent?.view else { return 0 } 109 | 110 | return parentView.bounds.intersection(view.frame).height 111 | } 112 | 113 | private var scrolledUpOnFirstContentInsetUpdate = false 114 | 115 | private var initialContentOffset: CGPoint = .zero 116 | private var previousContentOffset: CGPoint = .zero 117 | 118 | func updateContentInsets() { 119 | scrollView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: metrics.trueSheetHeight - availableHeight, right: 0) 120 | 121 | NSObject.cancelPreviousPerformRequests(withTarget: self, selector: #selector(scrollUpOnFirstContentInsetUpdateIfNeeded), object: nil) 122 | perform(#selector(scrollUpOnFirstContentInsetUpdateIfNeeded), with: nil, afterDelay: 0) 123 | } 124 | 125 | @objc private func scrollUpOnFirstContentInsetUpdateIfNeeded() { 126 | guard !scrollView.contentInset.top.isZero else { return } 127 | guard !scrolledUpOnFirstContentInsetUpdate else { return } 128 | scrolledUpOnFirstContentInsetUpdate = true 129 | 130 | scrollView.setContentOffset(CGPoint(x: 0, y: -scrollView.contentInset.top), animated: false) 131 | } 132 | 133 | deinit { 134 | print("\(String(describing: type(of: self))) DEINIT") 135 | } 136 | 137 | } 138 | 139 | final class SheetContentView: UIView { 140 | 141 | let metrics: SheetMetrics 142 | 143 | init(metrics: SheetMetrics) { 144 | self.metrics = metrics 145 | 146 | super.init(frame: .zero) 147 | } 148 | 149 | required init?(coder: NSCoder) { 150 | fatalError() 151 | } 152 | 153 | } 154 | 155 | extension SheetViewController: UIScrollViewDelegate { 156 | 157 | func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { 158 | initialContentOffset = scrollView.contentOffset 159 | } 160 | 161 | func scrollViewWillBeginDecelerating(_ scrollView: UIScrollView) { 162 | rubberBandingStartHandler?() 163 | } 164 | 165 | func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { 166 | rubberBandingFinishedHandler?() 167 | } 168 | 169 | func scrollViewDidScroll(_ scrollView: UIScrollView) { 170 | defer { previousContentOffset = scrollView.contentOffset } 171 | 172 | var isRubberBandingUp = false 173 | var bandOffset: CGFloat = 0 174 | 175 | if scrollView.isDecelerating { 176 | let effectiveOffset = scrollView.contentOffset.y + scrollView.contentInset.top 177 | 178 | if scrollView.contentOffset.y < initialContentOffset.y { 179 | if effectiveOffset < 0 { 180 | isRubberBandingUp = true 181 | bandOffset = effectiveOffset 182 | rubberBandingUpdateHandler?(effectiveOffset) 183 | } 184 | } 185 | } 186 | 187 | if isRubberBandingUp { 188 | // Counteract rubber banding by shifting contents so that they are flush with the top. 189 | // We can't use setContentOffset here because that kills the rubber banding. 190 | CATransaction.begin() 191 | CATransaction.setDisableActions(true) 192 | CATransaction.setAnimationDuration(0) 193 | 194 | contentController?.view.layer.transform = CATransform3DMakeTranslation(0, bandOffset, 0) 195 | 196 | CATransaction.commit() 197 | } else { 198 | let currentTransform = contentController?.view.layer.transform ?? CATransform3DIdentity 199 | 200 | if !CATransform3DIsIdentity(currentTransform) { 201 | CATransaction.begin() 202 | CATransaction.setDisableActions(true) 203 | CATransaction.setAnimationDuration(0) 204 | 205 | contentController?.view.layer.transform = CATransform3DIdentity 206 | 207 | CATransaction.commit() 208 | } 209 | } 210 | 211 | guard isScrollingEnabled else { 212 | scrollView.setContentOffset(previousContentOffset, animated: false) 213 | return 214 | } 215 | } 216 | 217 | } 218 | -------------------------------------------------------------------------------- /Sheet/Bottom Sheet/Presentation/SheetContainerViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SheetContainerViewController.swift 3 | // Sheet 4 | // 5 | // Created by Guilherme Rambo on 05/08/19. 6 | // Copyright © 2019 Guilherme Rambo. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public struct SheetMetrics { 12 | public static let `default` = SheetMetrics() 13 | 14 | public let bufferHeight: CGFloat = 400 15 | public let cornerRadius: CGFloat = 10 16 | public let shadowRadius: CGFloat = 10 17 | public let shadowOpacity: CGFloat = 0.12 18 | 19 | public var trueSheetHeight: CGFloat { 20 | return UIScreen.main.bounds.height + bufferHeight 21 | } 22 | } 23 | 24 | /// Defines snapping positions for the sheet. 25 | public enum SheetDetent: String, CaseIterable { 26 | 27 | /// A detent where the sheet will have its maximum height and have 28 | /// its top edge close to the top edge of the screen. 29 | case maximum 30 | 31 | /// A detent where the sheet's height will be about half the height 32 | /// of the screen, with its top edge close to the middle of the screen. 33 | case middle 34 | 35 | /// A detent at which the sheet's contents are effectively hidden, 36 | /// but the sheet's header still peek's through the bottom of the screen, 37 | /// allowing the user to expand it. 38 | case minimum 39 | 40 | /// The velocity at which the sheet will ignore the middle detent and transition directly 41 | /// from the maximum detent to the minimum detent when swiping down. 42 | static let thresholdVelocityForSkippingMiddleDetent: CGFloat = 2000 43 | 44 | /// The velocity at which the sheet will be dismissed instead of snapping 45 | /// to a detent when flung down. 46 | static let thresholdVelocityForFlingDismissal: CGFloat = 4000 47 | 48 | /// The velocity at which the sheet will snap to the middle detent when flung upwards from 49 | /// the minimum detent, ignoring the distance between the current position and the minimum detent. 50 | static let thresholdVelocityForEnforcingMinimumToMiddleTransition: CGFloat = 900 51 | 52 | } 53 | 54 | extension SheetDetent: CustomDebugStringConvertible { 55 | public var debugDescription: String { 56 | switch self { 57 | case .maximum: return "" 58 | case .middle: return "" 59 | case .minimum: return "" 60 | } 61 | } 62 | } 63 | 64 | class SheetContainerViewController: UIViewController { 65 | 66 | var transitionToMaximumDetentProgressDidChange: ((CGFloat) -> Void)? 67 | var performSnapCompanionAnimations: ((SheetDetent) -> Void)? 68 | 69 | let sheetContentController: UIViewController 70 | let initialDetent: SheetDetent 71 | let metrics: SheetMetrics 72 | 73 | let allowedDetents: [SheetDetent] 74 | let dismissWhenFlungDown: Bool 75 | 76 | weak var presentingSheetPresenter: SheetPresenter? 77 | 78 | init(sheetContentController: UIViewController, 79 | presentingSheetPresenter: SheetPresenter?, 80 | initialDetent: SheetDetent = .middle, 81 | allowedDetents: [SheetDetent] = SheetDetent.allCases, 82 | metrics: SheetMetrics = .default, 83 | dismissWhenFlungDown: Bool = false) 84 | { 85 | self.sheetContentController = sheetContentController 86 | self.presentingSheetPresenter = presentingSheetPresenter 87 | self.initialDetent = initialDetent 88 | self.metrics = metrics 89 | self.allowedDetents = allowedDetents 90 | self.dismissWhenFlungDown = dismissWhenFlungDown 91 | 92 | super.init(nibName: nil, bundle: nil) 93 | } 94 | 95 | private var overrideStatusBarStyle: UIStatusBarStyle? { 96 | didSet { 97 | setNeedsStatusBarAppearanceUpdate() 98 | } 99 | } 100 | 101 | override var preferredStatusBarStyle: UIStatusBarStyle { 102 | return overrideStatusBarStyle ?? super.preferredStatusBarStyle 103 | } 104 | 105 | override var childForStatusBarStyle: UIViewController? { 106 | return overrideStatusBarStyle != nil ? nil : sheetContentController 107 | } 108 | 109 | override var childForStatusBarHidden: UIViewController? { 110 | return sheetContentController 111 | } 112 | 113 | required init?(coder: NSCoder) { 114 | fatalError() 115 | } 116 | 117 | #warning("TODO: Rubber band and limit maximum sheet height while interactively moving") 118 | private var maximumSheetHeight: CGFloat { 119 | return value(for: .maximum) - view.safeAreaInsets.top 120 | } 121 | 122 | #warning("TODO: Rubber band and limit minimum sheet height while interactively moving") 123 | private var minimumSheetHeight: CGFloat { 124 | return metrics.shadowRadius + view.safeAreaInsets.bottom 125 | } 126 | 127 | private func heightToBottom(constant: CGFloat) -> CGFloat { 128 | return metrics.trueSheetHeight - constant 129 | } 130 | 131 | private func normalize(_ value: CGFloat, range: ClosedRange) -> CGFloat { 132 | return (value - range.lowerBound) / (range.upperBound - range.lowerBound) 133 | } 134 | 135 | private lazy var availableHeightOnMiddleDetent = abs(value(for: .middle) - metrics.trueSheetHeight) 136 | private lazy var availableHeightOnMaximumDetent = abs(value(for: .maximum) - metrics.trueSheetHeight) 137 | 138 | var maximumDetentAnimationProgress: CGFloat { 139 | guard sheetBottomConstraint.constant < value(for: .middle) else { return 0 } 140 | 141 | let currentAvailableHeight = sheetController.availableHeight 142 | 143 | let rawMin = availableHeightOnMiddleDetent / availableHeightOnMaximumDetent 144 | let rawValue = currentAvailableHeight / availableHeightOnMaximumDetent 145 | 146 | return normalize(rawValue, range: rawMin...1) 147 | } 148 | 149 | private func value(for detent: SheetDetent) -> CGFloat { 150 | switch detent { 151 | case .maximum: return heightToBottom(constant: UIScreen.main.bounds.height * 0.92) 152 | case .middle: return heightToBottom(constant: UIScreen.main.bounds.height * 0.54) 153 | case .minimum: return heightToBottom(constant: UIScreen.main.bounds.height * 0.16) 154 | } 155 | } 156 | 157 | private var flingDownDismissVelocity: CGFloat? 158 | 159 | private var snappingCancelled = false { 160 | didSet { 161 | if snappingCancelled { view.isUserInteractionEnabled = false } 162 | } 163 | } 164 | 165 | private func closestSnappingDetent(for height: CGFloat, velocity: CGPoint) -> SheetDetent { 166 | // print("SNAP velocity = \(velocity)") 167 | 168 | var winner: SheetDetent = .maximum 169 | 170 | let validDetents: [SheetDetent] 171 | 172 | if dismissWhenFlungDown, velocity.y > 0, abs(velocity.y) > SheetDetent.thresholdVelocityForFlingDismissal { 173 | snappingCancelled = true 174 | flingDownDismissVelocity = velocity.y 175 | 176 | presentingSheetPresenter?.dismiss() 177 | 178 | return .minimum 179 | } 180 | 181 | if abs(velocity.y) > SheetDetent.thresholdVelocityForSkippingMiddleDetent { 182 | if velocity.y < 0 { 183 | // Swiping up really hard, force maximum detent 184 | validDetents = [.maximum] 185 | } else { 186 | // Swiping hard in any direction, ignore middle detent 187 | validDetents = allowedDetents.filter({ $0 != .middle }) 188 | } 189 | } else if velocity.y < 0, 190 | abs(velocity.y) > SheetDetent.thresholdVelocityForEnforcingMinimumToMiddleTransition, 191 | sheetBottomConstraint.constant > value(for: .middle) 192 | { 193 | // Swiping up hard in between minimum and medium detent, force middle detent 194 | validDetents = [.middle] 195 | } else { 196 | validDetents = allowedDetents 197 | } 198 | 199 | for detent in validDetents { 200 | if abs(height - value(for: detent)) < abs(height - value(for: winner)) { 201 | winner = detent 202 | } 203 | } 204 | 205 | return winner 206 | } 207 | 208 | private weak var currentAnimator: UIViewPropertyAnimator? 209 | 210 | private func timingCurve(with velocity: CGFloat) -> FluidTimingCurve { 211 | let damping: CGFloat = velocity.isZero ? 100 : 30 212 | 213 | return FluidTimingCurve( 214 | velocity: CGVector(dx: velocity, dy: velocity), 215 | stiffness: 400, 216 | damping: damping 217 | ) 218 | } 219 | 220 | private func estimateTargetDetent(with velocity: CGFloat) -> SheetDetent { 221 | return .maximum 222 | } 223 | 224 | private func snap(to detent: SheetDetent, with velocity: CGPoint = .zero, completion: (() -> Void)? = nil) { 225 | guard !snappingCancelled else { return } 226 | 227 | if currentAnimator?.state == .some(.active) { 228 | currentAnimator?.stopAnimation(true) 229 | } 230 | 231 | let targetValue = value(for: detent) 232 | // the 0.5 is to ensure there's always some distance for the gesture to work with 233 | let distanceY = (sheetBottomConstraint.constant - 0.5) - targetValue 234 | 235 | let effectiveVelocity = velocity.y.isInfinite || velocity.y.isNaN ? 2000 : velocity.y 236 | 237 | let initialVelocityY = distanceY.isZero ? 0 : effectiveVelocity/distanceY * -1 238 | 239 | let timing = timingCurve(with: initialVelocityY) 240 | let animator = UIViewPropertyAnimator(duration: 0.3, timingParameters: timing) 241 | 242 | animator.isUserInteractionEnabled = true 243 | 244 | self.sheetBottomConstraint.constant = targetValue 245 | 246 | animator.addAnimations { 247 | self.performSnapCompanionAnimations?(detent) 248 | 249 | self.view.setNeedsLayout() 250 | self.view.layoutIfNeeded() 251 | 252 | self.sheetController.updateContentInsets() 253 | 254 | if detent == .maximum { 255 | self.dimmingView.alpha = self.maximumDimmingAlpha 256 | self.overrideStatusBarStyle = .lightContent 257 | } else { 258 | if detent == .minimum { 259 | self.dimmingView.alpha = 0 260 | } else { 261 | self.dimmingView.alpha = self.minimumDimmingAlpha 262 | } 263 | 264 | self.overrideStatusBarStyle = nil 265 | } 266 | } 267 | 268 | animator.addCompletion { pos in 269 | guard pos == .end else { return } 270 | 271 | completion?() 272 | } 273 | 274 | currentAnimator = animator 275 | 276 | animator.startAnimation() 277 | } 278 | 279 | func dismissSheet(coordinator: UIViewControllerTransitionCoordinator? = nil, duration: TimeInterval = 0.3, completion: (() -> Void)? = nil) { 280 | let targetValue = metrics.trueSheetHeight 281 | 282 | let animationBlock = { 283 | self.dimmingView.alpha = 0 284 | 285 | self.performSnapCompanionAnimations?(.minimum) 286 | 287 | self.view.setNeedsLayout() 288 | self.view.layoutIfNeeded() 289 | 290 | self.sheetController.updateContentInsets() 291 | 292 | self.overrideStatusBarStyle = nil 293 | } 294 | 295 | if let coordinator = coordinator { 296 | coordinator.animate(alongsideTransition: { _ in 297 | animationBlock() 298 | }, completion: { _ in 299 | completion?() 300 | }) 301 | } else { 302 | let distanceY = sheetBottomConstraint.constant - targetValue 303 | 304 | let effectiveVelocity: CGFloat 305 | 306 | if let flingVelocity = flingDownDismissVelocity { 307 | effectiveVelocity = flingVelocity.isInfinite || flingVelocity.isNaN ? 2000 : flingVelocity 308 | } else { 309 | effectiveVelocity = 0 310 | } 311 | 312 | let initialVelocityY = distanceY.isZero ? 0 : effectiveVelocity/distanceY * -1 313 | 314 | let timing = timingCurve(with: initialVelocityY) 315 | 316 | let animator = UIViewPropertyAnimator(duration: duration, timingParameters: timing) 317 | 318 | animator.isUserInteractionEnabled = true 319 | 320 | self.sheetBottomConstraint.constant = targetValue 321 | 322 | animator.addAnimations { 323 | animationBlock() 324 | } 325 | 326 | animator.addCompletion { pos in 327 | guard pos == .end else { return } 328 | 329 | completion?() 330 | } 331 | 332 | animator.startAnimation() 333 | } 334 | } 335 | 336 | private lazy var sheetBottomConstraint: NSLayoutConstraint = { 337 | return sheetController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: metrics.trueSheetHeight) 338 | }() 339 | 340 | private(set) lazy var sheetController: SheetViewController = { 341 | let v = SheetViewController(metrics: self.metrics) 342 | 343 | v.rubberBandingStartHandler = { [weak self] in 344 | self?.registerRubberBandingStart() 345 | } 346 | v.rubberBandingUpdateHandler = { [weak self] offset in 347 | self?.followSheetScrollViewRubberBanding(with: offset) 348 | } 349 | v.rubberBandingFinishedHandler = { [weak self] in 350 | self?.rubberBandingFinished() 351 | } 352 | 353 | return v 354 | }() 355 | 356 | private lazy var dimmingView: UIView = { 357 | let v = UIView() 358 | 359 | v.backgroundColor = .black 360 | v.alpha = 0 361 | v.autoresizingMask = [.flexibleWidth, .flexibleHeight] 362 | v.frame = view.bounds 363 | 364 | return v 365 | }() 366 | 367 | private lazy var panGesture: UIPanGestureRecognizer = { 368 | let g = UIPanGestureRecognizer(target: self, action: #selector(handlePan)) 369 | 370 | g.delegate = self 371 | 372 | return g 373 | }() 374 | 375 | override func loadView() { 376 | view = SheetContainerView(metrics: metrics) 377 | 378 | view.addSubview(dimmingView) 379 | 380 | addChild(sheetController) 381 | view.addSubview(sheetController.view) 382 | sheetController.didMove(toParent: self) 383 | 384 | NSLayoutConstraint.activate([ 385 | sheetController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor), 386 | sheetController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor), 387 | sheetController.view.heightAnchor.constraint(equalToConstant: metrics.trueSheetHeight), 388 | sheetBottomConstraint 389 | ]) 390 | 391 | sheetController.installContent(sheetContentController) 392 | 393 | sheetController.view.addGestureRecognizer(panGesture) 394 | } 395 | 396 | private var snappedToInitialDetent = false 397 | 398 | private func snapToInitialDetent() { 399 | NSObject.cancelPreviousPerformRequests(withTarget: self) 400 | perform(#selector(doSnapToInitialDetent), with: nil, afterDelay: 0) 401 | } 402 | 403 | @objc private func doSnapToInitialDetent() { 404 | snap(to: initialDetent) 405 | } 406 | 407 | override func viewDidLoad() { 408 | super.viewDidLoad() 409 | 410 | snapToInitialDetent() 411 | } 412 | 413 | private var isDraggingSheet = false 414 | private var lastTranslationY: CGFloat = 0 415 | private var initialSheetHeightConstant: CGFloat = 0 416 | 417 | private var minimumDimmingAlpha: CGFloat = 0.1 418 | private var maximumDimmingAlpha: CGFloat = 0.5 419 | 420 | private func snapToClosestDetent(with velocity: CGPoint) { 421 | let target = closestSnappingDetent(for: sheetBottomConstraint.constant, velocity: velocity) 422 | 423 | snap(to: target, with: velocity) 424 | } 425 | 426 | @objc private func handlePan(_ recognizer: UIPanGestureRecognizer) { 427 | let translation = recognizer.translation(in: view) 428 | let velocity = recognizer.velocity(in: view) 429 | 430 | switch recognizer.state { 431 | case .began: 432 | isDraggingSheet = true 433 | initialSheetHeightConstant = sheetBottomConstraint.constant 434 | case .ended, .cancelled, .failed: 435 | isDraggingSheet = false 436 | 437 | if !sheetController.isScrollingEnabled { 438 | snapToClosestDetent(with: velocity) 439 | } 440 | 441 | lastTranslationY = 0 442 | case .changed: 443 | let newConstant = sheetBottomConstraint.constant + (translation.y - lastTranslationY) 444 | 445 | if sheetController.isScrollViewAtTheTop { 446 | if newConstant > value(for: .maximum) || translation.y > 0 { 447 | sheetBottomConstraint.constant = newConstant 448 | 449 | sheetController.updateContentInsets() 450 | 451 | sheetController.isScrollingEnabled = false 452 | 453 | progressMaximumDetentInteractiveAnimation() 454 | } else { 455 | sheetController.isScrollingEnabled = true 456 | } 457 | } else { 458 | sheetController.isScrollingEnabled = true 459 | } 460 | 461 | lastTranslationY = translation.y 462 | default: 463 | break 464 | } 465 | } 466 | 467 | private var sheetBottomConstantAtRubberBandingStart: CGFloat = 0 468 | 469 | private func registerRubberBandingStart() { 470 | sheetBottomConstantAtRubberBandingStart = sheetBottomConstraint.constant 471 | } 472 | 473 | private func rubberBandingFinished() { 474 | guard currentAnimator?.state != .active else { return } 475 | 476 | snapToClosestDetent(with: .zero) 477 | } 478 | 479 | private func followSheetScrollViewRubberBanding(with offset: CGFloat) { 480 | guard offset < 0 else { return } // only follow rubber banding when at the top 481 | 482 | sheetBottomConstraint.constant = sheetBottomConstantAtRubberBandingStart - offset 483 | 484 | progressMaximumDetentInteractiveAnimation() 485 | } 486 | 487 | private func progressMaximumDetentInteractiveAnimation() { 488 | let progressToMaxDetent = maximumDetentAnimationProgress 489 | 490 | if progressToMaxDetent >= 0.5 { 491 | overrideStatusBarStyle = .lightContent 492 | } else { 493 | overrideStatusBarStyle = nil 494 | } 495 | 496 | if sheetBottomConstraint.constant < value(for: .minimum) { 497 | let duration: TimeInterval = dimmingView.alpha == 0 ? 0.3 : 0 498 | 499 | UIView.animate(withDuration: duration) { 500 | self.dimmingView.alpha = self.minimumDimmingAlpha + self.maximumDimmingAlpha * progressToMaxDetent 501 | } 502 | } else { 503 | dimmingView.alpha = 0 504 | } 505 | 506 | transitionToMaximumDetentProgressDidChange?(progressToMaxDetent) 507 | } 508 | 509 | deinit { 510 | print("\(String(describing: type(of: self))) DEINIT") 511 | } 512 | 513 | } 514 | 515 | private final class SheetContainerView: UIView { 516 | 517 | let metrics: SheetMetrics 518 | 519 | init(metrics: SheetMetrics) { 520 | self.metrics = metrics 521 | 522 | super.init(frame: .zero) 523 | } 524 | 525 | required init?(coder: NSCoder) { 526 | fatalError() 527 | } 528 | 529 | override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { 530 | guard let result = super.hitTest(point, with: event) else { return nil } 531 | 532 | return result.isSheetDescendant ? result : nil 533 | } 534 | 535 | } 536 | 537 | extension UIView { 538 | var isSheetDescendant: Bool { 539 | var currentView: UIView? = self 540 | 541 | repeat { 542 | if currentView is SheetContentView { return true } 543 | 544 | currentView = currentView?.superview 545 | } while currentView != nil 546 | 547 | return false 548 | } 549 | } 550 | 551 | extension SheetContainerViewController: UIGestureRecognizerDelegate { 552 | func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { 553 | return true 554 | } 555 | } 556 | -------------------------------------------------------------------------------- /Sheet/Bottom Sheet/Presentation/SheetPresentationWindow.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SheetPresentationWindow.swift 3 | // Sheet 4 | // 5 | // Created by Guilherme Rambo on 05/08/19. 6 | // Copyright © 2019 Guilherme Rambo. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | final class SheetPresentationWindow: UIWindow { 12 | 13 | override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { 14 | return rootViewController?.view.hitTest(point, with: event) != nil 15 | } 16 | 17 | deinit { 18 | print("\(String(describing: type(of: self))) DEINIT") 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /Sheet/Bottom Sheet/Presentation/SheetPresenter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SheetPresenter.swift 3 | // Sheet 4 | // 5 | // Created by Guilherme Rambo on 05/08/19. 6 | // Copyright © 2019 Guilherme Rambo. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | /// Allows a controller to present another controller as a sheet that can be 12 | /// snapped to different positions. 13 | public final class SheetPresenter: NSObject { 14 | 15 | private var window: SheetPresentationWindow? 16 | 17 | private var container: SheetContainerViewController? 18 | 19 | private weak var presenter: UIViewController? 20 | 21 | private var presenterWindow: UIWindow? { 22 | return presenter?.view.window 23 | } 24 | 25 | /// Whether the sheet is currently being presented. 26 | private(set) var isPresentingSheet = false 27 | 28 | /// Starts the presentation of a controller as a sheet. 29 | /// - Parameter presenter: The view controller that's presenting the sheet. 30 | /// - Parameter content: The view controller that will be inside the sheet. 31 | /// - Parameter initialDetent: The initial position of the sheet (defaults to `.middle`) 32 | /// - Parameter allowedDetents: The allowed snapping positions for the sheet (defaults to all positions). 33 | /// - Parameter dismissWhenFlungDown: Whether the sheet can be dismissed when flung down by the user. 34 | /// - Parameter metrics: Metrics defining the look of the sheet (can be ommited to use default metrics). 35 | public func presentSheet(from presenter: UIViewController, 36 | with content: UIViewController, 37 | initialDetent: SheetDetent = .middle, 38 | allowedDetents: [SheetDetent] = SheetDetent.allCases, 39 | dismissWhenFlungDown: Bool = false, 40 | metrics: SheetMetrics = .default) 41 | { 42 | guard !isPresentingSheet else { return } 43 | 44 | assert(presenter.view.window != nil, "Tried to present a sheet from a view controller that's not currently on screen!") 45 | 46 | guard window == nil else { return } 47 | 48 | self.presenter = presenter 49 | presenterWindow?.clipsToBounds = true 50 | 51 | let w = SheetPresentationWindow(frame: presenter.view.bounds) 52 | let c = SheetContainerViewController( 53 | sheetContentController: content, 54 | presentingSheetPresenter: self, 55 | initialDetent: initialDetent, 56 | allowedDetents: allowedDetents, 57 | metrics: metrics, 58 | dismissWhenFlungDown: dismissWhenFlungDown 59 | ) 60 | 61 | w.rootViewController = c 62 | w.windowLevel = .alert 63 | w.makeKeyAndVisible() 64 | 65 | self.window = w 66 | self.container = c 67 | 68 | c.performSnapCompanionAnimations = { [weak self] detent in 69 | guard let self = self else { return } 70 | 71 | switch detent { 72 | case .maximum: 73 | self.animateToMaximumDetent() 74 | default: 75 | self.animateToNonMaximumDetent() 76 | } 77 | } 78 | 79 | c.transitionToMaximumDetentProgressDidChange = { [weak self] progress in 80 | self?.updateSheetAnimationStateToMaximumDetent(with: progress) 81 | } 82 | 83 | isPresentingSheet = true 84 | } 85 | 86 | /// Dismisses the sheet. 87 | /// - Parameter coordinator: Perform the dismissal together with an animated transition. 88 | /// - Parameter completion: Called when the dismissal animation has completed. 89 | public func dismiss(with coordinator: UIViewControllerTransitionCoordinator? = nil, completion: (() -> Void)? = nil) { 90 | container?.dismissSheet(duration: 0.4) { [weak self] in 91 | completion?() 92 | 93 | self?.window?.resignKey() 94 | self?.window?.isHidden = true 95 | self?.window?.removeFromSuperview() 96 | 97 | self?.container = nil 98 | self?.presenter = nil 99 | self?.window = nil 100 | 101 | self?.isPresentingSheet = false 102 | } 103 | } 104 | 105 | private var presenterTranslationWhenAtMaximumDetent: CGFloat { 106 | let safeAreaTop = container?.view.safeAreaInsets.top ?? 0 107 | 108 | return safeAreaTop <= 20 ? safeAreaTop + 22 : safeAreaTop + 8 109 | } 110 | 111 | private let presenterHorizontalScaleWhenAtMaximumDetent: CGFloat = 0.914 112 | 113 | private let presenterCornerRadiusWhenAtMaximumDetent: CGFloat = 10 114 | 115 | private let presenterScaleWhenAtMaximumDetent: CGFloat = 0.9 116 | 117 | private func updateSheetAnimationStateToMaximumDetent(with progress: CGFloat) { 118 | let translation = presenterTranslationWhenAtMaximumDetent * progress 119 | let radius = presenterCornerRadiusWhenAtMaximumDetent * progress 120 | let scale = min(1, 1 - progress + presenterScaleWhenAtMaximumDetent) 121 | 122 | let translationTransform = CATransform3DMakeTranslation(0, translation, 0) 123 | let scaleTransform = CATransform3DMakeScale(scale, 1, 1) 124 | 125 | presenterWindow?.layer.transform = CATransform3DConcat(translationTransform, scaleTransform) 126 | presenterWindow?.layer.cornerRadius = radius 127 | } 128 | 129 | private func animateToMaximumDetent() { 130 | let translationTransform = CATransform3DMakeTranslation(0, presenterTranslationWhenAtMaximumDetent, 0) 131 | let scaleTransform = CATransform3DMakeScale(presenterScaleWhenAtMaximumDetent, 1, 1) 132 | 133 | presenterWindow?.layer.transform = CATransform3DConcat(translationTransform, scaleTransform) 134 | presenterWindow?.layer.cornerRadius = presenterCornerRadiusWhenAtMaximumDetent 135 | } 136 | 137 | private func animateToNonMaximumDetent() { 138 | presenterWindow?.layer.transform = CATransform3DIdentity 139 | presenterWindow?.layer.cornerRadius = 0 140 | } 141 | 142 | } 143 | -------------------------------------------------------------------------------- /Sheet/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIMainStoryboardFile 26 | Main 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | 35 | UISupportedInterfaceOrientations~ipad 36 | 37 | UIInterfaceOrientationPortrait 38 | UIInterfaceOrientationPortraitUpsideDown 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /Sheet/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // Sheet 4 | // 5 | // Created by Guilherme Rambo on 28/08/19. 6 | // Copyright © 2019 Guilherme Rambo. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class ViewController: UIViewController { 12 | 13 | private lazy var sheetContentController: UIViewController = { 14 | let c = UIViewController() 15 | 16 | c.view.backgroundColor = .red 17 | 18 | return c 19 | }() 20 | 21 | private lazy var sheetPresenter = SheetPresenter() 22 | 23 | @IBAction func showSheet(_ sender: UIButton) { 24 | sheetPresenter.presentSheet(from: self, with: sheetContentController) 25 | } 26 | 27 | } 28 | 29 | --------------------------------------------------------------------------------