├── .gitignore ├── Example ├── BottomSheetExample.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ └── WorkspaceSettings.xcsettings │ └── xcshareddata │ │ └── xcschemes │ │ └── BottomSheetExample.xcscheme └── BottomSheetExample │ ├── Apple Applications │ └── StocksExample.swift │ ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── Contents.json │ ├── BottomSheetExampleApp.swift │ ├── ExampleOverview.swift │ ├── Examples │ └── StaticScrollViewExample.swift │ ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json │ └── View Modifiers │ └── CornerRadius.swift ├── LICENSE ├── Package.swift ├── README.md ├── Sources └── BottomSheet │ ├── Animation │ ├── Animation.swift │ └── AnimationDefaults.swift │ ├── BottomSheet.swift │ ├── Detents │ ├── DetentDefaults.swift │ ├── DetentHelpers.swift │ └── Detents.swift │ ├── Helpers │ ├── KeyboardReader.swift │ └── Snapping.swift │ ├── Preference Keys │ ├── BackgroundInteractionKey.swift │ ├── ConfigKey.swift │ └── IndicatorKey.swift │ ├── UIKit Views │ └── UIScrollViewWrapper.swift │ ├── View Modifiers │ ├── View+AnimationChange.swift │ ├── View+BackgroundInteraction.swift │ ├── View+Detents.swift │ ├── View+DragIndicator.swift │ └── View+SheetPlus.swift │ └── Views │ └── DragIndicator.swift └── Tests └── BottomSheetTests └── BottomSheetTests.swift /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | DerivedData/ 7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 8 | -------------------------------------------------------------------------------- /Example/BottomSheetExample.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 55; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 6C0BD46D27DA862D000AF3CD /* BottomSheetExampleApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C0BD46C27DA862D000AF3CD /* BottomSheetExampleApp.swift */; }; 11 | 6C0BD47127DA862D000AF3CD /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 6C0BD47027DA862D000AF3CD /* Assets.xcassets */; }; 12 | 6C0BD47427DA862D000AF3CD /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 6C0BD47327DA862D000AF3CD /* Preview Assets.xcassets */; }; 13 | 6C6EB0B6292AEADC00106A1D /* BottomSheet in Frameworks */ = {isa = PBXBuildFile; productRef = 6C6EB0B5292AEADC00106A1D /* BottomSheet */; }; 14 | 6C763147283D774500463709 /* ExampleOverview.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C763146283D774500463709 /* ExampleOverview.swift */; }; 15 | 6C8CBF1D2AED12E00007E10E /* StaticScrollViewExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C8CBF1C2AED12E00007E10E /* StaticScrollViewExample.swift */; }; 16 | 6CDF5A0D27ED33C7004609F4 /* CornerRadius.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CDF5A0C27ED33C7004609F4 /* CornerRadius.swift */; }; 17 | 6CF78515293D36FB000E6581 /* StocksExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CF78514293D36FB000E6581 /* StocksExample.swift */; }; 18 | /* End PBXBuildFile section */ 19 | 20 | /* Begin PBXFileReference section */ 21 | 6C0BD46927DA862D000AF3CD /* BottomSheetExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = BottomSheetExample.app; sourceTree = BUILT_PRODUCTS_DIR; }; 22 | 6C0BD46C27DA862D000AF3CD /* BottomSheetExampleApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BottomSheetExampleApp.swift; sourceTree = ""; }; 23 | 6C0BD47027DA862D000AF3CD /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 24 | 6C0BD47327DA862D000AF3CD /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 25 | 6C1DE0102889CF10003C6EE9 /* BottomSheet */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = BottomSheet; path = ..; sourceTree = ""; }; 26 | 6C763146283D774500463709 /* ExampleOverview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleOverview.swift; sourceTree = ""; }; 27 | 6C8CBF1C2AED12E00007E10E /* StaticScrollViewExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StaticScrollViewExample.swift; sourceTree = ""; }; 28 | 6CDF5A0C27ED33C7004609F4 /* CornerRadius.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CornerRadius.swift; sourceTree = ""; }; 29 | 6CF78514293D36FB000E6581 /* StocksExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StocksExample.swift; sourceTree = ""; }; 30 | /* End PBXFileReference section */ 31 | 32 | /* Begin PBXFrameworksBuildPhase section */ 33 | 6C0BD46627DA862D000AF3CD /* Frameworks */ = { 34 | isa = PBXFrameworksBuildPhase; 35 | buildActionMask = 2147483647; 36 | files = ( 37 | 6C6EB0B6292AEADC00106A1D /* BottomSheet in Frameworks */, 38 | ); 39 | runOnlyForDeploymentPostprocessing = 0; 40 | }; 41 | /* End PBXFrameworksBuildPhase section */ 42 | 43 | /* Begin PBXGroup section */ 44 | 6C0BD46027DA862C000AF3CD = { 45 | isa = PBXGroup; 46 | children = ( 47 | 6C1DE0102889CF10003C6EE9 /* BottomSheet */, 48 | 6C0BD46B27DA862D000AF3CD /* BottomSheetExample */, 49 | 6C0BD46A27DA862D000AF3CD /* Products */, 50 | 6C0BD47A27DA87A1000AF3CD /* Frameworks */, 51 | ); 52 | sourceTree = ""; 53 | }; 54 | 6C0BD46A27DA862D000AF3CD /* Products */ = { 55 | isa = PBXGroup; 56 | children = ( 57 | 6C0BD46927DA862D000AF3CD /* BottomSheetExample.app */, 58 | ); 59 | name = Products; 60 | sourceTree = ""; 61 | }; 62 | 6C0BD46B27DA862D000AF3CD /* BottomSheetExample */ = { 63 | isa = PBXGroup; 64 | children = ( 65 | 6C0BD46C27DA862D000AF3CD /* BottomSheetExampleApp.swift */, 66 | 6C763146283D774500463709 /* ExampleOverview.swift */, 67 | 6CF78516293D3703000E6581 /* Apple Applications */, 68 | 6C8CBF1B2AED12C60007E10E /* Examples */, 69 | 6CDF5A0E27ED33D3004609F4 /* View Modifiers */, 70 | 6C0BD47027DA862D000AF3CD /* Assets.xcassets */, 71 | 6C0BD47227DA862D000AF3CD /* Preview Content */, 72 | ); 73 | path = BottomSheetExample; 74 | sourceTree = ""; 75 | }; 76 | 6C0BD47227DA862D000AF3CD /* Preview Content */ = { 77 | isa = PBXGroup; 78 | children = ( 79 | 6C0BD47327DA862D000AF3CD /* Preview Assets.xcassets */, 80 | ); 81 | path = "Preview Content"; 82 | sourceTree = ""; 83 | }; 84 | 6C0BD47A27DA87A1000AF3CD /* Frameworks */ = { 85 | isa = PBXGroup; 86 | children = ( 87 | ); 88 | name = Frameworks; 89 | sourceTree = ""; 90 | }; 91 | 6C8CBF1B2AED12C60007E10E /* Examples */ = { 92 | isa = PBXGroup; 93 | children = ( 94 | 6C8CBF1C2AED12E00007E10E /* StaticScrollViewExample.swift */, 95 | ); 96 | path = Examples; 97 | sourceTree = ""; 98 | }; 99 | 6CDF5A0E27ED33D3004609F4 /* View Modifiers */ = { 100 | isa = PBXGroup; 101 | children = ( 102 | 6CDF5A0C27ED33C7004609F4 /* CornerRadius.swift */, 103 | ); 104 | path = "View Modifiers"; 105 | sourceTree = ""; 106 | }; 107 | 6CF78516293D3703000E6581 /* Apple Applications */ = { 108 | isa = PBXGroup; 109 | children = ( 110 | 6CF78514293D36FB000E6581 /* StocksExample.swift */, 111 | ); 112 | path = "Apple Applications"; 113 | sourceTree = ""; 114 | }; 115 | /* End PBXGroup section */ 116 | 117 | /* Begin PBXNativeTarget section */ 118 | 6C0BD46827DA862D000AF3CD /* BottomSheetExample */ = { 119 | isa = PBXNativeTarget; 120 | buildConfigurationList = 6C0BD47727DA862D000AF3CD /* Build configuration list for PBXNativeTarget "BottomSheetExample" */; 121 | buildPhases = ( 122 | 6C0BD46527DA862D000AF3CD /* Sources */, 123 | 6C0BD46627DA862D000AF3CD /* Frameworks */, 124 | 6C939DBE294DFF9200F6EF50 /* Swiftlint */, 125 | 6C0BD46727DA862D000AF3CD /* Resources */, 126 | ); 127 | buildRules = ( 128 | ); 129 | dependencies = ( 130 | ); 131 | name = BottomSheetExample; 132 | packageProductDependencies = ( 133 | 6C6EB0B5292AEADC00106A1D /* BottomSheet */, 134 | ); 135 | productName = BottomSheetExample; 136 | productReference = 6C0BD46927DA862D000AF3CD /* BottomSheetExample.app */; 137 | productType = "com.apple.product-type.application"; 138 | }; 139 | /* End PBXNativeTarget section */ 140 | 141 | /* Begin PBXProject section */ 142 | 6C0BD46127DA862C000AF3CD /* Project object */ = { 143 | isa = PBXProject; 144 | attributes = { 145 | BuildIndependentTargetsInParallel = 1; 146 | LastSwiftUpdateCheck = 1320; 147 | LastUpgradeCheck = 1320; 148 | TargetAttributes = { 149 | 6C0BD46827DA862D000AF3CD = { 150 | CreatedOnToolsVersion = 13.2.1; 151 | }; 152 | }; 153 | }; 154 | buildConfigurationList = 6C0BD46427DA862C000AF3CD /* Build configuration list for PBXProject "BottomSheetExample" */; 155 | compatibilityVersion = "Xcode 13.0"; 156 | developmentRegion = en; 157 | hasScannedForEncodings = 0; 158 | knownRegions = ( 159 | en, 160 | Base, 161 | ); 162 | mainGroup = 6C0BD46027DA862C000AF3CD; 163 | packageReferences = ( 164 | ); 165 | productRefGroup = 6C0BD46A27DA862D000AF3CD /* Products */; 166 | projectDirPath = ""; 167 | projectRoot = ""; 168 | targets = ( 169 | 6C0BD46827DA862D000AF3CD /* BottomSheetExample */, 170 | ); 171 | }; 172 | /* End PBXProject section */ 173 | 174 | /* Begin PBXResourcesBuildPhase section */ 175 | 6C0BD46727DA862D000AF3CD /* Resources */ = { 176 | isa = PBXResourcesBuildPhase; 177 | buildActionMask = 2147483647; 178 | files = ( 179 | 6C0BD47427DA862D000AF3CD /* Preview Assets.xcassets in Resources */, 180 | 6C0BD47127DA862D000AF3CD /* Assets.xcassets in Resources */, 181 | ); 182 | runOnlyForDeploymentPostprocessing = 0; 183 | }; 184 | /* End PBXResourcesBuildPhase section */ 185 | 186 | /* Begin PBXShellScriptBuildPhase section */ 187 | 6C939DBE294DFF9200F6EF50 /* Swiftlint */ = { 188 | isa = PBXShellScriptBuildPhase; 189 | alwaysOutOfDate = 1; 190 | buildActionMask = 2147483647; 191 | files = ( 192 | ); 193 | inputFileListPaths = ( 194 | ); 195 | inputPaths = ( 196 | ); 197 | name = Swiftlint; 198 | outputFileListPaths = ( 199 | ); 200 | outputPaths = ( 201 | ); 202 | runOnlyForDeploymentPostprocessing = 0; 203 | shellPath = /bin/sh; 204 | shellScript = "export PATH=\"$PATH:/opt/homebrew/bin\"\nif which swiftlint > /dev/null; then\n swiftlint\nelse\n echo \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi\n"; 205 | }; 206 | /* End PBXShellScriptBuildPhase section */ 207 | 208 | /* Begin PBXSourcesBuildPhase section */ 209 | 6C0BD46527DA862D000AF3CD /* Sources */ = { 210 | isa = PBXSourcesBuildPhase; 211 | buildActionMask = 2147483647; 212 | files = ( 213 | 6C0BD46D27DA862D000AF3CD /* BottomSheetExampleApp.swift in Sources */, 214 | 6C8CBF1D2AED12E00007E10E /* StaticScrollViewExample.swift in Sources */, 215 | 6CDF5A0D27ED33C7004609F4 /* CornerRadius.swift in Sources */, 216 | 6C763147283D774500463709 /* ExampleOverview.swift in Sources */, 217 | 6CF78515293D36FB000E6581 /* StocksExample.swift in Sources */, 218 | ); 219 | runOnlyForDeploymentPostprocessing = 0; 220 | }; 221 | /* End PBXSourcesBuildPhase section */ 222 | 223 | /* Begin XCBuildConfiguration section */ 224 | 6C0BD47527DA862D000AF3CD /* Debug */ = { 225 | isa = XCBuildConfiguration; 226 | buildSettings = { 227 | ALWAYS_SEARCH_USER_PATHS = NO; 228 | CLANG_ANALYZER_NONNULL = YES; 229 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 230 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; 231 | CLANG_CXX_LIBRARY = "libc++"; 232 | CLANG_ENABLE_MODULES = YES; 233 | CLANG_ENABLE_OBJC_ARC = YES; 234 | CLANG_ENABLE_OBJC_WEAK = YES; 235 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 236 | CLANG_WARN_BOOL_CONVERSION = YES; 237 | CLANG_WARN_COMMA = YES; 238 | CLANG_WARN_CONSTANT_CONVERSION = YES; 239 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 240 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 241 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 242 | CLANG_WARN_EMPTY_BODY = YES; 243 | CLANG_WARN_ENUM_CONVERSION = YES; 244 | CLANG_WARN_INFINITE_RECURSION = YES; 245 | CLANG_WARN_INT_CONVERSION = YES; 246 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 247 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 248 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 249 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 250 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 251 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 252 | CLANG_WARN_STRICT_PROTOTYPES = YES; 253 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 254 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 255 | CLANG_WARN_UNREACHABLE_CODE = YES; 256 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 257 | COPY_PHASE_STRIP = NO; 258 | DEBUG_INFORMATION_FORMAT = dwarf; 259 | ENABLE_STRICT_OBJC_MSGSEND = YES; 260 | ENABLE_TESTABILITY = YES; 261 | GCC_C_LANGUAGE_STANDARD = gnu11; 262 | GCC_DYNAMIC_NO_PIC = NO; 263 | GCC_NO_COMMON_BLOCKS = YES; 264 | GCC_OPTIMIZATION_LEVEL = 0; 265 | GCC_PREPROCESSOR_DEFINITIONS = ( 266 | "DEBUG=1", 267 | "$(inherited)", 268 | ); 269 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 270 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 271 | GCC_WARN_UNDECLARED_SELECTOR = YES; 272 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 273 | GCC_WARN_UNUSED_FUNCTION = YES; 274 | GCC_WARN_UNUSED_VARIABLE = YES; 275 | IPHONEOS_DEPLOYMENT_TARGET = 15.2; 276 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 277 | MTL_FAST_MATH = YES; 278 | ONLY_ACTIVE_ARCH = YES; 279 | SDKROOT = iphoneos; 280 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 281 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 282 | }; 283 | name = Debug; 284 | }; 285 | 6C0BD47627DA862D000AF3CD /* Release */ = { 286 | isa = XCBuildConfiguration; 287 | buildSettings = { 288 | ALWAYS_SEARCH_USER_PATHS = NO; 289 | CLANG_ANALYZER_NONNULL = YES; 290 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 291 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; 292 | CLANG_CXX_LIBRARY = "libc++"; 293 | CLANG_ENABLE_MODULES = YES; 294 | CLANG_ENABLE_OBJC_ARC = YES; 295 | CLANG_ENABLE_OBJC_WEAK = YES; 296 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 297 | CLANG_WARN_BOOL_CONVERSION = YES; 298 | CLANG_WARN_COMMA = YES; 299 | CLANG_WARN_CONSTANT_CONVERSION = YES; 300 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 301 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 302 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 303 | CLANG_WARN_EMPTY_BODY = YES; 304 | CLANG_WARN_ENUM_CONVERSION = YES; 305 | CLANG_WARN_INFINITE_RECURSION = YES; 306 | CLANG_WARN_INT_CONVERSION = YES; 307 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 308 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 309 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 310 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 311 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 312 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 313 | CLANG_WARN_STRICT_PROTOTYPES = YES; 314 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 315 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 316 | CLANG_WARN_UNREACHABLE_CODE = YES; 317 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 318 | COPY_PHASE_STRIP = NO; 319 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 320 | ENABLE_NS_ASSERTIONS = NO; 321 | ENABLE_STRICT_OBJC_MSGSEND = YES; 322 | GCC_C_LANGUAGE_STANDARD = gnu11; 323 | GCC_NO_COMMON_BLOCKS = YES; 324 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 325 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 326 | GCC_WARN_UNDECLARED_SELECTOR = YES; 327 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 328 | GCC_WARN_UNUSED_FUNCTION = YES; 329 | GCC_WARN_UNUSED_VARIABLE = YES; 330 | IPHONEOS_DEPLOYMENT_TARGET = 15.2; 331 | MTL_ENABLE_DEBUG_INFO = NO; 332 | MTL_FAST_MATH = YES; 333 | SDKROOT = iphoneos; 334 | SWIFT_COMPILATION_MODE = wholemodule; 335 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 336 | VALIDATE_PRODUCT = YES; 337 | }; 338 | name = Release; 339 | }; 340 | 6C0BD47827DA862D000AF3CD /* Debug */ = { 341 | isa = XCBuildConfiguration; 342 | buildSettings = { 343 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 344 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 345 | CODE_SIGN_STYLE = Automatic; 346 | CURRENT_PROJECT_VERSION = 1; 347 | DEVELOPMENT_ASSET_PATHS = "BottomSheetExample/Preview\\ Content"; 348 | DEVELOPMENT_TEAM = KZAMEFAGHT; 349 | ENABLE_PREVIEWS = YES; 350 | GENERATE_INFOPLIST_FILE = YES; 351 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 352 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 353 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 354 | INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; 355 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; 356 | IPHONEOS_DEPLOYMENT_TARGET = 16.0; 357 | LD_RUNPATH_SEARCH_PATHS = ( 358 | "$(inherited)", 359 | "@executable_path/Frameworks", 360 | ); 361 | MARKETING_VERSION = 1.0; 362 | PRODUCT_BUNDLE_IDENTIFIER = com.chargetrip.BottomSheetExample; 363 | PRODUCT_NAME = "$(TARGET_NAME)"; 364 | SWIFT_EMIT_LOC_STRINGS = YES; 365 | SWIFT_VERSION = 5.0; 366 | TARGETED_DEVICE_FAMILY = "1,2"; 367 | }; 368 | name = Debug; 369 | }; 370 | 6C0BD47927DA862D000AF3CD /* Release */ = { 371 | isa = XCBuildConfiguration; 372 | buildSettings = { 373 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 374 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 375 | CODE_SIGN_STYLE = Automatic; 376 | CURRENT_PROJECT_VERSION = 1; 377 | DEVELOPMENT_ASSET_PATHS = "BottomSheetExample/Preview\\ Content"; 378 | DEVELOPMENT_TEAM = KZAMEFAGHT; 379 | ENABLE_PREVIEWS = YES; 380 | GENERATE_INFOPLIST_FILE = YES; 381 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 382 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 383 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 384 | INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; 385 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; 386 | IPHONEOS_DEPLOYMENT_TARGET = 16.0; 387 | LD_RUNPATH_SEARCH_PATHS = ( 388 | "$(inherited)", 389 | "@executable_path/Frameworks", 390 | ); 391 | MARKETING_VERSION = 1.0; 392 | PRODUCT_BUNDLE_IDENTIFIER = com.chargetrip.BottomSheetExample; 393 | PRODUCT_NAME = "$(TARGET_NAME)"; 394 | SWIFT_EMIT_LOC_STRINGS = YES; 395 | SWIFT_VERSION = 5.0; 396 | TARGETED_DEVICE_FAMILY = "1,2"; 397 | }; 398 | name = Release; 399 | }; 400 | /* End XCBuildConfiguration section */ 401 | 402 | /* Begin XCConfigurationList section */ 403 | 6C0BD46427DA862C000AF3CD /* Build configuration list for PBXProject "BottomSheetExample" */ = { 404 | isa = XCConfigurationList; 405 | buildConfigurations = ( 406 | 6C0BD47527DA862D000AF3CD /* Debug */, 407 | 6C0BD47627DA862D000AF3CD /* Release */, 408 | ); 409 | defaultConfigurationIsVisible = 0; 410 | defaultConfigurationName = Release; 411 | }; 412 | 6C0BD47727DA862D000AF3CD /* Build configuration list for PBXNativeTarget "BottomSheetExample" */ = { 413 | isa = XCConfigurationList; 414 | buildConfigurations = ( 415 | 6C0BD47827DA862D000AF3CD /* Debug */, 416 | 6C0BD47927DA862D000AF3CD /* Release */, 417 | ); 418 | defaultConfigurationIsVisible = 0; 419 | defaultConfigurationName = Release; 420 | }; 421 | /* End XCConfigurationList section */ 422 | 423 | /* Begin XCSwiftPackageProductDependency section */ 424 | 6C6EB0B5292AEADC00106A1D /* BottomSheet */ = { 425 | isa = XCSwiftPackageProductDependency; 426 | productName = BottomSheet; 427 | }; 428 | /* End XCSwiftPackageProductDependency section */ 429 | }; 430 | rootObject = 6C0BD46127DA862C000AF3CD /* Project object */; 431 | } 432 | -------------------------------------------------------------------------------- /Example/BottomSheetExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Example/BottomSheetExample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Example/BottomSheetExample.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Example/BottomSheetExample.xcodeproj/xcshareddata/xcschemes/BottomSheetExample.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 45 | 51 | 52 | 53 | 54 | 60 | 62 | 68 | 69 | 70 | 71 | 73 | 74 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /Example/BottomSheetExample/Apple Applications/StocksExample.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Stocks.swift 3 | // BottomSheetExample 4 | // 5 | // Created by Wouter van de Kamp on 04/12/2022. 6 | // 7 | 8 | import SwiftUI 9 | import BottomSheet 10 | 11 | struct StocksExample: View { 12 | @EnvironmentObject var settings: SheetSettings 13 | 14 | var body: some View { 15 | VStack { 16 | Button("Close") { 17 | settings.isPresented.toggle() 18 | } 19 | 20 | Button("Change") { 21 | settings.selectedDetent = .large 22 | } 23 | 24 | Color.clear 25 | .navigationBarTitleDisplayMode(.inline) 26 | .navigationTitle("\(settings.translation.rounded())") 27 | .onAppear { 28 | settings.isPresented = true 29 | settings.activeSheetType = .stocks 30 | } 31 | } 32 | } 33 | } 34 | 35 | struct StocksHeader: View { 36 | var body: some View { 37 | VStack { 38 | HStack { 39 | VStack(alignment: .leading, spacing: 2) { 40 | Text("Business News") 41 | .font(.title) 42 | .fontWeight(.heavy) 43 | Text("From Yahoo Finance") 44 | .foregroundColor(Color(UIColor.secondaryLabel)) 45 | } 46 | .padding(.top, 10) 47 | .padding(.bottom, 16) 48 | 49 | Spacer() 50 | } 51 | 52 | Divider() 53 | .frame(height: 1) 54 | .background(Color(UIColor.systemGray6)) 55 | } 56 | .padding(.top, 8) 57 | .padding(.horizontal, 16) 58 | } 59 | } 60 | 61 | struct StocksMainContent: View { 62 | var body: some View { 63 | VStack(spacing: 0) { 64 | ScrollView { 65 | ForEach(0..<5, id: \.self) { _ in 66 | newsRow 67 | } 68 | } 69 | Spacer(minLength: 72) 70 | } 71 | } 72 | 73 | var newsRow: some View { 74 | VStack { 75 | HStack { 76 | VStack(alignment: .leading, spacing: 4) { 77 | Text("FX Empire") 78 | .font(.caption) 79 | .foregroundColor(Color(UIColor.secondaryLabel)) 80 | 81 | Text("Bitcoin (BTC) treads water after brief visit to sub-$39,000") 82 | .font(.headline) 83 | .foregroundColor(Color(UIColor.label)) 84 | 85 | Text("While Bitcoin (BTC) struggled on Saturday, XRP") 86 | .foregroundColor(Color(UIColor.secondaryLabel)) 87 | .lineLimit(1) 88 | } 89 | .padding(.vertical, 16) 90 | 91 | Spacer() 92 | } 93 | 94 | HStack { 95 | Text("13h ago") 96 | .font(.caption) 97 | .fontWeight(.medium) 98 | .foregroundColor(Color(UIColor.secondaryLabel)) 99 | 100 | Spacer() 101 | } 102 | } 103 | .padding(.horizontal, 16) 104 | } 105 | } 106 | 107 | struct StocksExample_Previews: PreviewProvider { 108 | static var previews: some View { 109 | StocksExample() 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /Example/BottomSheetExample/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 | -------------------------------------------------------------------------------- /Example/BottomSheetExample/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "scale" : "2x", 6 | "size" : "20x20" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "scale" : "3x", 11 | "size" : "20x20" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "scale" : "2x", 16 | "size" : "29x29" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "scale" : "3x", 21 | "size" : "29x29" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "scale" : "2x", 26 | "size" : "40x40" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "scale" : "2x", 36 | "size" : "60x60" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "scale" : "3x", 41 | "size" : "60x60" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "scale" : "1x", 46 | "size" : "20x20" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "scale" : "2x", 51 | "size" : "20x20" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "scale" : "1x", 56 | "size" : "29x29" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "scale" : "2x", 61 | "size" : "29x29" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "scale" : "1x", 66 | "size" : "40x40" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "scale" : "2x", 71 | "size" : "40x40" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "scale" : "1x", 76 | "size" : "76x76" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "scale" : "2x", 81 | "size" : "76x76" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "scale" : "2x", 86 | "size" : "83.5x83.5" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "scale" : "1x", 91 | "size" : "1024x1024" 92 | } 93 | ], 94 | "info" : { 95 | "author" : "xcode", 96 | "version" : 1 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /Example/BottomSheetExample/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Example/BottomSheetExample/BottomSheetExampleApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BottomSheetExampleApp.swift 3 | // BottomSheetExample 4 | // 5 | // Created by Wouter van de Kamp on 10/03/2022. 6 | // 7 | 8 | import SwiftUI 9 | 10 | @main 11 | struct BottomSheetExampleApp: App { 12 | var body: some Scene { 13 | WindowGroup { 14 | ExampleOverview() 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Example/BottomSheetExample/ExampleOverview.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ExampleOverview.swift 3 | // BottomSheetExample 4 | // 5 | // Created by Wouter van de Kamp on 24/05/2022. 6 | // 7 | 8 | import SwiftUI 9 | import BottomSheet 10 | 11 | enum SheetExampleTypes { 12 | case home 13 | case stocks 14 | case staticScrollView 15 | } 16 | 17 | class SheetSettings: ObservableObject { 18 | @Published var isPresented = false 19 | @Published var activeSheetType: SheetExampleTypes = .home 20 | @Published var selectedDetent: BottomSheet.PresentationDetent = .medium 21 | @Published var translation: CGFloat = BottomSheet.PresentationDetent.large.size 22 | } 23 | 24 | struct ExampleOverview: View { 25 | @StateObject var settings = SheetSettings() 26 | 27 | var views: [(label: String, view: AnyView)] = [ 28 | (label: "Stocks example", view: AnyView(StocksExample())), 29 | (label: "Static scrollview example", view: AnyView(StaticScrollViewExample())) 30 | ] 31 | 32 | @ViewBuilder 33 | var headerContent: some View { 34 | switch settings.activeSheetType { 35 | case .stocks: 36 | StocksHeader() 37 | case .staticScrollView: 38 | StaticScrollViewHeader() 39 | default: 40 | EmptyView() 41 | } 42 | } 43 | 44 | @ViewBuilder 45 | var mainContent: some View { 46 | switch settings.activeSheetType { 47 | case .stocks: 48 | StocksMainContent() 49 | .presentationDetentsPlus( 50 | [.height(244), .medium, .large], 51 | selection: $settings.selectedDetent 52 | ) 53 | case .staticScrollView: 54 | StaticScrollViewContent() 55 | .presentationDetentsPlus( 56 | [.height(244), .height(380), .height(480), .large], 57 | selection: $settings.selectedDetent 58 | ) 59 | .presentationDragIndicatorPlus(.visible) 60 | .presentationBackgroundInteractionPlus(.enabled(upThrough: .height(380))) 61 | default: 62 | EmptyView() 63 | } 64 | } 65 | 66 | var body: some View { 67 | ZStack { 68 | NavigationView { 69 | List(views.indices, id: \.self) { index in 70 | NavigationLink(destination: views[index].view) { 71 | Text(views[index].label) 72 | } 73 | } 74 | .background(Color(UIColor.systemGroupedBackground)) 75 | .listStyle(.grouped) 76 | .navigationTitle("Examples") 77 | .onAppear { 78 | settings.isPresented = false 79 | settings.activeSheetType = .home 80 | settings.selectedDetent = .medium 81 | } 82 | } 83 | .navigationViewStyle(.stack) 84 | } 85 | .environmentObject(settings) 86 | .sheetPlus( 87 | isPresented: $settings.isPresented, 88 | background: ( 89 | Color(UIColor.secondarySystemBackground) 90 | .cornerRadius(12, corners: [.topLeft, .topRight]) 91 | ), 92 | onDrag: { translation in 93 | settings.translation = translation 94 | }, 95 | header: { headerContent }, 96 | main: { 97 | mainContent 98 | } 99 | ) 100 | .overlay( 101 | VStack(spacing: 0) { 102 | Divider() 103 | .frame(height: 1) 104 | .background(Color(UIColor.systemGray6)) 105 | 106 | HStack { 107 | Text("Yahoo Finance") 108 | Spacer() 109 | } 110 | .padding(.horizontal, 16) 111 | .padding(.vertical, 16) 112 | } 113 | .background( 114 | Color(UIColor.secondarySystemBackground) 115 | .edgesIgnoringSafeArea([.bottom]) 116 | ) 117 | .opacity( 118 | settings.activeSheetType == .stocks ? 1 : 0 119 | ) 120 | , 121 | alignment: .bottom 122 | ) 123 | } 124 | } 125 | 126 | struct ExampleOverview_Previews: PreviewProvider { 127 | static var previews: some View { 128 | ExampleOverview() 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /Example/BottomSheetExample/Examples/StaticScrollViewExample.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StaticScrollViewExample.swift 3 | // BottomSheetExample 4 | // 5 | // Created by Wouter van de Kamp on 28/10/2023. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct StaticScrollViewExample: View { 11 | @EnvironmentObject var settings: SheetSettings 12 | 13 | var body: some View { 14 | VStack { 15 | Button("Close") { 16 | settings.isPresented.toggle() 17 | } 18 | 19 | Button("Change") { 20 | settings.selectedDetent = .large 21 | } 22 | 23 | Color.clear 24 | .navigationBarTitleDisplayMode(.inline) 25 | .navigationTitle("\(settings.translation.rounded())") 26 | .onAppear { 27 | settings.isPresented = true 28 | settings.activeSheetType = .staticScrollView 29 | } 30 | } 31 | } 32 | } 33 | 34 | struct StaticScrollViewHeader: View { 35 | @State private var searchterm = "" 36 | 37 | var body: some View { 38 | VStack { 39 | TextField("Search item", text: $searchterm) 40 | } 41 | } 42 | } 43 | 44 | struct StaticScrollViewContent: View { 45 | var body: some View { 46 | ScrollView { 47 | ForEach(0..<5, id: \.self) { idx in 48 | Text("Item \(idx)") 49 | } 50 | } 51 | } 52 | } 53 | 54 | struct StaticScrollViewExample_Previews: PreviewProvider { 55 | static var previews: some View { 56 | StaticScrollViewExample() 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Example/BottomSheetExample/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Example/BottomSheetExample/View Modifiers/CornerRadius.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CornerRadius.swift 3 | // BottomSheetExample 4 | // 5 | // Created by Wouter van de Kamp on 25/03/2022. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct CornerRadiusStyle: ViewModifier { 11 | var radius: CGFloat 12 | var corners: UIRectCorner 13 | 14 | struct CornerRadiusShape: Shape { 15 | var radius = CGFloat.infinity 16 | var corners = UIRectCorner.allCorners 17 | 18 | func path(in rect: CGRect) -> Path { 19 | let path = UIBezierPath( 20 | roundedRect: rect, 21 | byRoundingCorners: corners, 22 | cornerRadii: CGSize(width: radius, height: radius) 23 | ) 24 | return Path(path.cgPath) 25 | } 26 | } 27 | 28 | func body(content: Content) -> some View { 29 | content 30 | .clipShape(CornerRadiusShape(radius: radius, corners: corners)) 31 | } 32 | } 33 | 34 | extension View { 35 | func cornerRadius(_ radius: CGFloat, corners: UIRectCorner) -> some View { 36 | ModifiedContent(content: self, modifier: CornerRadiusStyle(radius: radius, corners: corners)) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Wouter van de Kamp. 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. -------------------------------------------------------------------------------- /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(.v14) 10 | ], 11 | products: [ 12 | // Products define the executables and libraries a package produces, and make them visible to other packages. 13 | .library( 14 | name: "BottomSheet", 15 | targets: ["BottomSheet"]), 16 | ], 17 | dependencies: [ 18 | // Dependencies declare other packages that this package depends on. 19 | // .package(url: /* package url */, from: "1.0.0"), 20 | ], 21 | targets: [ 22 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 23 | // Targets can depend on other targets in this package, and on products in packages this package depends on. 24 | .target( 25 | name: "BottomSheet", 26 | dependencies: []), 27 | .testTarget( 28 | name: "BottomSheetTests", 29 | dependencies: ["BottomSheet"]), 30 | ] 31 | ) 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BottomSheet 2 | 3 | An iOS library for SwiftUI to create draggable sheet experiences similar to iOS applications like Maps and Stocks. 4 | 5 | ## Feature overview 6 | 7 | The library currently supports; 8 | 9 | - [x] Unlimited snap positions 10 | - [x] Realtime position callback 11 | - [x] Absolute and relative positioning 12 | - [x] Customizable animation parameters 13 | - [x] An optional sticky header 14 | - [x] Views with and without a scrollview 15 | 16 | ## How to install 17 | 18 | Currently BottomSheet is only available through the [Swift Package Manager](https://swift.org/package-manager/) or manual install. 19 | 20 | 1. Installation through Swift Package Manager can be done by going to `File > Add Packages`. Then enter the following URL in the searchbar; `https://github.com/Wouter125/BottomSheet`. 21 | 22 | 2. Manual installation can be done by cloning this repository and dragging all assets into your Xcode Project. 23 | 24 | ## How to use 25 | 26 | 1. Import BottomSheet 27 | 28 | 2. Create a state property that contains the presentation state of the bottom sheet and one for the current selection; 29 | 30 | ``` 31 | @Published var isPresented = false 32 | @Published var selectedDetent: BottomSheet.PresentationDetent = .medium 33 | ``` 34 | 35 | 4. Add the `BottomSheetView` to your SwiftUI view hierachy by using a view modifier; 36 | 37 | ``` 38 | .sheetPlus( 39 | isPresented: $isPresented, 40 | header: { }, 41 | main: { 42 | EmptyView() 43 | .presentationDetentsPlus( 44 | [.height(244), .fraction(0.4), .medium, .large], 45 | selection: $selectedDetent 46 | ) 47 | } 48 | ) 49 | ``` 50 | 51 | 5. Optionally receive the current panel position with a callback, change the background color, show a drag indicator or limit the background interaction based on the height; 52 | ``` 53 | .sheetPlus( 54 | isPresented: $isPresented, 55 | background: ( 56 | Color(UIColor.secondarySystemBackground) 57 | ), 58 | onDrag: { translation in 59 | print(translation) 60 | }, 61 | header: { EmptyView() }, 62 | main: { 63 | EmptyView() 64 | .presentationDetentsPlus( 65 | [.height(244), .fraction(0.4), .medium, .large], 66 | selection: $selectedDetent 67 | ) 68 | .presentationDragIndicatorPlus(.visible) 69 | .presentationBackgroundInteractionPlus(.enabled(upThrough: .height(380))) 70 | } 71 | ) 72 | ``` 73 | 74 | ## Interface 75 | 76 | | Modifier | Type | Default | Description | 77 | |--------------------------|---------------------|---------|-----------------------------------------------------------------------------------| 78 | | animationCurve.mass | Double | 1.2 | The mass of the object attached to the spring. | 79 | | animationCurve.stiffness | Double | 200 | The stiffness of the spring. | 80 | | animationCurve.damping | Double | 25 | The spring damping value. | 81 | 82 | ## Example 83 | 84 | To give you an idea of how to use this library you can use the example that is attached to this repo. Simply clone it and open the `BottomSheetExample` folder in Xcode. 85 | 86 | ## Roadmap 87 | 88 | 1. Add landscape support 89 | 2. Add iPad support 90 | -------------------------------------------------------------------------------- /Sources/BottomSheet/Animation/Animation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Animation.swift 3 | // 4 | // 5 | // Created by Wouter van de Kamp on 26/11/2022. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct SheetAnimation { 11 | var mass: Double 12 | var stiffness: Double 13 | var damping: Double 14 | 15 | public init(mass: Double, stiffness: Double, damping: Double) { 16 | self.mass = mass 17 | self.stiffness = stiffness 18 | self.damping = damping 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Sources/BottomSheet/Animation/AnimationDefaults.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AnimationDefaults.swift 3 | // 4 | // 5 | // Created by Wouter van de Kamp on 26/11/2022. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct SheetAnimationDefaults { 11 | public static let mass: Double = 1.2 12 | public static let stiffness: Double = 200 13 | public static let damping: Double = 25 14 | } 15 | -------------------------------------------------------------------------------- /Sources/BottomSheet/BottomSheet.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BottomSheet.swift 3 | // 4 | // 5 | // Created by Wouter van de Kamp on 26/11/2022. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct SheetPlus: ViewModifier, KeyboardReader { 11 | @Binding private var isPresented: Bool 12 | 13 | @State private var translation: CGFloat = 0 14 | @State private var sheetConfig: SheetPlusConfig? 15 | @State private var showDragIndicator: VisibilityPlus? 16 | @State private var allowBackgroundInteraction: PresentationBackgroundInteractionPlus? 17 | 18 | @State private var newValue = 0.0 19 | @State private var startTime: DragGesture.Value? 20 | 21 | @State private var detents: Set = [] 22 | @State private var limits: (min: CGFloat, max: CGFloat) = (min: 0, max: 0) 23 | 24 | let mainContent: MContent 25 | let headerContent: HContent 26 | let animationCurve: SheetAnimation 27 | let onDismiss: () -> Void 28 | let onDrag: (CGFloat) -> Void 29 | let background: Background 30 | 31 | init( 32 | isPresented: Binding, 33 | animationCurve: SheetAnimation, 34 | background: Background, 35 | onDismiss: @escaping () -> Void, 36 | onDrag: @escaping (CGFloat) -> Void, 37 | @ViewBuilder hcontent: () -> HContent, 38 | @ViewBuilder mcontent: () -> MContent 39 | ) { 40 | self._isPresented = isPresented 41 | 42 | self.animationCurve = animationCurve 43 | self.background = background 44 | self.onDismiss = onDismiss 45 | self.onDrag = onDrag 46 | 47 | self.headerContent = hcontent() 48 | self.mainContent = mcontent() 49 | } 50 | 51 | func body(content: Content) -> some View { 52 | ZStack() { 53 | content 54 | .allowsHitTesting(allowBackgroundInteraction == .disabled ? false : true) 55 | 56 | if isPresented { 57 | GeometryReader { geometry in 58 | VStack(spacing: 0) { 59 | // If / else statement here breaks the animation from the bottom level 60 | // Might want to see if we can refactor the top level animation a bit 61 | DragIndicator( 62 | translation: $translation, 63 | detents: detents 64 | ) 65 | .frame(height: showDragIndicator == .visible ? 22 : 0) 66 | .opacity(showDragIndicator == .visible ? 1 : 0) 67 | 68 | headerContent 69 | .contentShape(Rectangle()) 70 | .gesture( 71 | DragGesture(coordinateSpace: .global) 72 | .onChanged { value in 73 | translation -= value.location.y - value.startLocation.y - newValue 74 | newValue = value.location.y - value.startLocation.y 75 | 76 | if startTime == nil { 77 | startTime = value 78 | } 79 | } 80 | .onEnded { value in 81 | // Reset the distance on release so we start with a 82 | // clean translation next time 83 | newValue = 0 84 | 85 | // Calculate velocity based on pt/s so it matches the UIPanGesture 86 | let distance: CGFloat = value.translation.height 87 | let time: CGFloat = value.time.timeIntervalSince(startTime!.time) 88 | 89 | let yVelocity: CGFloat = -1 * ((distance / time) / 1000) 90 | startTime = nil 91 | 92 | if let result = snapBottomSheet(translation, detents, yVelocity) { 93 | translation = result.size 94 | sheetConfig?.selectedDetent = result 95 | } 96 | } 97 | ) 98 | 99 | UIScrollViewWrapper( 100 | translation: $translation, 101 | preferenceKey: $sheetConfig, 102 | limits: limits, 103 | detents: detents 104 | ) { 105 | mainContent 106 | .frame(width: geometry.size.width) 107 | } 108 | } 109 | .background(background) 110 | .frame(height: 111 | (limits.max - geometry.safeAreaInsets.top) > 0 112 | ? limits.max - geometry.safeAreaInsets.top 113 | : limits.max 114 | ) 115 | .onChange(of: translation) { newValue in 116 | // Small little hack to make the iOS scroll behaviour work smoothly 117 | if limits.max == 0 { return } 118 | translation = min(limits.max, max(newValue, limits.min)) 119 | 120 | currentGlobalTranslation = translation 121 | } 122 | .onAnimationChange(of: translation) { value in 123 | onDrag(value) 124 | } 125 | .offset(y: UIScreen.main.bounds.height - translation) 126 | .onDisappear { 127 | translation = 0 128 | detents = [] 129 | 130 | onDismiss() 131 | } 132 | .animation( 133 | .interpolatingSpring( 134 | mass: animationCurve.mass, 135 | stiffness: animationCurve.stiffness, 136 | damping: animationCurve.damping 137 | ) 138 | ) 139 | } 140 | .edgesIgnoringSafeArea([.bottom]) 141 | .transition(.move(edge: .bottom)) 142 | } 143 | } 144 | .onPreferenceChange(SheetPlusKey.self) { value in 145 | /// Quick hack to prevent the scrollview from resetting the height when keyboard shows up. 146 | /// Replace if the root cause has been located. 147 | if value.detents.count == 0 { return } 148 | 149 | sheetConfig = value 150 | translation = value.translation 151 | 152 | detents = value.detents 153 | limits = detentLimits(detents: detents) 154 | } 155 | .onPreferenceChange(SheetPlusIndicatorKey.self) { value in 156 | showDragIndicator = value 157 | } 158 | .onPreferenceChange(SheetPlusBackgroundInteractionKey.self) { value in 159 | allowBackgroundInteraction = value 160 | } 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /Sources/BottomSheet/Detents/DetentDefaults.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DetentsDefaults.swift 3 | // 4 | // 5 | // Created by Wouter van de Kamp on 20/11/2022. 6 | // 7 | 8 | import SwiftUI 9 | 10 | internal struct PresentationDetentDefaults { 11 | static let small: CGFloat = UIScreen.main.bounds.height * 0.2 12 | static let medium: CGFloat = UIScreen.main.bounds.height * 0.5 13 | static let large: CGFloat = UIScreen.main.bounds.height * 0.9 14 | } 15 | -------------------------------------------------------------------------------- /Sources/BottomSheet/Detents/DetentHelpers.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DetentsHelper.swift 3 | // 4 | // 5 | // Created by Wouter van de Kamp on 20/11/2022. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /// Computes the limits of how far the sheet can move. 11 | /// - Parameter detents: The list of detents provided when initializing the bottomsheet 12 | /// - Returns: Tuple with top and bottom. Top reflects the offset from the top of the screen when the sheet is in it's largest form. Bottom reflects the offset from the top of the screen when the sheet is in it's smallest form. 13 | internal func detentLimits(detents: Set) -> (min: CGFloat, max: CGFloat) { 14 | let detentLimits: [CGFloat] = detents 15 | .map { detent in 16 | switch detent { 17 | case .small: 18 | return PresentationDetentDefaults.small 19 | case .medium: 20 | return PresentationDetentDefaults.medium 21 | case .large: 22 | return PresentationDetentDefaults.large 23 | case .fraction(let fraction): 24 | return UIScreen.main.bounds.height * fraction 25 | case .height(let height): 26 | return height 27 | } 28 | } 29 | .sorted(by: <) 30 | 31 | return (min: detentLimits.first ?? 0, max: detentLimits.last ?? 0) 32 | } 33 | -------------------------------------------------------------------------------- /Sources/BottomSheet/Detents/Detents.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Detents.swift 3 | // 4 | // 5 | // Created by Wouter van de Kamp on 20/11/2022. 6 | // 7 | 8 | import SwiftUI 9 | 10 | public enum PresentationDetent: Hashable { 11 | case small 12 | case medium 13 | case large 14 | case fraction(CGFloat) 15 | case height(CGFloat) 16 | 17 | public var size: CGFloat { 18 | switch self { 19 | case .small: 20 | return PresentationDetentDefaults.small 21 | case .medium: 22 | return PresentationDetentDefaults.medium 23 | case .large: 24 | return PresentationDetentDefaults.large 25 | case .fraction(let fraction): 26 | return min( 27 | UIScreen.main.bounds.height * fraction, 28 | UIScreen.main.bounds.height 29 | ) 30 | case .height(let height): 31 | return min( 32 | height, 33 | UIScreen.main.bounds.height 34 | ) 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Sources/BottomSheet/Helpers/KeyboardReader.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KeyboardReader.swift 3 | // 4 | // 5 | // Created by Wouter van de Kamp on 28/10/2023. 6 | // 7 | 8 | import Combine 9 | import UIKit 10 | 11 | protocol KeyboardReader { 12 | var keyboardPublisher: AnyPublisher { get } 13 | } 14 | 15 | extension KeyboardReader { 16 | var keyboardPublisher: AnyPublisher { 17 | Publishers.Merge( 18 | NotificationCenter.default 19 | .publisher(for: UIResponder.keyboardWillShowNotification) 20 | .map { _ in true }, 21 | 22 | NotificationCenter.default 23 | .publisher(for: UIResponder.keyboardWillHideNotification) 24 | .map { _ in false } 25 | ) 26 | .eraseToAnyPublisher() 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Sources/BottomSheet/Helpers/Snapping.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Snapping.swift 3 | // 4 | // 5 | // Created by Wouter van de Kamp on 20/11/2022. 6 | // 7 | 8 | import Foundation 9 | 10 | /// Helper function that computes where the bottomsheet should snap to 11 | /// - Parameters: 12 | /// - translation: the current translated distance 13 | /// - detents: the detents the translation can snap to 14 | /// - yVelocity: the speed at which the drag gesture ended. Used to compute a snapping behaviour 15 | /// - Returns: The snapping position distance 16 | internal func snapBottomSheet(_ translation: CGFloat, _ detents: Set, _ yVelocity: CGFloat) -> PresentationDetent? { 17 | let detents = detents.sorted(by: { $0.size < $1.size }) 18 | 19 | let position: [PresentationDetent] = detents.enumerated().compactMap { idx, detent in 20 | if idx < detents.index(before: detents.count) { 21 | let detentBracket = ( 22 | lower: detents[idx], 23 | middle: detents[idx].size + ((detents[idx + 1].size - detents[idx].size) / 2), 24 | upper: detents[idx + 1] 25 | ) 26 | 27 | if detentBracket.lower.size...detentBracket.upper.size ~= translation { 28 | if abs(yVelocity) > 1.8 { 29 | return yVelocity > 0 ? detentBracket.upper : detentBracket.lower 30 | } else { 31 | return translation > detentBracket.middle 32 | ? detentBracket.upper 33 | : detentBracket.lower 34 | } 35 | } 36 | } 37 | 38 | return nil 39 | } 40 | 41 | return position.first 42 | } 43 | -------------------------------------------------------------------------------- /Sources/BottomSheet/Preference Keys/BackgroundInteractionKey.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BackgroundInteractionKey.swift 3 | // 4 | // 5 | // Created by Wouter van de Kamp on 29/10/2023. 6 | // 7 | 8 | import SwiftUI 9 | 10 | // Currently using a global var. 11 | // Might want to rework this by setting up the view modifiers a bit different. 12 | // Probably something that we can hold translation in 1 var. Now both need to be in sync. 13 | var currentGlobalTranslation: CGFloat = 0 14 | 15 | public enum PresentationBackgroundInteractionPlus { 16 | case automatic 17 | case disabled 18 | case enabled 19 | 20 | public static func enabled(upThrough detent: PresentationDetent) -> PresentationBackgroundInteractionPlus { 21 | currentGlobalTranslation > detent.size ? .disabled : .enabled 22 | } 23 | } 24 | 25 | struct SheetPlusBackgroundInteractionKey: PreferenceKey { 26 | static var defaultValue: PresentationBackgroundInteractionPlus = .automatic 27 | 28 | static func reduce(value: inout PresentationBackgroundInteractionPlus, nextValue: () -> PresentationBackgroundInteractionPlus) { 29 | value = nextValue() 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Sources/BottomSheet/Preference Keys/ConfigKey.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ConfigKey.swift 3 | // 4 | // 5 | // Created by Wouter van de Kamp on 20/11/2022. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct SheetPlusConfig: Equatable { 11 | let detents: Set 12 | @Binding var selectedDetent: PresentationDetent 13 | let translation: CGFloat 14 | 15 | 16 | static func == (lhs: SheetPlusConfig, rhs: SheetPlusConfig) -> Bool { 17 | return lhs.selectedDetent == rhs.selectedDetent && lhs.translation == rhs.translation && lhs.detents == rhs.detents 18 | } 19 | } 20 | 21 | struct SheetPlusKey: PreferenceKey { 22 | static var defaultValue: SheetPlusConfig = SheetPlusConfig(detents: [], selectedDetent: .constant(.height(.zero)), translation: 0) 23 | 24 | static func reduce(value: inout SheetPlusConfig, nextValue: () -> SheetPlusConfig) { 25 | /// This prevents the translation changes to be called whenever the keyboard is triggered. 26 | /// If the keyboard gets triggered it will also reset the whole configkey and losing the binding. 27 | /// https://stackoverflow.com/questions/67644164/preferencekey-issue-swiftui-sometimes-seems-to-generate-additional-views-that 28 | value = nextValue() != defaultValue ? nextValue() : value 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Sources/BottomSheet/Preference Keys/IndicatorKey.swift: -------------------------------------------------------------------------------- 1 | // 2 | // IndicatorKey.swift 3 | // 4 | // 5 | // Created by Wouter van de Kamp on 29/10/2023. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | 11 | struct SheetPlusIndicatorKey: PreferenceKey { 12 | static var defaultValue: VisibilityPlus = .automatic 13 | 14 | static func reduce(value: inout VisibilityPlus, nextValue: () -> VisibilityPlus) { 15 | value = nextValue() 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Sources/BottomSheet/UIKit Views/UIScrollViewWrapper.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIScrollViewWrapper.swift 3 | // 4 | // 5 | // Created by Wouter van de Kamp on 20/11/2022. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | import UIKit 11 | 12 | internal struct UIScrollViewWrapper: UIViewRepresentable { 13 | @Binding var translation: CGFloat 14 | @Binding var preferenceKey: SheetPlusConfig? 15 | 16 | let limits: (min: CGFloat, max: CGFloat) 17 | let detents: Set 18 | 19 | let content: () -> Content 20 | 21 | func makeUIView(context: Context) -> UIScrollView { 22 | let scrollView = UIScrollView() 23 | let hostingController = context.coordinator.hostingController 24 | 25 | scrollView.addSubview(hostingController.view) 26 | 27 | scrollView.contentInsetAdjustmentBehavior = .automatic 28 | scrollView.alwaysBounceVertical = true 29 | scrollView.delegate = context.coordinator 30 | 31 | hostingController.view.translatesAutoresizingMaskIntoConstraints = false 32 | 33 | scrollView.addConstraints([ 34 | hostingController.view.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor), 35 | hostingController.view.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor), 36 | hostingController.view.topAnchor.constraint(equalTo: scrollView.topAnchor), 37 | hostingController.view.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor) 38 | ]) 39 | 40 | hostingController.view.backgroundColor = .clear 41 | scrollView.backgroundColor = .clear 42 | 43 | scrollView.layoutIfNeeded() 44 | 45 | return scrollView 46 | } 47 | 48 | func updateUIView(_ scrollView: UIScrollView, context: Context) { 49 | context.coordinator.limits = limits 50 | context.coordinator.detents = detents 51 | 52 | context.coordinator.hostingController.rootView = self.content() 53 | } 54 | 55 | func makeCoordinator() -> Coordinator { 56 | return Coordinator( 57 | representable: self, 58 | hostingController: UIHostingController(rootView: content()), 59 | limits: limits, 60 | detents: detents 61 | ) 62 | } 63 | 64 | class Coordinator: NSObject, UIScrollViewDelegate { 65 | private var scrollOffset: CGFloat = 0 66 | private var newValue: CGFloat = 0 67 | 68 | var representable: UIScrollViewWrapper 69 | var hostingController: UIHostingController 70 | 71 | var limits: (min: CGFloat, max: CGFloat) 72 | var detents: Set 73 | 74 | init( 75 | representable: UIScrollViewWrapper, 76 | hostingController: UIHostingController, 77 | limits: (min: CGFloat, max: CGFloat), 78 | detents: Set 79 | ) { 80 | self.hostingController = hostingController 81 | self.limits = limits 82 | self.detents = detents 83 | self.representable = representable 84 | } 85 | 86 | private func shouldDragSheet(_ scrollViewPosition: CGFloat, isFixedHeight: Bool) -> Bool { 87 | // Translation on a scrollview without an overflow get's set to 0 somehow. 88 | // Implemented this check to prevent it from snapping back to the original position. 89 | // Need to dive deeper to figure out why it gets set to 0. 90 | if isFixedHeight && representable.translation == 0 { 91 | if scrollViewPosition > scrollOffset { 92 | scrollOffset = scrollViewPosition 93 | } 94 | 95 | return scrollViewPosition < 0 96 | } 97 | 98 | if representable.translation >= limits.max { 99 | if scrollViewPosition > scrollOffset { 100 | scrollOffset = scrollViewPosition 101 | } 102 | 103 | return scrollViewPosition < 0 104 | } 105 | 106 | return true 107 | } 108 | 109 | func scrollViewDidScroll(_ scrollView: UIScrollView) { 110 | let isFixedHeight = scrollView.contentSize.height < scrollView.frame.size.height 111 | 112 | guard scrollView.isTracking else { return } 113 | guard shouldDragSheet(scrollView.contentOffset.y, isFixedHeight: isFixedHeight) else { 114 | scrollView.showsVerticalScrollIndicator = true 115 | return 116 | } 117 | 118 | let localTranslation = scrollView.panGestureRecognizer.translation(in: scrollView.superview).y - scrollOffset 119 | let translationDelta = localTranslation - newValue 120 | 121 | representable.translation -= translationDelta 122 | 123 | newValue = localTranslation 124 | 125 | scrollView.showsVerticalScrollIndicator = false 126 | scrollView.contentOffset.y = .zero 127 | } 128 | 129 | func scrollViewWillEndDragging( 130 | _ scrollView: UIScrollView, 131 | withVelocity velocity: CGPoint, 132 | targetContentOffset: UnsafeMutablePointer 133 | ) { 134 | if representable.translation != limits.max { 135 | targetContentOffset.pointee = .zero 136 | } 137 | 138 | if let result = snapBottomSheet( 139 | representable.translation, 140 | detents, 141 | scrollView.contentOffset.y > 0 ? 0 : velocity.y 142 | ) { 143 | representable.translation = result.size 144 | representable.preferenceKey?.selectedDetent = result 145 | } 146 | 147 | scrollOffset = 0 148 | newValue = 0 149 | } 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /Sources/BottomSheet/View Modifiers/View+AnimationChange.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OnAnimationChange.swift 3 | // 4 | // 5 | // Created by Wouter van de Kamp on 10/03/2022. 6 | // 7 | 8 | import SwiftUI 9 | 10 | internal struct AnimationObserverModifier: AnimatableModifier where Value: VectorArithmetic { 11 | var animatableData: Value { 12 | didSet { 13 | updateAnimationData() 14 | } 15 | } 16 | 17 | private var update: (CGFloat) -> Void 18 | 19 | init(observedValue: Value, update: @escaping (CGFloat) -> Void) { 20 | self.animatableData = observedValue 21 | self.update = update 22 | } 23 | 24 | func body(content: Content) -> some View { 25 | return content 26 | } 27 | 28 | private func updateAnimationData() { 29 | DispatchQueue.main.async { 30 | // swiftlint:disable force_cast 31 | update(animatableData as! CGFloat) 32 | } 33 | } 34 | } 35 | 36 | extension View { 37 | func onAnimationChange( 38 | of value: Value, 39 | perform: @escaping (CGFloat) -> Void 40 | ) -> ModifiedContent> { 41 | return modifier(AnimationObserverModifier(observedValue: value, update: perform)) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Sources/BottomSheet/View Modifiers/View+BackgroundInteraction.swift: -------------------------------------------------------------------------------- 1 | // 2 | // View+BackgroundInteraction.swift 3 | // 4 | // 5 | // Created by Wouter van de Kamp on 29/10/2023. 6 | // 7 | 8 | import SwiftUI 9 | 10 | extension View { 11 | public func presentationBackgroundInteractionPlus( 12 | _ interaction: PresentationBackgroundInteractionPlus 13 | ) -> some View { 14 | return self.preference( 15 | key: SheetPlusBackgroundInteractionKey.self, 16 | value: interaction 17 | ) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Sources/BottomSheet/View Modifiers/View+Detents.swift: -------------------------------------------------------------------------------- 1 | // 2 | // View+Detents.swift 3 | // 4 | // 5 | // Created by Wouter van de Kamp on 02/07/2023. 6 | // 7 | 8 | import SwiftUI 9 | 10 | extension View { 11 | public func presentationDetentsPlus( 12 | _ detents: Set 13 | ) -> some View { 14 | let sortedDetents = Array(detents).sorted(by: { $0.size < $1.size }) 15 | 16 | return self.preference( 17 | key: SheetPlusKey.self, 18 | value: SheetPlusConfig( 19 | detents: detents, 20 | selectedDetent: Binding(get: { sortedDetents.first! }, set: { _ in }), 21 | translation: sortedDetents.first!.size 22 | ) 23 | ) 24 | } 25 | 26 | public func presentationDetentsPlus( 27 | _ detents: Set, 28 | selection: Binding 29 | ) -> some View { 30 | return self.preference( 31 | key: SheetPlusKey.self, 32 | value: SheetPlusConfig( 33 | detents: detents, 34 | selectedDetent: selection, 35 | translation: selection.wrappedValue.size 36 | ) 37 | ) 38 | } 39 | } 40 | 41 | -------------------------------------------------------------------------------- /Sources/BottomSheet/View Modifiers/View+DragIndicator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // View+DragIndicator.swift 3 | // 4 | // 5 | // Created by Wouter van de Kamp on 29/10/2023. 6 | // 7 | 8 | import SwiftUI 9 | 10 | public enum VisibilityPlus { 11 | case hidden 12 | case visible 13 | case automatic 14 | } 15 | 16 | extension View { 17 | public func presentationDragIndicatorPlus( 18 | _ visibility: VisibilityPlus 19 | ) -> some View { 20 | return self.preference( 21 | key: SheetPlusIndicatorKey.self, 22 | value: visibility 23 | ) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Sources/BottomSheet/View Modifiers/View+SheetPlus.swift: -------------------------------------------------------------------------------- 1 | // 2 | // View+SheetPlus.swift 3 | // 4 | // 5 | // Created by Wouter van de Kamp on 21/11/2022. 6 | // 7 | 8 | import SwiftUI 9 | 10 | extension View { 11 | public func sheetPlus( 12 | isPresented: Binding, 13 | animationCurve: SheetAnimation = SheetAnimation( 14 | mass: SheetAnimationDefaults.mass, 15 | stiffness: SheetAnimationDefaults.stiffness, 16 | damping: SheetAnimationDefaults.damping 17 | ), 18 | background: Background = Color(UIColor.systemBackground), 19 | onDismiss: @escaping () -> Void = {}, 20 | onDrag: @escaping (CGFloat) -> Void = { _ in }, 21 | header: () -> HContent = { EmptyView() }, 22 | main: () -> MContent 23 | ) -> some View { 24 | modifier( 25 | SheetPlus( 26 | isPresented: isPresented, 27 | animationCurve: animationCurve, 28 | background: background, 29 | onDismiss: onDismiss, 30 | onDrag: onDrag, 31 | hcontent: header, 32 | mcontent: main 33 | ) 34 | ) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Sources/BottomSheet/Views/DragIndicator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DragIndicator.swift 3 | // 4 | // 5 | // Created by Wouter van de Kamp on 29/10/2023. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct DragIndicator: View { 11 | @Binding var translation: CGFloat 12 | var detents: Set 13 | 14 | var body: some View { 15 | RoundedRectangle(cornerRadius: 3) 16 | .contentShape(Rectangle()) 17 | .frame(width: 42, height: 6) 18 | .foregroundColor(Color(UIColor.systemGray3)) 19 | .padding(.vertical, 8) 20 | .onTapGesture { 21 | let sortedDetents = detents.sorted { $0.size < $1.size } 22 | let nextDetent = sortedDetents.first(where: { $0.size > translation }) 23 | 24 | if let nextDetent = nextDetent { 25 | translation = nextDetent.size 26 | } else { 27 | translation = sortedDetents.first!.size 28 | } 29 | } 30 | } 31 | } 32 | 33 | struct DragIndicator_Previews: PreviewProvider { 34 | static var previews: some View { 35 | DragIndicator( 36 | translation: .constant(0), 37 | detents: [] 38 | ) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Tests/BottomSheetTests/BottomSheetTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import BottomSheet 3 | 4 | final class BottomSheetTests: XCTestCase { 5 | func testExample() throws { 6 | 7 | } 8 | } 9 | --------------------------------------------------------------------------------