├── .gitignore ├── DraggableOverlayExample.xcodeproj ├── project.pbxproj └── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── DraggableOverlayExample ├── AppDelegate.swift ├── Resources │ ├── Assets.xcassets │ │ ├── AccentColor.colorset │ │ │ └── Contents.json │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ └── Contents.json │ ├── Info.plist │ └── UI │ │ └── Base.lproj │ │ ├── LaunchScreen.storyboard │ │ └── Main.storyboard └── UI │ ├── ExampleDraggableDetailsContentViewController.swift │ └── ExampleDraggableDetailsOverlayViewController.swift ├── DraggableOverlayFramework.xcodeproj ├── project.pbxproj └── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── DraggableOverlayFramework └── DraggableOverlayFramework.h ├── DraggableOverlayWorkspace.xcworkspace ├── contents.xcworkspacedata └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── LICENSE ├── Podfile ├── Podfile.lock ├── README.md ├── Resources ├── draggable_overlay_example_1.gif ├── draggable_overlay_example_2.gif ├── draggable_overlay_example_3.gif ├── draggable_overlay_example_4.gif └── title_image.png ├── Shakuro.DraggableOverlay.podspec └── Source ├── DraggableDetailsOverlayHandleView.swift ├── DraggableDetailsOverlayViewController.swift └── TouchTransparentView.swift /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | */build/* 3 | *.pbxuser 4 | !default.pbxuser 5 | *.mode1v3 6 | !default.mode1v3 7 | *.mode2v3 8 | !default.mode2v3 9 | *.perspectivev3 10 | !default.perspectivev3 11 | xcuserdata 12 | .DS_Store 13 | *.moved-aside 14 | DerivedData 15 | .idea/ 16 | *.hmap 17 | *.xccheckout 18 | 19 | ## Playgrounds 20 | timeline.xctimeline 21 | playground.xcworkspace 22 | 23 | # R.Swift 24 | *.generated.swift 25 | 26 | #CocoaPods 27 | #Pods 28 | -------------------------------------------------------------------------------- /DraggableOverlayExample.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 55; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 55881E0727E06C6F005ADEFB /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55881E0627E06C6F005ADEFB /* AppDelegate.swift */; }; 11 | 55881E0E27E06C6F005ADEFB /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 55881E0C27E06C6F005ADEFB /* Main.storyboard */; }; 12 | 55881E1027E06C70005ADEFB /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 55881E0F27E06C70005ADEFB /* Assets.xcassets */; }; 13 | 55881E1327E06C70005ADEFB /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 55881E1127E06C70005ADEFB /* LaunchScreen.storyboard */; }; 14 | 55F7094C27E08368007A0A69 /* ExampleDraggableDetailsOverlayViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55F7094B27E08368007A0A69 /* ExampleDraggableDetailsOverlayViewController.swift */; }; 15 | 55F7094E27E085DD007A0A69 /* DraggableOverlayFramework.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 55F7094D27E085DD007A0A69 /* DraggableOverlayFramework.framework */; }; 16 | 55F7094F27E085DD007A0A69 /* DraggableOverlayFramework.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 55F7094D27E085DD007A0A69 /* DraggableOverlayFramework.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 17 | A5107A4A27F237D9000F558A /* ExampleDraggableDetailsContentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5107A4927F237D9000F558A /* ExampleDraggableDetailsContentViewController.swift */; }; 18 | /* End PBXBuildFile section */ 19 | 20 | /* Begin PBXCopyFilesBuildPhase section */ 21 | 55F7095027E085DD007A0A69 /* Embed Frameworks */ = { 22 | isa = PBXCopyFilesBuildPhase; 23 | buildActionMask = 2147483647; 24 | dstPath = ""; 25 | dstSubfolderSpec = 10; 26 | files = ( 27 | 55F7094F27E085DD007A0A69 /* DraggableOverlayFramework.framework in Embed Frameworks */, 28 | ); 29 | name = "Embed Frameworks"; 30 | runOnlyForDeploymentPostprocessing = 0; 31 | }; 32 | /* End PBXCopyFilesBuildPhase section */ 33 | 34 | /* Begin PBXFileReference section */ 35 | 55881E0327E06C6F005ADEFB /* DraggableOverlayExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = DraggableOverlayExample.app; sourceTree = BUILT_PRODUCTS_DIR; }; 36 | 55881E0627E06C6F005ADEFB /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 37 | 55881E0D27E06C6F005ADEFB /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 38 | 55881E0F27E06C70005ADEFB /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 39 | 55881E1227E06C70005ADEFB /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 40 | 55881E1427E06C70005ADEFB /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 41 | 55F7094B27E08368007A0A69 /* ExampleDraggableDetailsOverlayViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExampleDraggableDetailsOverlayViewController.swift; sourceTree = ""; }; 42 | 55F7094D27E085DD007A0A69 /* DraggableOverlayFramework.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = DraggableOverlayFramework.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 43 | A5107A4927F237D9000F558A /* ExampleDraggableDetailsContentViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleDraggableDetailsContentViewController.swift; sourceTree = ""; }; 44 | /* End PBXFileReference section */ 45 | 46 | /* Begin PBXFrameworksBuildPhase section */ 47 | 55881E0027E06C6F005ADEFB /* Frameworks */ = { 48 | isa = PBXFrameworksBuildPhase; 49 | buildActionMask = 2147483647; 50 | files = ( 51 | 55F7094E27E085DD007A0A69 /* DraggableOverlayFramework.framework in Frameworks */, 52 | ); 53 | runOnlyForDeploymentPostprocessing = 0; 54 | }; 55 | /* End PBXFrameworksBuildPhase section */ 56 | 57 | /* Begin PBXGroup section */ 58 | 3A95E299DF709156521010EC /* Pods */ = { 59 | isa = PBXGroup; 60 | children = ( 61 | ); 62 | path = Pods; 63 | sourceTree = ""; 64 | }; 65 | 55881DFA27E06C6F005ADEFB = { 66 | isa = PBXGroup; 67 | children = ( 68 | 55881E0527E06C6F005ADEFB /* DraggableOverlayExample */, 69 | 55881E0427E06C6F005ADEFB /* Products */, 70 | 3A95E299DF709156521010EC /* Pods */, 71 | C0BF0BD5D01213766B6AFECA /* Frameworks */, 72 | ); 73 | sourceTree = ""; 74 | }; 75 | 55881E0427E06C6F005ADEFB /* Products */ = { 76 | isa = PBXGroup; 77 | children = ( 78 | 55881E0327E06C6F005ADEFB /* DraggableOverlayExample.app */, 79 | ); 80 | name = Products; 81 | sourceTree = ""; 82 | }; 83 | 55881E0527E06C6F005ADEFB /* DraggableOverlayExample */ = { 84 | isa = PBXGroup; 85 | children = ( 86 | 55881E0627E06C6F005ADEFB /* AppDelegate.swift */, 87 | 6B65ED1F28D876530073F353 /* UI */, 88 | 6B65ED1D28D8763B0073F353 /* Resources */, 89 | ); 90 | path = DraggableOverlayExample; 91 | sourceTree = ""; 92 | }; 93 | 6B65ED1D28D8763B0073F353 /* Resources */ = { 94 | isa = PBXGroup; 95 | children = ( 96 | 55881E1427E06C70005ADEFB /* Info.plist */, 97 | 55881E0F27E06C70005ADEFB /* Assets.xcassets */, 98 | 6B65ED1E28D876480073F353 /* UI */, 99 | ); 100 | path = Resources; 101 | sourceTree = ""; 102 | }; 103 | 6B65ED1E28D876480073F353 /* UI */ = { 104 | isa = PBXGroup; 105 | children = ( 106 | 55881E1127E06C70005ADEFB /* LaunchScreen.storyboard */, 107 | 55881E0C27E06C6F005ADEFB /* Main.storyboard */, 108 | ); 109 | path = UI; 110 | sourceTree = ""; 111 | }; 112 | 6B65ED1F28D876530073F353 /* UI */ = { 113 | isa = PBXGroup; 114 | children = ( 115 | A5107A4927F237D9000F558A /* ExampleDraggableDetailsContentViewController.swift */, 116 | 55F7094B27E08368007A0A69 /* ExampleDraggableDetailsOverlayViewController.swift */, 117 | ); 118 | path = UI; 119 | sourceTree = ""; 120 | }; 121 | C0BF0BD5D01213766B6AFECA /* Frameworks */ = { 122 | isa = PBXGroup; 123 | children = ( 124 | 55F7094D27E085DD007A0A69 /* DraggableOverlayFramework.framework */, 125 | ); 126 | name = Frameworks; 127 | sourceTree = ""; 128 | }; 129 | /* End PBXGroup section */ 130 | 131 | /* Begin PBXNativeTarget section */ 132 | 55881E0227E06C6F005ADEFB /* DraggableOverlayExample */ = { 133 | isa = PBXNativeTarget; 134 | buildConfigurationList = 55881E1727E06C70005ADEFB /* Build configuration list for PBXNativeTarget "DraggableOverlayExample" */; 135 | buildPhases = ( 136 | 55881DFF27E06C6F005ADEFB /* Sources */, 137 | 55881E0027E06C6F005ADEFB /* Frameworks */, 138 | 55881E0127E06C6F005ADEFB /* Resources */, 139 | 55F7095027E085DD007A0A69 /* Embed Frameworks */, 140 | ); 141 | buildRules = ( 142 | ); 143 | dependencies = ( 144 | ); 145 | name = DraggableOverlayExample; 146 | productName = DraggableOverlayExample; 147 | productReference = 55881E0327E06C6F005ADEFB /* DraggableOverlayExample.app */; 148 | productType = "com.apple.product-type.application"; 149 | }; 150 | /* End PBXNativeTarget section */ 151 | 152 | /* Begin PBXProject section */ 153 | 55881DFB27E06C6F005ADEFB /* Project object */ = { 154 | isa = PBXProject; 155 | attributes = { 156 | BuildIndependentTargetsInParallel = 1; 157 | LastSwiftUpdateCheck = 1320; 158 | LastUpgradeCheck = 1320; 159 | TargetAttributes = { 160 | 55881E0227E06C6F005ADEFB = { 161 | CreatedOnToolsVersion = 13.2.1; 162 | }; 163 | }; 164 | }; 165 | buildConfigurationList = 55881DFE27E06C6F005ADEFB /* Build configuration list for PBXProject "DraggableOverlayExample" */; 166 | compatibilityVersion = "Xcode 13.0"; 167 | developmentRegion = en; 168 | hasScannedForEncodings = 0; 169 | knownRegions = ( 170 | en, 171 | Base, 172 | ); 173 | mainGroup = 55881DFA27E06C6F005ADEFB; 174 | productRefGroup = 55881E0427E06C6F005ADEFB /* Products */; 175 | projectDirPath = ""; 176 | projectRoot = ""; 177 | targets = ( 178 | 55881E0227E06C6F005ADEFB /* DraggableOverlayExample */, 179 | ); 180 | }; 181 | /* End PBXProject section */ 182 | 183 | /* Begin PBXResourcesBuildPhase section */ 184 | 55881E0127E06C6F005ADEFB /* Resources */ = { 185 | isa = PBXResourcesBuildPhase; 186 | buildActionMask = 2147483647; 187 | files = ( 188 | 55881E1327E06C70005ADEFB /* LaunchScreen.storyboard in Resources */, 189 | 55881E1027E06C70005ADEFB /* Assets.xcassets in Resources */, 190 | 55881E0E27E06C6F005ADEFB /* Main.storyboard in Resources */, 191 | ); 192 | runOnlyForDeploymentPostprocessing = 0; 193 | }; 194 | /* End PBXResourcesBuildPhase section */ 195 | 196 | /* Begin PBXSourcesBuildPhase section */ 197 | 55881DFF27E06C6F005ADEFB /* Sources */ = { 198 | isa = PBXSourcesBuildPhase; 199 | buildActionMask = 2147483647; 200 | files = ( 201 | 55F7094C27E08368007A0A69 /* ExampleDraggableDetailsOverlayViewController.swift in Sources */, 202 | 55881E0727E06C6F005ADEFB /* AppDelegate.swift in Sources */, 203 | A5107A4A27F237D9000F558A /* ExampleDraggableDetailsContentViewController.swift in Sources */, 204 | ); 205 | runOnlyForDeploymentPostprocessing = 0; 206 | }; 207 | /* End PBXSourcesBuildPhase section */ 208 | 209 | /* Begin PBXVariantGroup section */ 210 | 55881E0C27E06C6F005ADEFB /* Main.storyboard */ = { 211 | isa = PBXVariantGroup; 212 | children = ( 213 | 55881E0D27E06C6F005ADEFB /* Base */, 214 | ); 215 | name = Main.storyboard; 216 | sourceTree = ""; 217 | }; 218 | 55881E1127E06C70005ADEFB /* LaunchScreen.storyboard */ = { 219 | isa = PBXVariantGroup; 220 | children = ( 221 | 55881E1227E06C70005ADEFB /* Base */, 222 | ); 223 | name = LaunchScreen.storyboard; 224 | sourceTree = ""; 225 | }; 226 | /* End PBXVariantGroup section */ 227 | 228 | /* Begin XCBuildConfiguration section */ 229 | 55881E1527E06C70005ADEFB /* Debug */ = { 230 | isa = XCBuildConfiguration; 231 | buildSettings = { 232 | ALWAYS_SEARCH_USER_PATHS = NO; 233 | CLANG_ANALYZER_NONNULL = YES; 234 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 235 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; 236 | CLANG_CXX_LIBRARY = "libc++"; 237 | CLANG_ENABLE_MODULES = YES; 238 | CLANG_ENABLE_OBJC_ARC = YES; 239 | CLANG_ENABLE_OBJC_WEAK = YES; 240 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 241 | CLANG_WARN_BOOL_CONVERSION = YES; 242 | CLANG_WARN_COMMA = YES; 243 | CLANG_WARN_CONSTANT_CONVERSION = YES; 244 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 245 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 246 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 247 | CLANG_WARN_EMPTY_BODY = YES; 248 | CLANG_WARN_ENUM_CONVERSION = YES; 249 | CLANG_WARN_INFINITE_RECURSION = YES; 250 | CLANG_WARN_INT_CONVERSION = YES; 251 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 252 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 253 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 254 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 255 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 256 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 257 | CLANG_WARN_STRICT_PROTOTYPES = YES; 258 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 259 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 260 | CLANG_WARN_UNREACHABLE_CODE = YES; 261 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 262 | COPY_PHASE_STRIP = NO; 263 | DEBUG_INFORMATION_FORMAT = dwarf; 264 | ENABLE_STRICT_OBJC_MSGSEND = YES; 265 | ENABLE_TESTABILITY = YES; 266 | GCC_C_LANGUAGE_STANDARD = gnu11; 267 | GCC_DYNAMIC_NO_PIC = NO; 268 | GCC_NO_COMMON_BLOCKS = YES; 269 | GCC_OPTIMIZATION_LEVEL = 0; 270 | GCC_PREPROCESSOR_DEFINITIONS = ( 271 | "DEBUG=1", 272 | "$(inherited)", 273 | ); 274 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 275 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 276 | GCC_WARN_UNDECLARED_SELECTOR = YES; 277 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 278 | GCC_WARN_UNUSED_FUNCTION = YES; 279 | GCC_WARN_UNUSED_VARIABLE = YES; 280 | IPHONEOS_DEPLOYMENT_TARGET = 10.0; 281 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 282 | MTL_FAST_MATH = YES; 283 | ONLY_ACTIVE_ARCH = YES; 284 | SDKROOT = iphoneos; 285 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 286 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 287 | }; 288 | name = Debug; 289 | }; 290 | 55881E1627E06C70005ADEFB /* Release */ = { 291 | isa = XCBuildConfiguration; 292 | buildSettings = { 293 | ALWAYS_SEARCH_USER_PATHS = NO; 294 | CLANG_ANALYZER_NONNULL = YES; 295 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 296 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; 297 | CLANG_CXX_LIBRARY = "libc++"; 298 | CLANG_ENABLE_MODULES = YES; 299 | CLANG_ENABLE_OBJC_ARC = YES; 300 | CLANG_ENABLE_OBJC_WEAK = YES; 301 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 302 | CLANG_WARN_BOOL_CONVERSION = YES; 303 | CLANG_WARN_COMMA = YES; 304 | CLANG_WARN_CONSTANT_CONVERSION = YES; 305 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 306 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 307 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 308 | CLANG_WARN_EMPTY_BODY = YES; 309 | CLANG_WARN_ENUM_CONVERSION = YES; 310 | CLANG_WARN_INFINITE_RECURSION = YES; 311 | CLANG_WARN_INT_CONVERSION = YES; 312 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 313 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 314 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 315 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 316 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 317 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 318 | CLANG_WARN_STRICT_PROTOTYPES = YES; 319 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 320 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 321 | CLANG_WARN_UNREACHABLE_CODE = YES; 322 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 323 | COPY_PHASE_STRIP = NO; 324 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 325 | ENABLE_NS_ASSERTIONS = NO; 326 | ENABLE_STRICT_OBJC_MSGSEND = YES; 327 | GCC_C_LANGUAGE_STANDARD = gnu11; 328 | GCC_NO_COMMON_BLOCKS = YES; 329 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 330 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 331 | GCC_WARN_UNDECLARED_SELECTOR = YES; 332 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 333 | GCC_WARN_UNUSED_FUNCTION = YES; 334 | GCC_WARN_UNUSED_VARIABLE = YES; 335 | IPHONEOS_DEPLOYMENT_TARGET = 10.0; 336 | MTL_ENABLE_DEBUG_INFO = NO; 337 | MTL_FAST_MATH = YES; 338 | SDKROOT = iphoneos; 339 | SWIFT_COMPILATION_MODE = wholemodule; 340 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 341 | VALIDATE_PRODUCT = YES; 342 | }; 343 | name = Release; 344 | }; 345 | 55881E1827E06C70005ADEFB /* Debug */ = { 346 | isa = XCBuildConfiguration; 347 | buildSettings = { 348 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 349 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 350 | CODE_SIGN_STYLE = Automatic; 351 | CURRENT_PROJECT_VERSION = 1; 352 | DEVELOPMENT_TEAM = MW2UF479VW; 353 | GENERATE_INFOPLIST_FILE = YES; 354 | INFOPLIST_FILE = DraggableOverlayExample/Resources/Info.plist; 355 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 356 | INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; 357 | INFOPLIST_KEY_UIMainStoryboardFile = Main; 358 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 359 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 360 | IPHONEOS_DEPLOYMENT_TARGET = 11.0; 361 | LD_RUNPATH_SEARCH_PATHS = ( 362 | "$(inherited)", 363 | "@executable_path/Frameworks", 364 | ); 365 | MARKETING_VERSION = 1.0; 366 | PRODUCT_BUNDLE_IDENTIFIER = Shakuro.DraggableOverlayExample; 367 | PRODUCT_NAME = "$(TARGET_NAME)"; 368 | SWIFT_EMIT_LOC_STRINGS = YES; 369 | SWIFT_VERSION = 5.0; 370 | TARGETED_DEVICE_FAMILY = "1,2"; 371 | }; 372 | name = Debug; 373 | }; 374 | 55881E1927E06C70005ADEFB /* Release */ = { 375 | isa = XCBuildConfiguration; 376 | buildSettings = { 377 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 378 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 379 | CODE_SIGN_STYLE = Automatic; 380 | CURRENT_PROJECT_VERSION = 1; 381 | DEVELOPMENT_TEAM = MW2UF479VW; 382 | GENERATE_INFOPLIST_FILE = YES; 383 | INFOPLIST_FILE = DraggableOverlayExample/Resources/Info.plist; 384 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 385 | INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; 386 | INFOPLIST_KEY_UIMainStoryboardFile = Main; 387 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 388 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 389 | IPHONEOS_DEPLOYMENT_TARGET = 11.0; 390 | LD_RUNPATH_SEARCH_PATHS = ( 391 | "$(inherited)", 392 | "@executable_path/Frameworks", 393 | ); 394 | MARKETING_VERSION = 1.0; 395 | PRODUCT_BUNDLE_IDENTIFIER = Shakuro.DraggableOverlayExample; 396 | PRODUCT_NAME = "$(TARGET_NAME)"; 397 | SWIFT_EMIT_LOC_STRINGS = YES; 398 | SWIFT_VERSION = 5.0; 399 | TARGETED_DEVICE_FAMILY = "1,2"; 400 | }; 401 | name = Release; 402 | }; 403 | /* End XCBuildConfiguration section */ 404 | 405 | /* Begin XCConfigurationList section */ 406 | 55881DFE27E06C6F005ADEFB /* Build configuration list for PBXProject "DraggableOverlayExample" */ = { 407 | isa = XCConfigurationList; 408 | buildConfigurations = ( 409 | 55881E1527E06C70005ADEFB /* Debug */, 410 | 55881E1627E06C70005ADEFB /* Release */, 411 | ); 412 | defaultConfigurationIsVisible = 0; 413 | defaultConfigurationName = Release; 414 | }; 415 | 55881E1727E06C70005ADEFB /* Build configuration list for PBXNativeTarget "DraggableOverlayExample" */ = { 416 | isa = XCConfigurationList; 417 | buildConfigurations = ( 418 | 55881E1827E06C70005ADEFB /* Debug */, 419 | 55881E1927E06C70005ADEFB /* Release */, 420 | ); 421 | defaultConfigurationIsVisible = 0; 422 | defaultConfigurationName = Release; 423 | }; 424 | /* End XCConfigurationList section */ 425 | }; 426 | rootObject = 55881DFB27E06C6F005ADEFB /* Project object */; 427 | } 428 | -------------------------------------------------------------------------------- /DraggableOverlayExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /DraggableOverlayExample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /DraggableOverlayExample/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2022 Shakuro (https://shakuro.com/) 3 | // 4 | 5 | import UIKit 6 | 7 | @main 8 | class AppDelegate: UIResponder, UIApplicationDelegate { 9 | 10 | var window: UIWindow? 11 | 12 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 13 | // Override point for customization after application launch. 14 | return true 15 | } 16 | 17 | } 18 | 19 | -------------------------------------------------------------------------------- /DraggableOverlayExample/Resources/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 | -------------------------------------------------------------------------------- /DraggableOverlayExample/Resources/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 | -------------------------------------------------------------------------------- /DraggableOverlayExample/Resources/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /DraggableOverlayExample/Resources/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | UIMainStoryboardFile 6 | Main 7 | 8 | 9 | -------------------------------------------------------------------------------- /DraggableOverlayExample/Resources/UI/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 | -------------------------------------------------------------------------------- /DraggableOverlayExample/UI/ExampleDraggableDetailsContentViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2022 Shakuro (https://shakuro.com/) 3 | // 4 | 5 | import Foundation 6 | import DraggableOverlayFramework 7 | import UIKit 8 | 9 | internal protocol ExampleDraggableDetailsContentViewControllerDelegate: AnyObject { 10 | func contentDidPressCloseButton() 11 | } 12 | 13 | internal class ExampleDraggableDetailsContentViewController: UIViewController { 14 | 15 | internal weak var delegate: ExampleDraggableDetailsContentViewControllerDelegate? 16 | 17 | @IBOutlet private var topTableView: UITableView! 18 | @IBOutlet private var bottomTableView: UITableView! 19 | 20 | private var shouldPreventScrolling: Bool = false 21 | private var currentContentScrollOffsetTop: CGPoint = .zero 22 | private var currentContentScrollOffsetBottom: CGPoint = .zero 23 | 24 | internal static func instantiate() -> ExampleDraggableDetailsContentViewController { 25 | let storyboard = UIStoryboard(name: "Main", bundle: Bundle.main) 26 | let controller: ExampleDraggableDetailsContentViewController = storyboard.instantiateViewController(withIdentifier: "kExampleDraggableDetailsContentViewControllerID") 27 | return controller 28 | } 29 | 30 | override func viewDidLoad() { 31 | super.viewDidLoad() 32 | topTableView.delegate = self 33 | topTableView.dataSource = self 34 | bottomTableView.delegate = self 35 | bottomTableView.dataSource = self 36 | } 37 | 38 | @IBAction private func closeOverlayButtondidPress() { 39 | delegate?.contentDidPressCloseButton() 40 | } 41 | 42 | } 43 | 44 | // MARK: UITableViewDataSource, UITableViewDelegate 45 | 46 | extension ExampleDraggableDetailsContentViewController: UITableViewDataSource, UITableViewDelegate { 47 | 48 | func numberOfSections(in tableView: UITableView) -> Int { 49 | return 1 50 | } 51 | 52 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 53 | return 30 54 | } 55 | 56 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 57 | let cell = tableView.dequeueReusableCell(withIdentifier: "kExampleDraggableDetailsContentCellID", for: indexPath) 58 | cell.textLabel?.text = (tableView === topTableView ? "top" : "bottom") + " #\(indexPath.row)" 59 | return cell 60 | } 61 | 62 | } 63 | 64 | // MARK: UIScrollViewDelegate 65 | 66 | extension ExampleDraggableDetailsContentViewController: UIScrollViewDelegate { 67 | 68 | func scrollViewDidScroll(_ scrollView: UIScrollView) { 69 | if shouldPreventScrolling { 70 | if scrollView === topTableView { 71 | scrollView.contentOffset = currentContentScrollOffsetTop 72 | } else if scrollView === bottomTableView { 73 | scrollView.contentOffset = currentContentScrollOffsetBottom 74 | } 75 | } 76 | } 77 | 78 | func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer) { 79 | if shouldPreventScrolling { 80 | if scrollView === topTableView { 81 | targetContentOffset.pointee = currentContentScrollOffsetTop 82 | } else if scrollView === bottomTableView { 83 | targetContentOffset.pointee = currentContentScrollOffsetBottom 84 | } 85 | } 86 | } 87 | 88 | } 89 | 90 | // MARK: DraggableDetailsOverlayNestedInterface 91 | 92 | extension ExampleDraggableDetailsContentViewController: DraggableDetailsOverlayNestedInterface { 93 | 94 | func draggableDetailsOverlay(_ overlay: DraggableDetailsOverlayViewController, requirePreventOfScroll: Bool) { 95 | shouldPreventScrolling = requirePreventOfScroll 96 | topTableView.showsVerticalScrollIndicator = !requirePreventOfScroll 97 | bottomTableView.showsVerticalScrollIndicator = !requirePreventOfScroll 98 | if requirePreventOfScroll { 99 | currentContentScrollOffsetTop = topTableView.contentOffset 100 | currentContentScrollOffsetBottom = bottomTableView.contentOffset 101 | } 102 | } 103 | 104 | func draggableDetailsOverlayContentScrollViews(_ overlay: DraggableDetailsOverlayViewController) -> [UIScrollView] { 105 | return [topTableView, bottomTableView] 106 | } 107 | 108 | } 109 | -------------------------------------------------------------------------------- /DraggableOverlayExample/UI/ExampleDraggableDetailsOverlayViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2022 Shakuro (https://shakuro.com/) 3 | // 4 | 5 | import Foundation 6 | import UIKit 7 | import Shakuro_CommonTypes 8 | import DraggableOverlayFramework 9 | 10 | internal class ExampleDraggableDetailsOverlayViewController: UIViewController { 11 | 12 | @IBOutlet private var contentScrollView: UIScrollView! 13 | 14 | @IBOutlet private var presentationStyleControl: UISegmentedControl! 15 | 16 | @IBOutlet private var shadowSwitch: UISwitch! 17 | @IBOutlet private var shadowColorButton: UIButton! 18 | 19 | @IBOutlet private var draggableContainerBackgroundColorButton: UIButton! 20 | 21 | @IBOutlet private var handleColorButton: UIButton! 22 | 23 | @IBOutlet private var overlayTopInsetSlider: UISlider! 24 | @IBOutlet private var overlayMaxHeightSlider: UISlider! 25 | 26 | @IBOutlet private var draggableContainerTopCornersRadiusSlider: UISlider! 27 | @IBOutlet private var handleContainerHeightSlider: UISlider! 28 | @IBOutlet private var handleWidthSlider: UISlider! 29 | @IBOutlet private var handleHeightSlider: UISlider! 30 | @IBOutlet private var handleCornerRadiusSlider: UISlider! 31 | @IBOutlet private var showHideAnimationDurationSlider: UISlider! 32 | @IBOutlet private var bounceDragDumpeningSlider: UISlider! 33 | @IBOutlet private var snapAnimationNormalDurationSlider: UISlider! 34 | @IBOutlet private var snapAnimationSpringDurationSlider: UISlider! 35 | @IBOutlet private var snapAnimationSpringDampingSlider: UISlider! 36 | @IBOutlet private var snapAnimationSpringInitialVelocitySlider: UISlider! 37 | 38 | @IBOutlet private var draggableContainerTopCornersRadiusLabel: UILabel! 39 | @IBOutlet private var handleContainerHeightLabel: UILabel! 40 | @IBOutlet private var handleSizeLabel: UILabel! 41 | @IBOutlet private var handleCornerRadiusLabel: UILabel! 42 | @IBOutlet private var showHideAnimationDurationLabel: UILabel! 43 | @IBOutlet private var bounceDragDumpeningLabel: UILabel! 44 | @IBOutlet private var snapAnimationNormalDurationLabel: UILabel! 45 | @IBOutlet private var snapAnimationSpringDurationLabel: UILabel! 46 | @IBOutlet private var snapAnimationSpringDampingLabel: UILabel! 47 | @IBOutlet private var snapAnimationSpringInitialVelocityLabel: UILabel! 48 | @IBOutlet private var overlayTopInsetLabel: UILabel! 49 | @IBOutlet private var overlayMaxHeightLabel: UILabel! 50 | 51 | @IBOutlet private var isSnapToAnchorsEnabledSwitch: UISwitch! 52 | @IBOutlet private var isDragOffScreenToHideEnabledSwitch: UISwitch! 53 | @IBOutlet private var isBounceEnabledSwitch: UISwitch! 54 | @IBOutlet private var snapCalculationUsesDecelerationSwitch: UISwitch! 55 | @IBOutlet private var snapCalculationDecelerationCanSkipNextAnchorSwitch: UISwitch! 56 | @IBOutlet private var snapAnimationUseSpringSwitch: UISwitch! 57 | @IBOutlet private var snapAnimationTopAnchorUseSpringSwitch: UISwitch! 58 | @IBOutlet private var containerShadowSwitch: UISwitch! 59 | 60 | @IBOutlet private var snapCalculationDecelerationRateSegmentedControl: UISegmentedControl! 61 | 62 | private var contentViewController: ExampleDraggableDetailsContentViewController! 63 | private var overlayViewController: DraggableDetailsOverlayViewController! 64 | private var keyboardHandler: KeyboardHandler? 65 | 66 | override func viewDidLoad() { 67 | super.viewDidLoad() 68 | 69 | title = "Draggable overlay" 70 | contentScrollView.delegate = self 71 | 72 | contentViewController = ExampleDraggableDetailsContentViewController.instantiate() 73 | contentViewController.delegate = self 74 | overlayViewController = DraggableDetailsOverlayViewController(nestedController: contentViewController, delegate: self) 75 | 76 | contentScrollView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: 400, right: 0) 77 | 78 | overlayViewController.isShadowEnabled = shadowSwitch.isOn 79 | overlayViewController.isSnapToAnchorsEnabled = isSnapToAnchorsEnabledSwitch.isOn 80 | isDragOffScreenToHideEnabledSwitch.isOn = overlayViewController.isDragOffScreenToHideEnabled 81 | isBounceEnabledSwitch.isOn = overlayViewController.isBounceEnabled 82 | snapCalculationUsesDecelerationSwitch.isOn = overlayViewController.snapCalculationUsesDeceleration 83 | snapCalculationDecelerationCanSkipNextAnchorSwitch.isOn = overlayViewController.snapCalculationDecelerationCanSkipNextAnchor 84 | snapAnimationUseSpringSwitch.isOn = overlayViewController.snapAnimationUseSpring 85 | snapAnimationTopAnchorUseSpringSwitch.isOn = overlayViewController.snapAnimationTopAnchorUseSpring 86 | containerShadowSwitch.isOn = overlayViewController.isContainerShadowEnabled 87 | 88 | [shadowColorButton, draggableContainerBackgroundColorButton, handleColorButton].forEach { (button: UIButton) in 89 | button.setTitleShadowColor(UIColor.black, for: .normal) 90 | } 91 | shadowColorButton.setTitleColor(overlayViewController.shadowBackgroundColor, for: .normal) 92 | draggableContainerBackgroundColorButton.setTitleColor(overlayViewController.draggableContainerBackgroundColor, for: .normal) 93 | handleColorButton.setTitleColor(overlayViewController.handleColor, for: .normal) 94 | 95 | draggableContainerTopCornersRadiusSlider.value = Float(overlayViewController.draggableContainerTopCornersRadius) 96 | handleCornerRadiusSlider.value = Float(overlayViewController.handleCornerRadius) 97 | 98 | handleWidthSlider.value = Float(overlayViewController.handleSize.width) 99 | handleHeightSlider.value = Float(overlayViewController.handleSize.height) 100 | handleContainerHeightSlider.value = Float(overlayViewController.handleContainerHeight) 101 | showHideAnimationDurationSlider.value = Float(overlayViewController.showHideAnimationDuration) 102 | bounceDragDumpeningSlider.value = Float(overlayViewController.bounceDragDumpening) 103 | 104 | snapAnimationNormalDurationSlider.value = Float(overlayViewController.snapAnimationNormalDuration) 105 | snapAnimationSpringDurationSlider.value = Float(overlayViewController.snapAnimationSpringDuration) 106 | snapAnimationSpringDampingSlider.value = Float(overlayViewController.snapAnimationSpringDamping) 107 | snapAnimationSpringInitialVelocitySlider.value = Float(overlayViewController.snapAnimationSpringInitialVelocity) 108 | 109 | overlayViewController.snapCalculationDecelerationRate = snapCalculationDecelerationRateSegmentedControl.selectedSegmentIndex == 0 ? .normal : .fast 110 | 111 | keyboardHandler = KeyboardHandler(enableCurveHack: false, heightDidChange: { [weak self] (change: KeyboardHandler.KeyboardChange) in 112 | guard let strongSelf = self else { 113 | return 114 | } 115 | UIView.animate( 116 | withDuration: change.animationDuration, 117 | delay: 0.0, 118 | animations: { 119 | UIView.setAnimationCurve(change.animationCurve) 120 | strongSelf.contentScrollView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: change.newHeight, right: 0) 121 | strongSelf.view.layoutIfNeeded() 122 | }, 123 | completion: nil) 124 | }) 125 | keyboardHandler?.isActive = true 126 | updateSliderLabels() 127 | } 128 | 129 | override func viewDidLayoutSubviews() { 130 | super.viewDidLayoutSubviews() 131 | let maxValue: Float = Float(view.bounds.size.height) 132 | if maxValue != overlayTopInsetSlider.maximumValue || maxValue != overlayMaxHeightSlider.maximumValue { 133 | overlayTopInsetSlider.maximumValue = maxValue 134 | overlayMaxHeightSlider.maximumValue = maxValue 135 | overlayViewController.updateLayout(animated: false) 136 | updateSliderLabels() 137 | } 138 | } 139 | 140 | @IBAction private func showOverlayButtonDidPress() { 141 | view.endEditing(true) 142 | showOverlay() 143 | } 144 | 145 | @IBAction private func switchValueChanged(_ sender: UISwitch) { 146 | switch sender { 147 | case shadowSwitch: 148 | overlayViewController.isShadowEnabled = shadowSwitch.isOn 149 | case isSnapToAnchorsEnabledSwitch: 150 | overlayViewController.isSnapToAnchorsEnabled = isSnapToAnchorsEnabledSwitch.isOn 151 | case isDragOffScreenToHideEnabledSwitch: 152 | overlayViewController.isDragOffScreenToHideEnabled = isDragOffScreenToHideEnabledSwitch.isOn 153 | case isBounceEnabledSwitch: 154 | overlayViewController.isBounceEnabled = isBounceEnabledSwitch.isOn 155 | case snapCalculationUsesDecelerationSwitch: 156 | overlayViewController.snapCalculationUsesDeceleration = snapCalculationUsesDecelerationSwitch.isOn 157 | case snapCalculationDecelerationCanSkipNextAnchorSwitch: 158 | overlayViewController.snapCalculationDecelerationCanSkipNextAnchor = snapCalculationDecelerationCanSkipNextAnchorSwitch.isOn 159 | case snapAnimationTopAnchorUseSpringSwitch: 160 | overlayViewController.snapAnimationTopAnchorUseSpring = snapAnimationTopAnchorUseSpringSwitch.isOn 161 | case snapAnimationUseSpringSwitch: 162 | overlayViewController.snapAnimationUseSpring = snapAnimationUseSpringSwitch.isOn 163 | case containerShadowSwitch: 164 | overlayViewController.isContainerShadowEnabled = containerShadowSwitch.isOn 165 | default: 166 | assertionFailure("unknown switch") 167 | } 168 | } 169 | 170 | @IBAction private func segmentedControlValueChanged(_ sender: UISegmentedControl) { 171 | switch sender { 172 | case snapCalculationDecelerationRateSegmentedControl: 173 | overlayViewController.snapCalculationDecelerationRate = snapCalculationDecelerationRateSegmentedControl.selectedSegmentIndex == 0 ? .normal : .fast 174 | case presentationStyleControl: 175 | showOverlay() 176 | default: 177 | assertionFailure("unknown segmented control") 178 | } 179 | } 180 | 181 | @IBAction private func sliderValueChanged(_ sender: UISlider) { 182 | switch sender { 183 | case draggableContainerTopCornersRadiusSlider: 184 | overlayViewController.draggableContainerTopCornersRadius = CGFloat(sender.value) 185 | case handleCornerRadiusSlider: 186 | overlayViewController.handleCornerRadius = CGFloat(sender.value) 187 | case handleWidthSlider, handleHeightSlider: 188 | overlayViewController.handleSize = CGSize(width: CGFloat(handleWidthSlider.value), height: CGFloat(handleHeightSlider.value)) 189 | case handleContainerHeightSlider: 190 | overlayViewController.handleContainerHeight = CGFloat(handleContainerHeightSlider.value) 191 | case showHideAnimationDurationSlider: 192 | overlayViewController.showHideAnimationDuration = TimeInterval(showHideAnimationDurationSlider.value) 193 | case bounceDragDumpeningSlider: 194 | overlayViewController.bounceDragDumpening = CGFloat(bounceDragDumpeningSlider.value) 195 | case snapAnimationNormalDurationSlider: 196 | overlayViewController.snapAnimationNormalDuration = TimeInterval(snapAnimationNormalDurationSlider.value) 197 | case snapAnimationSpringDurationSlider: 198 | overlayViewController.snapAnimationSpringDuration = TimeInterval(snapAnimationSpringDurationSlider.value) 199 | case snapAnimationSpringDampingSlider: 200 | overlayViewController.snapAnimationSpringDamping = CGFloat(snapAnimationSpringDampingSlider.value) 201 | case snapAnimationSpringInitialVelocitySlider: 202 | overlayViewController.snapAnimationSpringInitialVelocity = CGFloat(snapAnimationSpringInitialVelocitySlider.value) 203 | case overlayMaxHeightSlider, overlayTopInsetSlider: 204 | overlayViewController.updateLayout(animated: true) 205 | default: 206 | assertionFailure("unknown slider") 207 | } 208 | updateSliderLabels() 209 | } 210 | 211 | @IBAction private func changeShadowColor(_ sender: UIButton) { 212 | overlayViewController.shadowBackgroundColor = UIColor.random(alpha: 0.5) 213 | shadowColorButton.setTitleColor(overlayViewController.shadowBackgroundColor.withAlphaComponent(1.0), for: .normal) 214 | } 215 | 216 | @IBAction private func draggableContainerBackgroundColorButtonPressed(_ sender: UIButton) { 217 | overlayViewController.draggableContainerBackgroundColor = UIColor.random(alpha: 1.0) 218 | draggableContainerBackgroundColorButton.setTitleColor(overlayViewController.draggableContainerBackgroundColor, for: .normal) 219 | } 220 | 221 | @IBAction private func handleColorButtonPressed(_ sender: UIButton) { 222 | overlayViewController.handleColor = UIColor.random(alpha: 1.0) 223 | handleColorButton.setTitleColor(overlayViewController.handleColor, for: .normal) 224 | } 225 | } 226 | 227 | // MARK: ExampleDraggableDetailsContentViewControllerDelegate 228 | 229 | extension ExampleDraggableDetailsOverlayViewController: ExampleDraggableDetailsContentViewControllerDelegate { 230 | 231 | func contentDidPressCloseButton() { 232 | overlayViewController.hide(animated: true) 233 | } 234 | 235 | } 236 | 237 | // MARK: DraggableDetailsOverlayViewControllerDelegate 238 | 239 | extension ExampleDraggableDetailsOverlayViewController: DraggableDetailsOverlayViewControllerDelegate { 240 | 241 | func draggableDetailsOverlayAnchors(_ overlay: DraggableDetailsOverlayViewController) -> [DraggableDetailsOverlayViewController.Anchor] { 242 | return [ 243 | DraggableDetailsOverlayViewController.Anchor(topOffset: 40, tag: 1), 244 | DraggableDetailsOverlayViewController.Anchor(height: 300, tag: 2), 245 | DraggableDetailsOverlayViewController.Anchor(height: 100, tag: 3) 246 | ] 247 | } 248 | 249 | func draggableDetailsOverlayTopInset(_ overlay: DraggableDetailsOverlayViewController) -> CGFloat { 250 | return CGFloat(overlayTopInsetSlider.value) 251 | } 252 | 253 | func draggableDetailsOverlayMaxHeight(_ overlay: DraggableDetailsOverlayViewController) -> CGFloat? { 254 | return CGFloat(overlayMaxHeightSlider.value) 255 | } 256 | 257 | func draggableDetailsOverlayDidDrag(_ overlay: DraggableDetailsOverlayViewController) { 258 | print("did drag") 259 | } 260 | 261 | func draggableDetailsOverlayDidEndDragging(_ overlay: DraggableDetailsOverlayViewController) { 262 | print("did end dragging") 263 | } 264 | 265 | func draggableDetailsOverlayDidChangeIsVisible(_ overlay: DraggableDetailsOverlayViewController) { 266 | if !overlay.isVisible, presentedViewController != nil { 267 | dismiss(animated: false, completion: nil) 268 | } 269 | } 270 | 271 | func draggableDetailsOverlayDidUpdatedLayout(_ overlay: DraggableDetailsOverlayViewController) { 272 | print("did update layout") 273 | } 274 | 275 | func draggableDetailsOverlayWillDragOffScreenToHide(_ overlay: DraggableDetailsOverlayViewController) { 276 | print("will drag offscreen") 277 | } 278 | 279 | func draggableDetailsOverlayDidHideByShadowTap(_ overlay: DraggableDetailsOverlayViewController) { 280 | print("will hide by shadow tap") 281 | } 282 | 283 | func draggableDetailsOverlay(_ overlay: DraggableDetailsOverlayViewController, willAnimateEndDragToNearestAnchor anchor: DraggableDetailsOverlayViewController.Anchor) { 284 | print("will animate end drag to nearest anchor: \(anchor)") 285 | } 286 | 287 | } 288 | 289 | // MARK: UIScrollViewDelegate 290 | 291 | extension ExampleDraggableDetailsOverlayViewController: UIScrollViewDelegate { 292 | 293 | func scrollViewDidScroll(_ scrollView: UIScrollView) { 294 | if scrollView === contentScrollView { 295 | view.endEditing(false) 296 | } 297 | } 298 | 299 | } 300 | 301 | private extension ExampleDraggableDetailsOverlayViewController { 302 | 303 | func showOverlay() { 304 | guard presentedViewController == nil else { 305 | dismiss(animated: true) { [weak self] in 306 | guard let actualSelf = self else { 307 | return 308 | } 309 | actualSelf.showOverlay() 310 | } 311 | return 312 | } 313 | let selectedSegment = presentationStyleControl.selectedSegmentIndex 314 | switch selectedSegment { 315 | case 0: 316 | addChildViewController(overlayViewController, notifyAboutAppearanceTransition: true) 317 | overlayViewController.view.layoutIfNeeded() 318 | overlayViewController.hide(animated: false) 319 | overlayViewController.show(initialAnchor: DraggableDetailsOverlayViewController.Anchor(height: 400, tag: 4), animated: true) 320 | case 1, 2: 321 | overlayViewController.hide(animated: false) 322 | if overlayViewController.parent != nil { 323 | overlayViewController.removeFromParentViewController(notifyAboutAppearanceTransition: true) 324 | overlayViewController.hide(animated: false) 325 | } 326 | overlayViewController.modalPresentationStyle = .overCurrentContext 327 | if selectedSegment == 1 { 328 | overlayViewController.isShadowEnabled = false 329 | overlayViewController.modalTransitionStyle = .coverVertical 330 | overlayViewController.show(initialAnchor: DraggableDetailsOverlayViewController.Anchor(height: 400, tag: 4), animated: false) 331 | present(overlayViewController, animated: true, completion: nil) 332 | } else { 333 | overlayViewController.isShadowEnabled = true 334 | overlayViewController.modalTransitionStyle = .crossDissolve 335 | present(overlayViewController, animated: true) { 336 | self.overlayViewController.show(initialAnchor: DraggableDetailsOverlayViewController.Anchor(height: 400, tag: 4), animated: true) 337 | } 338 | } 339 | default: 340 | break 341 | } 342 | 343 | } 344 | 345 | func updateSliderLabels() { 346 | draggableContainerTopCornersRadiusLabel.text = String(format: "%.1f", draggableContainerTopCornersRadiusSlider.value) 347 | handleContainerHeightLabel.text = String(format: "%.1f", handleContainerHeightSlider.value) 348 | handleSizeLabel.text = String(format: "W: %.1f; H: %.1f", handleWidthSlider.value, handleHeightSlider.value) 349 | handleCornerRadiusLabel.text = String(format: "%.1f", handleCornerRadiusSlider.value) 350 | showHideAnimationDurationLabel.text = String(format: "%.2f", showHideAnimationDurationSlider.value) 351 | bounceDragDumpeningLabel.text = String(format: "%.2f", bounceDragDumpeningSlider.value) 352 | snapAnimationNormalDurationLabel.text = String(format: "%.2f", snapAnimationNormalDurationSlider.value) 353 | snapAnimationSpringDurationLabel.text = String(format: "%.2f", snapAnimationSpringDurationSlider.value) 354 | snapAnimationSpringDampingLabel.text = String(format: "%.2f", snapAnimationSpringDampingSlider.value) 355 | snapAnimationSpringInitialVelocityLabel.text = String(format: "%.2f", snapAnimationSpringInitialVelocitySlider.value) 356 | overlayTopInsetLabel.text = String(format: "%.1f", overlayTopInsetSlider.value) 357 | overlayMaxHeightLabel.text = String(format: "%.1f", overlayMaxHeightSlider.value) 358 | } 359 | } 360 | -------------------------------------------------------------------------------- /DraggableOverlayFramework.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 55; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 55881E2827E07267005ADEFB /* DraggableOverlayFramework.h in Headers */ = {isa = PBXBuildFile; fileRef = 55881E2727E07267005ADEFB /* DraggableOverlayFramework.h */; settings = {ATTRIBUTES = (Public, ); }; }; 11 | 55881E3227E073D7005ADEFB /* TouchTransparentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55881E2F27E073D7005ADEFB /* TouchTransparentView.swift */; }; 12 | 55881E3327E073D7005ADEFB /* DraggableDetailsOverlayViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55881E3027E073D7005ADEFB /* DraggableDetailsOverlayViewController.swift */; }; 13 | 55881E3427E073D7005ADEFB /* DraggableDetailsOverlayHandleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55881E3127E073D7005ADEFB /* DraggableDetailsOverlayHandleView.swift */; }; 14 | /* End PBXBuildFile section */ 15 | 16 | /* Begin PBXFileReference section */ 17 | 55881E2427E07267005ADEFB /* DraggableOverlayFramework.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = DraggableOverlayFramework.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 18 | 55881E2727E07267005ADEFB /* DraggableOverlayFramework.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = DraggableOverlayFramework.h; sourceTree = ""; }; 19 | 55881E2F27E073D7005ADEFB /* TouchTransparentView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TouchTransparentView.swift; sourceTree = ""; }; 20 | 55881E3027E073D7005ADEFB /* DraggableDetailsOverlayViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DraggableDetailsOverlayViewController.swift; sourceTree = ""; }; 21 | 55881E3127E073D7005ADEFB /* DraggableDetailsOverlayHandleView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DraggableDetailsOverlayHandleView.swift; sourceTree = ""; }; 22 | /* End PBXFileReference section */ 23 | 24 | /* Begin PBXFrameworksBuildPhase section */ 25 | 55881E2127E07267005ADEFB /* Frameworks */ = { 26 | isa = PBXFrameworksBuildPhase; 27 | buildActionMask = 2147483647; 28 | files = ( 29 | ); 30 | runOnlyForDeploymentPostprocessing = 0; 31 | }; 32 | /* End PBXFrameworksBuildPhase section */ 33 | 34 | /* Begin PBXGroup section */ 35 | 431480235459EE8F5A101C9D /* Pods */ = { 36 | isa = PBXGroup; 37 | children = ( 38 | ); 39 | path = Pods; 40 | sourceTree = ""; 41 | }; 42 | 55881E1A27E07267005ADEFB = { 43 | isa = PBXGroup; 44 | children = ( 45 | 55881E2E27E073D7005ADEFB /* Source */, 46 | 55881E2627E07267005ADEFB /* DraggableOverlayFramework */, 47 | 55881E2527E07267005ADEFB /* Products */, 48 | 431480235459EE8F5A101C9D /* Pods */, 49 | ); 50 | sourceTree = ""; 51 | }; 52 | 55881E2527E07267005ADEFB /* Products */ = { 53 | isa = PBXGroup; 54 | children = ( 55 | 55881E2427E07267005ADEFB /* DraggableOverlayFramework.framework */, 56 | ); 57 | name = Products; 58 | sourceTree = ""; 59 | }; 60 | 55881E2627E07267005ADEFB /* DraggableOverlayFramework */ = { 61 | isa = PBXGroup; 62 | children = ( 63 | 55881E2727E07267005ADEFB /* DraggableOverlayFramework.h */, 64 | ); 65 | path = DraggableOverlayFramework; 66 | sourceTree = ""; 67 | }; 68 | 55881E2E27E073D7005ADEFB /* Source */ = { 69 | isa = PBXGroup; 70 | children = ( 71 | 55881E2F27E073D7005ADEFB /* TouchTransparentView.swift */, 72 | 55881E3027E073D7005ADEFB /* DraggableDetailsOverlayViewController.swift */, 73 | 55881E3127E073D7005ADEFB /* DraggableDetailsOverlayHandleView.swift */, 74 | ); 75 | path = Source; 76 | sourceTree = SOURCE_ROOT; 77 | }; 78 | /* End PBXGroup section */ 79 | 80 | /* Begin PBXHeadersBuildPhase section */ 81 | 55881E1F27E07267005ADEFB /* Headers */ = { 82 | isa = PBXHeadersBuildPhase; 83 | buildActionMask = 2147483647; 84 | files = ( 85 | 55881E2827E07267005ADEFB /* DraggableOverlayFramework.h in Headers */, 86 | ); 87 | runOnlyForDeploymentPostprocessing = 0; 88 | }; 89 | /* End PBXHeadersBuildPhase section */ 90 | 91 | /* Begin PBXNativeTarget section */ 92 | 55881E2327E07267005ADEFB /* DraggableOverlayFramework */ = { 93 | isa = PBXNativeTarget; 94 | buildConfigurationList = 55881E2B27E07267005ADEFB /* Build configuration list for PBXNativeTarget "DraggableOverlayFramework" */; 95 | buildPhases = ( 96 | 55881E1F27E07267005ADEFB /* Headers */, 97 | 55881E2027E07267005ADEFB /* Sources */, 98 | 55881E2127E07267005ADEFB /* Frameworks */, 99 | 55881E2227E07267005ADEFB /* Resources */, 100 | ); 101 | buildRules = ( 102 | ); 103 | dependencies = ( 104 | ); 105 | name = DraggableOverlayFramework; 106 | productName = DraggableOverlayFramework; 107 | productReference = 55881E2427E07267005ADEFB /* DraggableOverlayFramework.framework */; 108 | productType = "com.apple.product-type.framework"; 109 | }; 110 | /* End PBXNativeTarget section */ 111 | 112 | /* Begin PBXProject section */ 113 | 55881E1B27E07267005ADEFB /* Project object */ = { 114 | isa = PBXProject; 115 | attributes = { 116 | BuildIndependentTargetsInParallel = 1; 117 | LastUpgradeCheck = 1320; 118 | TargetAttributes = { 119 | 55881E2327E07267005ADEFB = { 120 | CreatedOnToolsVersion = 13.2.1; 121 | }; 122 | }; 123 | }; 124 | buildConfigurationList = 55881E1E27E07267005ADEFB /* Build configuration list for PBXProject "DraggableOverlayFramework" */; 125 | compatibilityVersion = "Xcode 13.0"; 126 | developmentRegion = en; 127 | hasScannedForEncodings = 0; 128 | knownRegions = ( 129 | en, 130 | Base, 131 | ); 132 | mainGroup = 55881E1A27E07267005ADEFB; 133 | productRefGroup = 55881E2527E07267005ADEFB /* Products */; 134 | projectDirPath = ""; 135 | projectRoot = ""; 136 | targets = ( 137 | 55881E2327E07267005ADEFB /* DraggableOverlayFramework */, 138 | ); 139 | }; 140 | /* End PBXProject section */ 141 | 142 | /* Begin PBXResourcesBuildPhase section */ 143 | 55881E2227E07267005ADEFB /* Resources */ = { 144 | isa = PBXResourcesBuildPhase; 145 | buildActionMask = 2147483647; 146 | files = ( 147 | ); 148 | runOnlyForDeploymentPostprocessing = 0; 149 | }; 150 | /* End PBXResourcesBuildPhase section */ 151 | 152 | /* Begin PBXSourcesBuildPhase section */ 153 | 55881E2027E07267005ADEFB /* Sources */ = { 154 | isa = PBXSourcesBuildPhase; 155 | buildActionMask = 2147483647; 156 | files = ( 157 | 55881E3327E073D7005ADEFB /* DraggableDetailsOverlayViewController.swift in Sources */, 158 | 55881E3427E073D7005ADEFB /* DraggableDetailsOverlayHandleView.swift in Sources */, 159 | 55881E3227E073D7005ADEFB /* TouchTransparentView.swift in Sources */, 160 | ); 161 | runOnlyForDeploymentPostprocessing = 0; 162 | }; 163 | /* End PBXSourcesBuildPhase section */ 164 | 165 | /* Begin XCBuildConfiguration section */ 166 | 55881E2927E07267005ADEFB /* Debug */ = { 167 | isa = XCBuildConfiguration; 168 | buildSettings = { 169 | ALWAYS_SEARCH_USER_PATHS = NO; 170 | CLANG_ANALYZER_NONNULL = YES; 171 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 172 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; 173 | CLANG_CXX_LIBRARY = "libc++"; 174 | CLANG_ENABLE_MODULES = YES; 175 | CLANG_ENABLE_OBJC_ARC = YES; 176 | CLANG_ENABLE_OBJC_WEAK = YES; 177 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 178 | CLANG_WARN_BOOL_CONVERSION = YES; 179 | CLANG_WARN_COMMA = YES; 180 | CLANG_WARN_CONSTANT_CONVERSION = YES; 181 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 182 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 183 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 184 | CLANG_WARN_EMPTY_BODY = YES; 185 | CLANG_WARN_ENUM_CONVERSION = YES; 186 | CLANG_WARN_INFINITE_RECURSION = YES; 187 | CLANG_WARN_INT_CONVERSION = YES; 188 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 189 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 190 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 191 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 192 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 193 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 194 | CLANG_WARN_STRICT_PROTOTYPES = YES; 195 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 196 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 197 | CLANG_WARN_UNREACHABLE_CODE = YES; 198 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 199 | COPY_PHASE_STRIP = NO; 200 | CURRENT_PROJECT_VERSION = 1; 201 | DEBUG_INFORMATION_FORMAT = dwarf; 202 | ENABLE_STRICT_OBJC_MSGSEND = YES; 203 | ENABLE_TESTABILITY = YES; 204 | GCC_C_LANGUAGE_STANDARD = gnu11; 205 | GCC_DYNAMIC_NO_PIC = NO; 206 | GCC_NO_COMMON_BLOCKS = YES; 207 | GCC_OPTIMIZATION_LEVEL = 0; 208 | GCC_PREPROCESSOR_DEFINITIONS = ( 209 | "DEBUG=1", 210 | "$(inherited)", 211 | ); 212 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 213 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 214 | GCC_WARN_UNDECLARED_SELECTOR = YES; 215 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 216 | GCC_WARN_UNUSED_FUNCTION = YES; 217 | GCC_WARN_UNUSED_VARIABLE = YES; 218 | IPHONEOS_DEPLOYMENT_TARGET = 10.0; 219 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 220 | MTL_FAST_MATH = YES; 221 | ONLY_ACTIVE_ARCH = YES; 222 | SDKROOT = iphoneos; 223 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 224 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 225 | VERSIONING_SYSTEM = "apple-generic"; 226 | VERSION_INFO_PREFIX = ""; 227 | }; 228 | name = Debug; 229 | }; 230 | 55881E2A27E07267005ADEFB /* Release */ = { 231 | isa = XCBuildConfiguration; 232 | buildSettings = { 233 | ALWAYS_SEARCH_USER_PATHS = NO; 234 | CLANG_ANALYZER_NONNULL = YES; 235 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 236 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; 237 | CLANG_CXX_LIBRARY = "libc++"; 238 | CLANG_ENABLE_MODULES = YES; 239 | CLANG_ENABLE_OBJC_ARC = YES; 240 | CLANG_ENABLE_OBJC_WEAK = YES; 241 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 242 | CLANG_WARN_BOOL_CONVERSION = YES; 243 | CLANG_WARN_COMMA = YES; 244 | CLANG_WARN_CONSTANT_CONVERSION = YES; 245 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 246 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 247 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 248 | CLANG_WARN_EMPTY_BODY = YES; 249 | CLANG_WARN_ENUM_CONVERSION = YES; 250 | CLANG_WARN_INFINITE_RECURSION = YES; 251 | CLANG_WARN_INT_CONVERSION = YES; 252 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 253 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 254 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 255 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 256 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 257 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 258 | CLANG_WARN_STRICT_PROTOTYPES = YES; 259 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 260 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 261 | CLANG_WARN_UNREACHABLE_CODE = YES; 262 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 263 | COPY_PHASE_STRIP = NO; 264 | CURRENT_PROJECT_VERSION = 1; 265 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 266 | ENABLE_NS_ASSERTIONS = NO; 267 | ENABLE_STRICT_OBJC_MSGSEND = YES; 268 | GCC_C_LANGUAGE_STANDARD = gnu11; 269 | GCC_NO_COMMON_BLOCKS = YES; 270 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 271 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 272 | GCC_WARN_UNDECLARED_SELECTOR = YES; 273 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 274 | GCC_WARN_UNUSED_FUNCTION = YES; 275 | GCC_WARN_UNUSED_VARIABLE = YES; 276 | IPHONEOS_DEPLOYMENT_TARGET = 10.0; 277 | MTL_ENABLE_DEBUG_INFO = NO; 278 | MTL_FAST_MATH = YES; 279 | SDKROOT = iphoneos; 280 | SWIFT_COMPILATION_MODE = wholemodule; 281 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 282 | VALIDATE_PRODUCT = YES; 283 | VERSIONING_SYSTEM = "apple-generic"; 284 | VERSION_INFO_PREFIX = ""; 285 | }; 286 | name = Release; 287 | }; 288 | 55881E2C27E07267005ADEFB /* Debug */ = { 289 | isa = XCBuildConfiguration; 290 | buildSettings = { 291 | CODE_SIGN_STYLE = Automatic; 292 | CURRENT_PROJECT_VERSION = 1; 293 | DEFINES_MODULE = YES; 294 | DEVELOPMENT_TEAM = MW2UF479VW; 295 | DYLIB_COMPATIBILITY_VERSION = 1; 296 | DYLIB_CURRENT_VERSION = 1; 297 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 298 | GENERATE_INFOPLIST_FILE = YES; 299 | INFOPLIST_KEY_NSHumanReadableCopyright = ""; 300 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 301 | IPHONEOS_DEPLOYMENT_TARGET = 11.0; 302 | LD_RUNPATH_SEARCH_PATHS = ( 303 | "$(inherited)", 304 | "@executable_path/Frameworks", 305 | "@loader_path/Frameworks", 306 | ); 307 | MARKETING_VERSION = 1.0; 308 | PRODUCT_BUNDLE_IDENTIFIER = Shakuro.DraggableOverlayFramework; 309 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; 310 | SKIP_INSTALL = YES; 311 | SWIFT_EMIT_LOC_STRINGS = YES; 312 | SWIFT_VERSION = 5.0; 313 | TARGETED_DEVICE_FAMILY = "1,2"; 314 | }; 315 | name = Debug; 316 | }; 317 | 55881E2D27E07267005ADEFB /* Release */ = { 318 | isa = XCBuildConfiguration; 319 | buildSettings = { 320 | CODE_SIGN_STYLE = Automatic; 321 | CURRENT_PROJECT_VERSION = 1; 322 | DEFINES_MODULE = YES; 323 | DEVELOPMENT_TEAM = MW2UF479VW; 324 | DYLIB_COMPATIBILITY_VERSION = 1; 325 | DYLIB_CURRENT_VERSION = 1; 326 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 327 | GENERATE_INFOPLIST_FILE = YES; 328 | INFOPLIST_KEY_NSHumanReadableCopyright = ""; 329 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 330 | IPHONEOS_DEPLOYMENT_TARGET = 11.0; 331 | LD_RUNPATH_SEARCH_PATHS = ( 332 | "$(inherited)", 333 | "@executable_path/Frameworks", 334 | "@loader_path/Frameworks", 335 | ); 336 | MARKETING_VERSION = 1.0; 337 | PRODUCT_BUNDLE_IDENTIFIER = Shakuro.DraggableOverlayFramework; 338 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; 339 | SKIP_INSTALL = YES; 340 | SWIFT_EMIT_LOC_STRINGS = YES; 341 | SWIFT_VERSION = 5.0; 342 | TARGETED_DEVICE_FAMILY = "1,2"; 343 | }; 344 | name = Release; 345 | }; 346 | /* End XCBuildConfiguration section */ 347 | 348 | /* Begin XCConfigurationList section */ 349 | 55881E1E27E07267005ADEFB /* Build configuration list for PBXProject "DraggableOverlayFramework" */ = { 350 | isa = XCConfigurationList; 351 | buildConfigurations = ( 352 | 55881E2927E07267005ADEFB /* Debug */, 353 | 55881E2A27E07267005ADEFB /* Release */, 354 | ); 355 | defaultConfigurationIsVisible = 0; 356 | defaultConfigurationName = Release; 357 | }; 358 | 55881E2B27E07267005ADEFB /* Build configuration list for PBXNativeTarget "DraggableOverlayFramework" */ = { 359 | isa = XCConfigurationList; 360 | buildConfigurations = ( 361 | 55881E2C27E07267005ADEFB /* Debug */, 362 | 55881E2D27E07267005ADEFB /* Release */, 363 | ); 364 | defaultConfigurationIsVisible = 0; 365 | defaultConfigurationName = Release; 366 | }; 367 | /* End XCConfigurationList section */ 368 | }; 369 | rootObject = 55881E1B27E07267005ADEFB /* Project object */; 370 | } 371 | -------------------------------------------------------------------------------- /DraggableOverlayFramework.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /DraggableOverlayFramework.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /DraggableOverlayFramework/DraggableOverlayFramework.h: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2022 Shakuro (https://shakuro.com/) 3 | // 4 | 5 | #import 6 | 7 | //! Project version number for DraggableOverlayFramework. 8 | FOUNDATION_EXPORT double DraggableOverlayFrameworkVersionNumber; 9 | 10 | //! Project version string for DraggableOverlayFramework. 11 | FOUNDATION_EXPORT const unsigned char DraggableOverlayFrameworkVersionString[]; 12 | 13 | // In this header, you should import all the public headers of your framework using statements like #import 14 | 15 | 16 | -------------------------------------------------------------------------------- /DraggableOverlayWorkspace.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /DraggableOverlayWorkspace.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019-2022 Shakuro (https://shakuro.com/) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /Podfile: -------------------------------------------------------------------------------- 1 | source 'https://github.com/CocoaPods/Specs.git' 2 | 3 | platform :ios, '11.0' 4 | 5 | use_frameworks! 6 | 7 | workspace 'DraggableOverlayWorkspace' 8 | 9 | target 'DraggableOverlayFramework' do 10 | project 'DraggableOverlayFramework.xcodeproj' 11 | pod 'Shakuro.CommonTypes', '1.1.4' 12 | end 13 | 14 | target 'DraggableOverlayExample' do 15 | project 'DraggableOverlayExample.xcodeproj' 16 | pod 'SwiftLint', '0.43.1' 17 | pod 'Shakuro.CommonTypes', '1.1.4' 18 | end -------------------------------------------------------------------------------- /Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - Shakuro.CommonTypes (1.1.4) 3 | - SwiftLint (0.43.1) 4 | 5 | DEPENDENCIES: 6 | - Shakuro.CommonTypes (= 1.1.4) 7 | - SwiftLint (= 0.43.1) 8 | 9 | SPEC REPOS: 10 | https://github.com/CocoaPods/Specs.git: 11 | - Shakuro.CommonTypes 12 | - SwiftLint 13 | 14 | SPEC CHECKSUMS: 15 | Shakuro.CommonTypes: a3c3d432a2fc19e3e7971dad13aa9066d7ce5771 16 | SwiftLint: 99f82d07b837b942dd563c668de129a03fc3fb52 17 | 18 | PODFILE CHECKSUM: ff3b896d2ce42f432ca5ec70cfbe633f96f24a25 19 | 20 | COCOAPODS: 1.11.3 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Shakuro Draggable Overlay](Resources/title_image.png) 2 |

3 | # DraggableOverlay 4 | ![Version](https://img.shields.io/badge/version-1.0.0-blue.svg) 5 | ![Platform](https://img.shields.io/badge/platform-iOS-lightgrey.svg) 6 | ![License MIT](https://img.shields.io/badge/license-MIT-green.svg) 7 | 8 | - [Requirements](#requirements) 9 | - [Installation](#installation) 10 | - [Usage](#usage) 11 | - [License](#license) 12 | 13 | A `DraggableOverlay` is a Swift library - an overlay that dynamically reveals or hides the content inside it. It can be dragged up and down to stick to predefined anchors. Whenever a drag gesture ends, the overlay motion will continue until it reaches one of its anchors. `DraggableOverlay` has various configuration options. 14 | 15 | `DraggableOverlay` example with default options: 16 | 17 | ![](Resources/draggable_overlay_example_1.gif) 18 | 19 | `DraggableOverlay` example with enabled shadow (red color) and container shadow (green color), customized draggable container height (30 px): 20 | 21 | ![](Resources/draggable_overlay_example_2.gif) 22 | 23 | `DraggableOverlay` example with custom handle corner and custom handle container corner radius, customized handle color (yellow) and changed top inset: 24 | 25 | ![](Resources/draggable_overlay_example_3.gif) 26 | 27 | `DraggableOverlay` example with bounce animation: 28 | 29 | ![](Resources/draggable_overlay_example_4.gif) 30 | 31 | ## Requirements 32 | 33 | - iOS 11.0+ 34 | - Xcode 11.0+ 35 | - Swift 5.0+ 36 | 37 | ## Installation 38 | 39 | ### CocoaPods 40 | 41 | To integrate `DraggableOverlay` into your Xcode project with CocoaPods, specify it in your `Podfile`: 42 | 43 | ```ruby 44 | pod 'Shakuro.DraggableOverlay' 45 | ``` 46 | 47 | Then, run the following command: 48 | 49 | ```bash 50 | $ pod install 51 | ``` 52 | 53 | ### Manually 54 | 55 | If you prefer not to use CocoaPods, you can integrate Shakuro.DraggableOverlay simply by copying it to your project. 56 | 57 | ## Usage 58 | Just initilize `DraggableDetailsOverlayViewController` with your nested viewcontroller and delegate. Nested viewcontroller must adopt the `DraggableDetailsOverlayViewControllerDelegate` and `DraggableDetailsOverlayNestedInterface` protocols. The delegate allows to respond to scrolling events. 59 | Have a look at the [DraggableOverlayExample](https://github.com/shakurocom/DraggableOverlay/tree/main/DraggableOverlayExample) (perform `pod install` before usage) 60 | 61 | ## License 62 | 63 | Shakuro.DraggableOverlay is released under the MIT license. [See LICENSE](https://github.com/shakurocom/DraggableOverlay/blob/main/LICENSE.md) for details. 64 | 65 | ## Give it a try and reach us 66 | 67 | Explore our expertise in Native Mobile Development and iOS Development.

68 | 69 | If you need professional assistance with your mobile or web project, feel free to contact our team 70 | -------------------------------------------------------------------------------- /Resources/draggable_overlay_example_1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shakurocom/DraggableOverlay/557946292086eaa232b222c3d14fcc92b3be92cf/Resources/draggable_overlay_example_1.gif -------------------------------------------------------------------------------- /Resources/draggable_overlay_example_2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shakurocom/DraggableOverlay/557946292086eaa232b222c3d14fcc92b3be92cf/Resources/draggable_overlay_example_2.gif -------------------------------------------------------------------------------- /Resources/draggable_overlay_example_3.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shakurocom/DraggableOverlay/557946292086eaa232b222c3d14fcc92b3be92cf/Resources/draggable_overlay_example_3.gif -------------------------------------------------------------------------------- /Resources/draggable_overlay_example_4.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shakurocom/DraggableOverlay/557946292086eaa232b222c3d14fcc92b3be92cf/Resources/draggable_overlay_example_4.gif -------------------------------------------------------------------------------- /Resources/title_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shakurocom/DraggableOverlay/557946292086eaa232b222c3d14fcc92b3be92cf/Resources/title_image.png -------------------------------------------------------------------------------- /Shakuro.DraggableOverlay.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = 'Shakuro.DraggableOverlay' 3 | s.version = '1.0.4' 4 | s.summary = 'Shakuro Draggable Overlay' 5 | s.homepage = 'https://github.com/shakurocom/DraggableOverlay' 6 | s.license = { :type => "MIT", :file => "LICENSE.md" } 7 | s.authors = {'apopov1988' => 'apopov@shakuro.com', 'wwwpix' => 'spopov@shakuro.com', 'slaschuk' => 'slaschuk@shakuro.com'} 8 | s.source = { :git => 'https://github.com/shakurocom/DraggableOverlay.git', :tag => s.version } 9 | s.source_files = 'Source/*', 'Source/**/*' 10 | s.swift_version = ['5.1', '5.2', '5.3', '5.4', '5.5', '5.6'] 11 | s.ios.deployment_target = '11.0' 12 | 13 | s.dependency 'Shakuro.CommonTypes', '~> 1.1' 14 | 15 | end 16 | -------------------------------------------------------------------------------- /Source/DraggableDetailsOverlayHandleView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2022 Shakuro (https://shakuro.com/) 3 | // Sergey Laschuk 4 | // 5 | 6 | import UIKit 7 | 8 | internal class DraggableDetailsOverlayHandleView: UIView { 9 | 10 | internal private(set) var handleView: UIView! 11 | internal private(set) var handleWidthConstraint: NSLayoutConstraint! 12 | internal private(set) var handleHeightConstraint: NSLayoutConstraint! 13 | 14 | internal override init(frame: CGRect) { 15 | fatalError("use init(frame:, handleColor: , ... ) ") 16 | } 17 | 18 | internal required init?(coder aDecoder: NSCoder) { 19 | fatalError("use init(frame:, handleColor: , ... ) ") 20 | } 21 | 22 | internal init(frame: CGRect, handleColor: UIColor, handleSize: CGSize, handleCornerRadius: CGFloat) { 23 | super.init(frame: frame) 24 | backgroundColor = UIColor.clear 25 | clipsToBounds = true 26 | handleView = UIView(frame: CGRect(x: 0, y: 0, width: handleSize.width, height: handleSize.height)) 27 | handleView.backgroundColor = handleColor 28 | handleView.layer.masksToBounds = true 29 | handleView.layer.cornerRadius = handleCornerRadius 30 | handleView.translatesAutoresizingMaskIntoConstraints = false 31 | addSubview(handleView) 32 | handleWidthConstraint = handleView.widthAnchor.constraint(equalToConstant: handleSize.width) 33 | handleWidthConstraint.isActive = true 34 | handleHeightConstraint = handleView.heightAnchor.constraint(equalToConstant: handleSize.height) 35 | handleHeightConstraint.isActive = true 36 | handleView.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true 37 | handleView.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /Source/DraggableDetailsOverlayViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2022 Shakuro (https://shakuro.com/) 3 | // Sergey Laschuk 4 | // 5 | 6 | import Foundation 7 | import UIKit 8 | import Shakuro_CommonTypes 9 | 10 | /// Delegate of the draggable overlay. The one whole controls it. 11 | public protocol DraggableDetailsOverlayViewControllerDelegate: AnyObject { 12 | 13 | /// An array of anchors, that overlay will use for snapping. Anchors pointing to effectively the same point will be reduced to singular anchor. 14 | func draggableDetailsOverlayAnchors(_ overlay: DraggableDetailsOverlayViewController) -> [DraggableDetailsOverlayViewController.Anchor] 15 | 16 | /// Amount of background from the top, that overlay is not allowed to cover. 17 | /// Return 0 to be able to cover every available space. 18 | func draggableDetailsOverlayTopInset(_ overlay: DraggableDetailsOverlayViewController) -> CGFloat 19 | 20 | /// Maximum height of overlay. 21 | /// Return `nil` if height should not be limited. 22 | func draggableDetailsOverlayMaxHeight(_ overlay: DraggableDetailsOverlayViewController) -> CGFloat? 23 | 24 | /// This will also be reported, when user draggs overlay beyond allowed anchors (and overlay do not actually moves). 25 | func draggableDetailsOverlayDidDrag(_ overlay: DraggableDetailsOverlayViewController) 26 | 27 | /// Content's scroll will still be prevented for another runloop. 28 | func draggableDetailsOverlayDidEndDragging(_ overlay: DraggableDetailsOverlayViewController) 29 | 30 | /// Called on automatic and manual invoke of `show()` & `hide()`. 31 | func draggableDetailsOverlayDidChangeIsVisible(_ overlay: DraggableDetailsOverlayViewController) 32 | 33 | /// Called when layout (constraints for overlay position, etc... ) finished changing 34 | func draggableDetailsOverlayDidUpdatedLayout(_ overlay: DraggableDetailsOverlayViewController) 35 | 36 | /// Called just before overlay will hide because of dragging it to "off screen" position 37 | func draggableDetailsOverlayWillDragOffScreenToHide(_ overlay: DraggableDetailsOverlayViewController) 38 | 39 | /// Called just after overlay did hide because of tapping on shadow view 40 | func draggableDetailsOverlayDidHideByShadowTap(_ overlay: DraggableDetailsOverlayViewController) 41 | 42 | /// Will be called only if `isSnapToAnchorsEnabled` is set to `true`. 43 | /// Will be called after user ends drag gesture and before animation to nearest anchor. 44 | func draggableDetailsOverlay(_ overlay: DraggableDetailsOverlayViewController, 45 | willAnimateEndDragToNearestAnchor anchor: DraggableDetailsOverlayViewController.Anchor) 46 | 47 | } 48 | 49 | /// Interface for controller, that will be displayed inside draggable overlay. 50 | /// Content's layout notes: 51 | /// - height of container for content is dynamic and will change with drag. 52 | /// - minimum height is 0 53 | /// - priority of container's bottom constraint is 999 54 | public protocol DraggableDetailsOverlayNestedInterface { 55 | /// - parameter requirePreventOfScroll: `true` indicates that overlay is currently dragging. 56 | /// Nested controller should prevent any content scrolling. 57 | /// For better UX scrolling indicators should be disabled as well. 58 | /// methods to be aware of are: 59 | /// 1) func scrollViewDidScroll(_:) - keep offset at saved value 60 | /// 2) func scrollViewWillEndDragging(_:,withVelocity:,targetContentOffset:) - set targetContentOffset.pointee to saved offset 61 | func draggableDetailsOverlay(_ overlay: DraggableDetailsOverlayViewController, requirePreventOfScroll: Bool) 62 | func draggableDetailsOverlayContentScrollViews(_ overlay: DraggableDetailsOverlayViewController) -> [UIScrollView] 63 | } 64 | 65 | /// Overlay that can be dragged to cover more or less of available space. 66 | /// Can be configured to be "twitter-like". With limited content height. 67 | public class DraggableDetailsOverlayViewController: UIViewController { 68 | 69 | public typealias NestedController = UIViewController & DraggableDetailsOverlayNestedInterface 70 | 71 | /// Anchor points for resting positions of overlay. 72 | /// Anchors will be cached. 73 | /// Anchors pointing effectively to the same position on screen will be collapsed into single anchor. 74 | /// Tags are used for identification of anchors. Strictly by user. Please use positive numbers. 75 | /// Tags will be combined in case of collapsing of anchors. 76 | /// Default tag has tag of `-1`. 77 | public struct Anchor: Equatable { 78 | 79 | public static let defaultAnchor: Anchor = Anchor(topOffset: 0, tag: -1) 80 | 81 | public let tags: [Int] 82 | internal let isFromTop: Bool // used only as input value before caching 83 | internal let value: CGFloat 84 | 85 | /// Anchor point described as an offset for top of the `DraggableDetailsOverlayViewController.view`. 86 | public init(topOffset: CGFloat, tag: Int) { 87 | tags = [tag] 88 | isFromTop = true 89 | value = topOffset 90 | } 91 | 92 | /// Anchor point described as visible height of overlay. 93 | public init(height: CGFloat, tag: Int) { 94 | tags = [tag] 95 | isFromTop = false 96 | value = height 97 | } 98 | 99 | internal init(topOffset: CGFloat, tags: [Int]) { 100 | self.tags = tags 101 | isFromTop = true 102 | value = topOffset 103 | } 104 | 105 | } 106 | 107 | private enum Constant { 108 | static let hiddenContainerOffset: CGFloat = 10 109 | /// Anchors will be considered equal if they separated by no more than this amount of points. 110 | static let anchorsCachingGranularity: CGFloat = 1.0 111 | } 112 | 113 | /// Is on/off screen? 114 | /// Changes at the start of show() and at the end of hide(). 115 | public private(set) var isVisible: Bool = false { 116 | didSet { 117 | if oldValue != isVisible { 118 | delegate?.draggableDetailsOverlayDidChangeIsVisible(self) 119 | } 120 | } 121 | } 122 | 123 | /// Enable shadow background. 124 | /// Shadow will block interaction with everything underneath. 125 | /// Default value is `true`. 126 | public var isShadowEnabled: Bool = true { 127 | didSet { 128 | guard isViewLoaded else { return } 129 | shadowBackgroundView.isHidden = !isShadowEnabled 130 | } 131 | } 132 | 133 | /// Enable/disable overlay close on shadow tap. 134 | /// Default value is `false`. 135 | public var isTapOnShadowToCloseEnabled: Bool = false { 136 | didSet { 137 | guard isViewLoaded else { return } 138 | shadowTapGestureRecognizer.isEnabled = isTapOnShadowToCloseEnabled 139 | } 140 | } 141 | 142 | /// Enable shadow (blurred thingy) around averlay. 143 | /// Default value is `false`. 144 | public var isContainerShadowEnabled: Bool = false { 145 | didSet { 146 | guard isViewLoaded else { return } 147 | if isContainerShadowEnabled { 148 | addContainerShadow() 149 | } else { 150 | removeContainerShadow() 151 | } 152 | } 153 | } 154 | 155 | public var shadowBackgroundColor: UIColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0.5) { 156 | didSet { 157 | guard isViewLoaded else { return } 158 | shadowBackgroundView.backgroundColor = shadowBackgroundColor 159 | } 160 | } 161 | 162 | public var draggableContainerBackgroundColor: UIColor = UIColor.white { 163 | didSet { 164 | guard isViewLoaded else { return } 165 | draggableContainerView.backgroundColor = draggableContainerBackgroundColor 166 | } 167 | } 168 | 169 | public var draggableContainerTopCornersRadius: CGFloat = 5 { 170 | didSet { 171 | guard isViewLoaded else { return } 172 | draggableContainerView.layer.cornerRadius = draggableContainerTopCornersRadius 173 | draggableContainerBottomConstraint.constant = draggableContainerTopCornersRadius 174 | contentContainerBottomConstraint.constant = -draggableContainerTopCornersRadius 175 | } 176 | } 177 | 178 | /// Container for drag-handle. 179 | /// Handle is centered here. 180 | /// Use 0 to hide handle. 181 | /// Default value is `16`. 182 | public var handleContainerHeight: CGFloat = 16 { 183 | didSet { 184 | guard isViewLoaded else { return } 185 | handleHeightConstraint.constant = handleContainerHeight 186 | } 187 | } 188 | 189 | /// Color of drag-handle. 190 | /// Default value is `UIColor.lightGray`. 191 | public var handleColor: UIColor = UIColor.lightGray { 192 | didSet { 193 | guard isViewLoaded else { return } 194 | handleView.handleView.backgroundColor = handleColor 195 | } 196 | } 197 | 198 | /// Size of drag-handle element. 199 | /// Default value is `36 x 4`. 200 | public var handleSize: CGSize = CGSize(width: 36, height: 4) { 201 | didSet { 202 | guard isViewLoaded else { return } 203 | handleView.handleWidthConstraint.constant = handleSize.width 204 | handleView.handleHeightConstraint.constant = handleSize.height 205 | } 206 | } 207 | 208 | /// Corner radius value for drag-handle element. 209 | /// Independent of it's height. 210 | /// Default value is `2`. 211 | public var handleCornerRadius: CGFloat = 2 { 212 | didSet { 213 | guard isViewLoaded else { return } 214 | handleView.handleView.layer.cornerRadius = handleCornerRadius 215 | } 216 | } 217 | 218 | /// Animation duration for `show()` & `hide()` & `updateLayout(animated:)`. 219 | /// Default value is `0.25`. 220 | public var showHideAnimationDuration: TimeInterval = 0.25 221 | 222 | /// If enabled - overlay will be snap-animated to nearest anchor. 223 | /// Affects drag and show(). 224 | /// Default value `true`. 225 | public var isSnapToAnchorsEnabled: Bool = true 226 | 227 | /// If enabled, user can drag overlay below bottom 228 | /// Default value `false`. 229 | public var isDragOffScreenToHideEnabled: Bool = false 230 | 231 | /// If enabled - user can over-drag overlay beyond most periferal anchors. 232 | /// Over-drag is affected by `bounceDragDumpening`. 233 | /// Default value is `false`. 234 | public var isBounceEnabled: Bool = false 235 | 236 | /// How much harder it is to over-drag (comparing to normal drag). 237 | /// Default value is `0.5`. 238 | public var bounceDragDumpening: CGFloat = 0.5 239 | 240 | /// If `false` - snapping anchor will be calculated from current position of overlay. 241 | /// If `true` - current position + touch velocity will be used. 242 | /// Default value is `true`. 243 | public var snapCalculationUsesDeceleration: Bool = true 244 | 245 | /// If `false` - When user releases touch with some velocity, 246 | /// decelerating behaviour can't snap to anchors other then current or immediate next/previous one. 247 | /// Default value is `true`. 248 | public var snapCalculationDecelerationCanSkipNextAnchor: Bool = true 249 | 250 | /// Deceleartion rate used for calculation of snap anchors. 251 | /// Default value is `UIScrollView.DecelerationRate.normal` 252 | public var snapCalculationDecelerationRate: UIScrollView.DecelerationRate = .normal 253 | 254 | /// Duration of animation used, when user releases finger during drag. 255 | /// Default value is `0.2`. 256 | public var snapAnimationNormalDuration: TimeInterval = 0.2 257 | 258 | /// Use spring animation for snapping to anchors. 259 | /// Spring is not used in `show()`. 260 | /// Default value is `true`. 261 | public var snapAnimationUseSpring: Bool = true 262 | 263 | /// Same as `snapAnimationUseSpring`, but explicitly for top anchor. 264 | /// Default value is `false`. 265 | public var snapAnimationTopAnchorUseSpring: Bool = false 266 | 267 | /// Duration of animation used, when user releases finger during drag and container snaps to anchor. 268 | /// Default value is `0.4`. 269 | public var snapAnimationSpringDuration: TimeInterval = 0.4 270 | 271 | /// Parameter of spring animation (if enabled). 272 | /// Default value is `0.7`. 273 | public var snapAnimationSpringDamping: CGFloat = 0.7 274 | 275 | /// Parameter of spring animation (if enabled). 276 | /// Default value is `1.5`. 277 | public var snapAnimationSpringInitialVelocity: CGFloat = 1.5 278 | 279 | /// If enabled horizontal-ish drags will not activate drag of the overlay. 280 | /// Should be disabled if content 281 | /// Default value is `false`. 282 | public var allowHorizontalContentScrolling: Bool = false 283 | 284 | public var handleViewAccessibilityTitle: String? 285 | public var handleViewAccessibilitySubtitle: String? 286 | public var handleViewAccessibilityMaximizedTitle: String? 287 | public var handleViewAccessibilityMinimizedTitle: String? 288 | public var handleViewAccessibilityCollapseTitle: String? 289 | public var handleViewAccessibilityExpandTitle: String? 290 | public var handleViewAccessibilityHideTitle: String? 291 | 292 | private var shadowBackgroundView: UIView! 293 | private var draggableContainerView: UIView! 294 | private var draggableContainerHiddenTopConstraint: NSLayoutConstraint! 295 | private var draggableContainerShownTopConstraint: NSLayoutConstraint! 296 | private var draggableContainerBottomConstraint: NSLayoutConstraint! 297 | private var contentContainerView: UIView! 298 | private var contentContainerBottomConstraint: NSLayoutConstraint! 299 | private var handleView: DraggableDetailsOverlayHandleView! 300 | private var handleHeightConstraint: NSLayoutConstraint! 301 | private var dragGestureRecognizer: UIPanGestureRecognizer! 302 | private var shadowTapGestureRecognizer: UITapGestureRecognizer! 303 | 304 | private let nestedController: NestedController 305 | private weak var delegate: DraggableDetailsOverlayViewControllerDelegate? 306 | 307 | private var anchors: [Anchor] = [] 308 | /// Sorted top->bottom (lowest->highest). 309 | private var cachedAnchors: [Anchor] = [Anchor.defaultAnchor] 310 | private var screenBottomOffset: CGFloat = 0 311 | /// Height for which offsets/heights were cached/calculated. 312 | private var layoutCalculatedForHeight: CGFloat = 0 313 | 314 | /// Scroll view from nested content, where pan started. 315 | /// Downward drag is disabled if this scroll is not at the top of it's content. 316 | private var currentPanStartingContentScrollView: UIScrollView? 317 | 318 | // MARK: - Initialization 319 | 320 | required init?(coder aDecoder: NSCoder) { 321 | fatalError("init(coder) is not allowed. Use init(style:)") 322 | } 323 | 324 | public init(nestedController: NestedController, delegate: DraggableDetailsOverlayViewControllerDelegate) { 325 | self.nestedController = nestedController 326 | self.delegate = delegate 327 | super.init(nibName: nil, bundle: nil) 328 | } 329 | 330 | public override func loadView() { 331 | // some solid frame to operate with constraints 332 | let mainView = TouchTransparentView(frame: CGRect(x: 0, y: 0, width: 300, height: 300)) 333 | mainView.backgroundColor = UIColor.clear 334 | mainView.clipsToBounds = true 335 | view = mainView 336 | 337 | updateAnchors() 338 | 339 | setupShadowBackgroundView() 340 | setupDraggableContainer() 341 | setupHandle() 342 | if isContainerShadowEnabled { 343 | addContainerShadow() 344 | } 345 | setupContentContainer() 346 | setupPanRecognizer() 347 | setupTapRecognizer() 348 | } 349 | 350 | public override func viewDidLoad() { 351 | super.viewDidLoad() 352 | addChildViewController(nestedController, notifyAboutAppearanceTransition: false, targetContainerView: contentContainerView) 353 | 354 | setupAccessibility() 355 | } 356 | 357 | public override var childForStatusBarStyle: UIViewController? { 358 | return nestedController 359 | } 360 | 361 | // MARK: - Events 362 | 363 | public override func viewDidLayoutSubviews() { 364 | super.viewDidLayoutSubviews() 365 | updateLayout(animated: false, forced: false) 366 | } 367 | 368 | // MARK: - Public 369 | 370 | /// Affected by `isSnapToAnchorsEnabled`. 371 | public func show(initialAnchor: Anchor, animated: Bool, completion: (() -> Void)? = nil) { 372 | setVisible(true, animated: animated, initialAnchor: initialAnchor, completion: completion) 373 | setupAccessibility() 374 | } 375 | 376 | public func hide(animated: Bool, completion: (() -> Void)? = nil) { 377 | setVisible(false, animated: animated, initialAnchor: Anchor.defaultAnchor, completion: completion) 378 | } 379 | 380 | public func updateLayout(animated: Bool) { 381 | updateLayout(animated: animated, forced: true) 382 | } 383 | 384 | /// Current vertical space between allowed area's top and draggable container's top. 385 | /// - return's nil if view is not loaded or if overlay is hidden. 386 | public func currentTopOffset() -> CGFloat? { 387 | guard isViewLoaded, isVisible else { 388 | return nil 389 | } 390 | return draggableContainerShownTopConstraint.constant 391 | } 392 | 393 | } 394 | 395 | // MARK: - UIGestureRecognizerDelegate 396 | 397 | extension DraggableDetailsOverlayViewController: UIGestureRecognizerDelegate { 398 | 399 | public func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { 400 | guard !view.isHidden else { 401 | return false 402 | } 403 | if gestureRecognizer === dragGestureRecognizer, let view = gestureRecognizer.view, allowHorizontalContentScrolling { 404 | let translation = dragGestureRecognizer.translation(in: view) 405 | return abs(translation.x) <= abs(translation.y) 406 | } 407 | return true 408 | } 409 | 410 | public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, 411 | shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { 412 | if gestureRecognizer === dragGestureRecognizer || otherGestureRecognizer === dragGestureRecognizer { 413 | return true 414 | } 415 | return false 416 | } 417 | 418 | public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool { 419 | guard gestureRecognizer === dragGestureRecognizer, 420 | !view.isHidden, 421 | isShadowEnabled || draggableContainerView.frame.contains(touch.location(in: view)) 422 | else { 423 | return false 424 | } 425 | return true 426 | } 427 | 428 | } 429 | 430 | // MARK: - Interface Callbacks 431 | 432 | private extension DraggableDetailsOverlayViewController { 433 | 434 | @objc private func handleDragGesture(_ recognizer: UIGestureRecognizer) { 435 | guard recognizer === dragGestureRecognizer, !view.isHidden else { 436 | return 437 | } 438 | let translationY = dragGestureRecognizer.translation(in: dragGestureRecognizer.view).y 439 | let velocity = dragGestureRecognizer.velocity(in: dragGestureRecognizer.view) 440 | dragGestureRecognizer.setTranslation(CGPoint.zero, in: dragGestureRecognizer.view) 441 | switch recognizer.state { 442 | case .possible: 443 | break 444 | 445 | case .began: 446 | let contentScrollViews = nestedController.draggableDetailsOverlayContentScrollViews(self) 447 | currentPanStartingContentScrollView = contentScrollViews.first(where: { (scroll) -> Bool in 448 | let touchLocation = dragGestureRecognizer.location(in: scroll.superview) 449 | return scroll.frame.contains(touchLocation) 450 | }) 451 | setPreventContentScroll(true) 452 | 453 | case .changed: 454 | if isContentScrollAtTop(contentScrollView: currentPanStartingContentScrollView) || translationY < 0 { 455 | let newOffset = draggableContainerShownTopConstraint.constant + translationY 456 | let maxOffset = cachedAnchors.last?.value ?? 0 457 | let minOffset = cachedAnchors.first?.value ?? 0 458 | let newShadowAlpha: CGFloat 459 | if newOffset < minOffset { 460 | if isBounceEnabled { 461 | let dumpenedNewOffset = draggableContainerShownTopConstraint.constant + translationY * bounceDragDumpening 462 | draggableContainerShownTopConstraint.constant = dumpenedNewOffset 463 | newShadowAlpha = 1.0 464 | setPreventContentScroll(true) 465 | } else { 466 | draggableContainerShownTopConstraint.constant = minOffset 467 | newShadowAlpha = 1.0 468 | setPreventContentScroll(false) 469 | } 470 | } else if minOffset <= newOffset && newOffset <= maxOffset { 471 | draggableContainerShownTopConstraint.constant = newOffset 472 | newShadowAlpha = 1.0 473 | setPreventContentScroll(true) 474 | } else { // newOffset > maxOffset 475 | if isDragOffScreenToHideEnabled { 476 | draggableContainerShownTopConstraint.constant = newOffset 477 | newShadowAlpha = CGFloat.maximum((screenBottomOffset - newOffset) / (screenBottomOffset - maxOffset), 0.0) 478 | setPreventContentScroll(true) 479 | } else if isBounceEnabled { 480 | let dumpenedNewOffset = draggableContainerShownTopConstraint.constant + translationY * bounceDragDumpening 481 | draggableContainerShownTopConstraint.constant = dumpenedNewOffset 482 | newShadowAlpha = 1.0 483 | setPreventContentScroll(true) 484 | } else { 485 | draggableContainerShownTopConstraint.constant = maxOffset 486 | newShadowAlpha = 1.0 487 | setPreventContentScroll(false) 488 | } 489 | } 490 | if isShadowEnabled { 491 | shadowBackgroundView.alpha = newShadowAlpha 492 | } 493 | delegate?.draggableDetailsOverlayDidDrag(self) 494 | } else { 495 | setPreventContentScroll(false) 496 | } 497 | 498 | case .ended, 499 | .cancelled, 500 | .failed: 501 | let currentOffset = draggableContainerShownTopConstraint.constant 502 | if isSnapToAnchorsEnabled { 503 | let restAnchor: Anchor 504 | let shouldHide: Bool 505 | if snapCalculationUsesDeceleration { 506 | let deceleratedOffset = DecelerationHelper.project( 507 | value: currentOffset, 508 | initialVelocity: velocity.y / 1000.0, /* because this should be in milliseconds */ 509 | decelerationRate: snapCalculationDecelerationRate.rawValue) 510 | if snapCalculationDecelerationCanSkipNextAnchor { 511 | let temp = closestAnchor(targetOffset: deceleratedOffset) 512 | restAnchor = temp.anchor 513 | shouldHide = temp.shouldHide 514 | } else { 515 | let temp = closestAnchor(targetOffset: deceleratedOffset, currentOffset: currentOffset) 516 | restAnchor = temp.anchor 517 | shouldHide = temp.shouldHide 518 | } 519 | } else { 520 | let temp = closestAnchor(targetOffset: currentOffset) 521 | restAnchor = temp.anchor 522 | shouldHide = temp.shouldHide 523 | } 524 | let isContentScrollAtTop = self.isContentScrollAtTop(contentScrollView: currentPanStartingContentScrollView) 525 | if isDragOffScreenToHideEnabled && shouldHide && isContentScrollAtTop { 526 | delegate?.draggableDetailsOverlayWillDragOffScreenToHide(self) 527 | hide(animated: currentOffset < screenBottomOffset) 528 | } else { 529 | delegate?.draggableDetailsOverlay(self, willAnimateEndDragToNearestAnchor: restAnchor) 530 | if currentOffset != restAnchor.value && (velocity.y <= 0 || (velocity.y > 0 && isContentScrollAtTop)) { 531 | let isSpring = restAnchor == cachedAnchors.first ? snapAnimationTopAnchorUseSpring : snapAnimationUseSpring 532 | animateToOffset(restAnchor.value, isSpring: isSpring, completion: nil) 533 | } 534 | } 535 | } else if isDragOffScreenToHideEnabled && currentOffset >= screenBottomOffset { 536 | delegate?.draggableDetailsOverlayWillDragOffScreenToHide(self) 537 | hide(animated: false) 538 | } 539 | currentPanStartingContentScrollView = nil 540 | DispatchQueue.main.async(execute: { // to prevent deceleration behaviour in content's scroll 541 | self.setPreventContentScroll(false) 542 | }) 543 | delegate?.draggableDetailsOverlayDidEndDragging(self) 544 | 545 | @unknown default: 546 | break 547 | } 548 | } 549 | 550 | @objc private func handleShadowTapGesture() { 551 | hide(animated: true, completion: { [weak self] in 552 | guard let strongSelf = self else { return } 553 | strongSelf.delegate?.draggableDetailsOverlayDidHideByShadowTap(strongSelf) 554 | }) 555 | } 556 | 557 | @objc private func collapseButtonPressed() -> Bool { 558 | return performAccessibilityAction(isCollapse: true) 559 | } 560 | 561 | @objc private func expandButtonPressed() -> Bool { 562 | return performAccessibilityAction(isCollapse: false) 563 | } 564 | 565 | @objc private func hideButtonPressed() -> Bool { 566 | hide(animated: true) 567 | return true 568 | } 569 | 570 | } 571 | 572 | // MARK: - Private 573 | 574 | private extension DraggableDetailsOverlayViewController { 575 | 576 | private func setupShadowBackgroundView() { 577 | shadowBackgroundView = UIView(frame: view.bounds) 578 | shadowBackgroundView.backgroundColor = shadowBackgroundColor 579 | shadowBackgroundView.translatesAutoresizingMaskIntoConstraints = false 580 | view.addSubview(shadowBackgroundView) 581 | shadowBackgroundView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true 582 | shadowBackgroundView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true 583 | shadowBackgroundView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true 584 | shadowBackgroundView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true 585 | shadowBackgroundView.isHidden = !isShadowEnabled 586 | shadowBackgroundView.alpha = 0.0 587 | } 588 | 589 | private func setupDraggableContainer() { 590 | draggableContainerView = UIView(frame: view.bounds) 591 | draggableContainerView.backgroundColor = draggableContainerBackgroundColor 592 | draggableContainerView.layer.masksToBounds = true 593 | draggableContainerView.layer.cornerRadius = draggableContainerTopCornersRadius 594 | draggableContainerView.translatesAutoresizingMaskIntoConstraints = false 595 | view.addSubview(draggableContainerView) 596 | if #available(iOS 11.0, *) { 597 | draggableContainerShownTopConstraint = draggableContainerView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor) 598 | draggableContainerHiddenTopConstraint = draggableContainerView.topAnchor.constraint(equalTo: view.bottomAnchor, 599 | constant: Constant.hiddenContainerOffset) 600 | } else { 601 | draggableContainerShownTopConstraint = draggableContainerView.topAnchor.constraint(equalTo: topLayoutGuide.bottomAnchor) 602 | draggableContainerHiddenTopConstraint = draggableContainerView.topAnchor.constraint(equalTo: view.bottomAnchor, 603 | constant: Constant.hiddenContainerOffset) 604 | } 605 | draggableContainerShownTopConstraint.isActive = false 606 | draggableContainerHiddenTopConstraint.isActive = true 607 | draggableContainerView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 0).isActive = true 608 | draggableContainerView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: 0).isActive = true 609 | draggableContainerBottomConstraint = draggableContainerView.bottomAnchor.constraint(equalTo: view.bottomAnchor, 610 | constant: draggableContainerTopCornersRadius) 611 | draggableContainerBottomConstraint.priority = UILayoutPriority(rawValue: 999) 612 | draggableContainerBottomConstraint.isActive = true 613 | } 614 | 615 | private func setupHandle() { 616 | handleView = DraggableDetailsOverlayHandleView( 617 | frame: CGRect(x: 0, y: 0, width: draggableContainerView.bounds.width, height: handleContainerHeight), 618 | handleColor: handleColor, 619 | handleSize: handleSize, 620 | handleCornerRadius: handleCornerRadius) 621 | handleView.translatesAutoresizingMaskIntoConstraints = false 622 | draggableContainerView.addSubview(handleView) 623 | handleView.leadingAnchor.constraint(equalTo: draggableContainerView.leadingAnchor).isActive = true 624 | handleView.trailingAnchor.constraint(equalTo: draggableContainerView.trailingAnchor).isActive = true 625 | handleView.topAnchor.constraint(equalTo: draggableContainerView.topAnchor).isActive = true 626 | handleHeightConstraint = handleView.heightAnchor.constraint(equalToConstant: handleContainerHeight) 627 | handleHeightConstraint.isActive = true 628 | } 629 | 630 | private func addContainerShadow() { 631 | let layer = draggableContainerView.layer 632 | layer.masksToBounds = false 633 | layer.shadowColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0.2).cgColor 634 | layer.shadowOpacity = 1.0 635 | layer.shadowOffset = CGSize() 636 | layer.shadowRadius = 6 637 | } 638 | 639 | private func removeContainerShadow() { 640 | let layer = draggableContainerView.layer 641 | layer.shadowColor = UIColor.clear.cgColor 642 | } 643 | 644 | private func setupContentContainer() { 645 | contentContainerView = UIView(frame: CGRect(x: 0, 646 | y: 0, 647 | width: draggableContainerView.bounds.width, 648 | height: draggableContainerView.bounds.height - handleContainerHeight)) 649 | contentContainerView.backgroundColor = UIColor.clear 650 | contentContainerView.translatesAutoresizingMaskIntoConstraints = false 651 | draggableContainerView.addSubview(contentContainerView) 652 | contentContainerView.heightAnchor.constraint(greaterThanOrEqualToConstant: 0).isActive = true 653 | contentContainerView.leadingAnchor.constraint(equalTo: draggableContainerView.leadingAnchor).isActive = true 654 | contentContainerView.trailingAnchor.constraint(equalTo: draggableContainerView.trailingAnchor).isActive = true 655 | contentContainerView.topAnchor.constraint(equalTo: handleView.bottomAnchor).isActive = true 656 | contentContainerBottomConstraint = contentContainerView.bottomAnchor.constraint(equalTo: draggableContainerView.bottomAnchor, 657 | constant: -draggableContainerTopCornersRadius) 658 | contentContainerBottomConstraint.isActive = true 659 | } 660 | 661 | private func setupPanRecognizer() { 662 | dragGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(handleDragGesture)) 663 | dragGestureRecognizer.delegate = self 664 | dragGestureRecognizer.isEnabled = true 665 | view.addGestureRecognizer(dragGestureRecognizer) 666 | } 667 | 668 | private func setupTapRecognizer() { 669 | shadowTapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleShadowTapGesture)) 670 | shadowTapGestureRecognizer.isEnabled = isTapOnShadowToCloseEnabled 671 | shadowBackgroundView.addGestureRecognizer(shadowTapGestureRecognizer) 672 | } 673 | 674 | private func updateAnchors() { 675 | struct TempAnchor { 676 | internal let offset: CGFloat 677 | internal var tags: [Int] 678 | } 679 | anchors = delegate?.draggableDetailsOverlayAnchors(self) ?? [Anchor.defaultAnchor] 680 | let topInset = calculateTopInset() 681 | var newAnchors: [TempAnchor] = [] 682 | screenBottomOffset = view.bounds.height 683 | for anchor in anchors { 684 | let offset = offsetForAnchor(anchor, topInset: topInset) 685 | if !isOffsetsEqual(screenBottomOffset, offset) { 686 | // ignore very small steps 687 | if let index = newAnchors.firstIndex(where: { isOffsetsEqual($0.offset, offset) }) { 688 | newAnchors[index].tags.append(contentsOf: anchor.tags) 689 | } else { 690 | newAnchors.append(TempAnchor(offset: offset, tags: anchor.tags)) 691 | } 692 | } 693 | } 694 | cachedAnchors = newAnchors.sorted(by: { $0.offset < $1.offset }).map({ Anchor(topOffset: $0.offset, tags: $0.tags) }) 695 | 696 | setupAccessibility() 697 | } 698 | 699 | private func offsetForAnchor(_ anchor: Anchor, topInset: CGFloat) -> CGFloat { 700 | if anchor.isFromTop { 701 | return min(view.bounds.height, max(anchor.value, topInset)) 702 | } else { 703 | let inset = view.bounds.height - anchor.value - topSafeAreaInset() - bottomSafeAreaInset() 704 | return min(view.bounds.height, max(topInset, inset)) 705 | } 706 | } 707 | 708 | /// Calculates closest anchor regardless of current position. 709 | private func closestAnchor(targetOffset: CGFloat) -> (anchor: Anchor, shouldHide: Bool) { 710 | let closestAnchorTemp = cachedAnchors.min(by: { return abs($0.value - targetOffset) < abs($1.value - targetOffset) }) 711 | let closestAnchor = closestAnchorTemp ?? Anchor.defaultAnchor 712 | let shouldHide = abs(closestAnchor.value - targetOffset) > abs(screenBottomOffset - targetOffset) 713 | return (closestAnchor, shouldHide) 714 | } 715 | 716 | /// Calculates closes anchor from current position (current or next or previous). 717 | private func closestAnchor(targetOffset: CGFloat, currentOffset: CGFloat) -> (anchor: Anchor, shouldHide: Bool) { 718 | let currentAnchor = cachedAnchors.first(where: { isOffsetsEqual($0.value, currentOffset) }) 719 | let nextAnchor: Anchor? 720 | let previousAnchor: Anchor? 721 | if let realCurrentAnchor = currentAnchor { 722 | nextAnchor = cachedAnchors.first(where: { $0.value > realCurrentAnchor.value }) 723 | previousAnchor = cachedAnchors.last(where: { $0.value < realCurrentAnchor.value }) 724 | } else { 725 | nextAnchor = cachedAnchors.first(where: { $0.value > currentOffset }) 726 | previousAnchor = cachedAnchors.last(where: { $0.value < currentOffset }) 727 | } 728 | let anchors = [previousAnchor, currentAnchor, nextAnchor].compactMap({ $0 }) 729 | let closestAnchor = anchors.min(by: { return abs($0.value - targetOffset) < abs($1.value - targetOffset) }) ?? Anchor.defaultAnchor 730 | let shouldHide = abs(closestAnchor.value - targetOffset) > abs(screenBottomOffset - targetOffset) 731 | return (closestAnchor, shouldHide) 732 | } 733 | 734 | private func topSafeAreaInset() -> CGFloat { 735 | if #available(iOS 11.0, *) { 736 | return view.safeAreaInsets.top 737 | } else { 738 | return topLayoutGuide.length 739 | } 740 | } 741 | 742 | private func bottomSafeAreaInset() -> CGFloat { 743 | if #available(iOS 11.0, *) { 744 | return view.safeAreaInsets.bottom 745 | } else { 746 | return bottomLayoutGuide.length 747 | } 748 | } 749 | 750 | private func calculateTopInset() -> CGFloat { 751 | let topInsetFromMaxHeight: CGFloat 752 | if let maxHeight = delegate?.draggableDetailsOverlayMaxHeight(self) { 753 | topInsetFromMaxHeight = max(0, view.bounds.height - maxHeight) 754 | } else { 755 | topInsetFromMaxHeight = 0 756 | } 757 | return max(topInsetFromMaxHeight, delegate?.draggableDetailsOverlayTopInset(self) ?? 0) 758 | } 759 | 760 | private func isOffsetsEqual(_ left: CGFloat, _ right: CGFloat) -> Bool { 761 | return abs(left - right) < Constant.anchorsCachingGranularity 762 | } 763 | 764 | private func updateLayout(animated: Bool, forced: Bool) { 765 | guard view.bounds.height != layoutCalculatedForHeight || forced else { 766 | return 767 | } 768 | updateAnchors() 769 | layoutCalculatedForHeight = view.bounds.height 770 | guard isVisible else { 771 | return 772 | } 773 | let newCurrentOffset = closestAnchor(targetOffset: draggableContainerShownTopConstraint.constant).anchor.value 774 | guard newCurrentOffset != draggableContainerShownTopConstraint.constant else { 775 | delegate?.draggableDetailsOverlayDidUpdatedLayout(self) 776 | return 777 | } 778 | if animated { 779 | animateToOffset(newCurrentOffset, isSpring: false, completion: { 780 | self.delegate?.draggableDetailsOverlayDidUpdatedLayout(self) 781 | }) 782 | } else { 783 | draggableContainerShownTopConstraint.constant = newCurrentOffset 784 | view.layoutIfNeeded() 785 | delegate?.draggableDetailsOverlayDidUpdatedLayout(self) 786 | } 787 | } 788 | 789 | private func setVisible(_ newVisible: Bool, animated: Bool, initialAnchor: Anchor, completion: (() -> Void)? = nil) { 790 | let initialOffset: CGFloat 791 | if newVisible { 792 | isVisible = true 793 | updateLayout(animated: false, forced: true) 794 | view.isHidden = false 795 | let topInset = calculateTopInset() 796 | let wantedOffset = offsetForAnchor(initialAnchor, topInset: topInset) 797 | initialOffset = isSnapToAnchorsEnabled ? closestAnchor(targetOffset: wantedOffset).anchor.value : wantedOffset 798 | } else { 799 | initialOffset = 0 800 | } 801 | let animations = { () -> Void in 802 | if newVisible { 803 | self.shadowBackgroundView.alpha = 1.0 804 | self.draggableContainerHiddenTopConstraint.isActive = false 805 | self.draggableContainerShownTopConstraint.constant = initialOffset 806 | self.draggableContainerShownTopConstraint.isActive = true 807 | } else { 808 | self.shadowBackgroundView.alpha = 0.0 809 | self.draggableContainerShownTopConstraint.isActive = false 810 | self.draggableContainerHiddenTopConstraint.isActive = true 811 | } 812 | } 813 | let animationCompletion = { (_: Bool) -> Void in 814 | if !newVisible { 815 | self.view.isHidden = true 816 | self.isVisible = false 817 | } 818 | completion?() 819 | if self.isShadowEnabled { 820 | UIAccessibility.post(notification: .layoutChanged, argument: self.view) 821 | } 822 | } 823 | if animated { 824 | UIView.animate( 825 | withDuration: showHideAnimationDuration, 826 | delay: 0.0, 827 | options: [.beginFromCurrentState, .curveEaseOut], 828 | animations: { 829 | animations() 830 | self.view.layoutIfNeeded() 831 | }, 832 | completion: animationCompletion) 833 | } else { 834 | animations() 835 | animationCompletion(true) 836 | } 837 | } 838 | 839 | private func animateToOffset(_ targetOffset: CGFloat, isSpring: Bool, completion: (() -> Void)?) { 840 | let animations = { () -> Void in 841 | if self.isShadowEnabled { 842 | self.shadowBackgroundView.alpha = 1.0 843 | } 844 | self.draggableContainerShownTopConstraint.constant = targetOffset 845 | self.view.layoutIfNeeded() 846 | } 847 | if isSpring { 848 | UIView.animate(withDuration: snapAnimationSpringDuration, 849 | delay: 0.0, 850 | usingSpringWithDamping: snapAnimationSpringDamping, 851 | initialSpringVelocity: snapAnimationSpringInitialVelocity, 852 | options: [.beginFromCurrentState, .curveEaseOut], 853 | animations: animations, 854 | completion: { (_) -> Void in completion?() }) 855 | } else { 856 | UIView.animate(withDuration: snapAnimationNormalDuration, 857 | delay: 0.0, 858 | options: [.beginFromCurrentState, .curveEaseOut], 859 | animations: animations, 860 | completion: { (_) -> Void in completion?() }) 861 | } 862 | } 863 | 864 | private func setPreventContentScroll(_ newValue: Bool) { 865 | nestedController.draggableDetailsOverlay(self, requirePreventOfScroll: newValue) 866 | } 867 | 868 | private func isContentScrollAtTop(contentScrollView: UIScrollView?) -> Bool { 869 | guard let scroll = contentScrollView else { 870 | return true 871 | } 872 | return scroll.contentOffset.y <= -scroll.contentInset.top 873 | } 874 | 875 | private func performAccessibilityAction(isCollapse: Bool) -> Bool { 876 | let currentOffset = draggableContainerShownTopConstraint.constant 877 | let nextAnchorIndex = cachedAnchors.firstIndex(where: { isOffsetsEqual($0.value, currentOffset) }).map({ $0 + (isCollapse ? 1 : -1) }) 878 | guard let nextAnchorIndexActual = nextAnchorIndex, nextAnchorIndexActual >= 0 && nextAnchorIndexActual < cachedAnchors.count else { 879 | return false 880 | } 881 | let topInset = calculateTopInset() 882 | let newCurrentOffset = offsetForAnchor(cachedAnchors[nextAnchorIndexActual], topInset: topInset) 883 | animateToOffset(newCurrentOffset, isSpring: false, completion: { 884 | self.delegate?.draggableDetailsOverlayDidUpdatedLayout(self) 885 | }) 886 | setupAccessibility() 887 | return true 888 | } 889 | 890 | private func setupAccessibility() { 891 | guard isViewLoaded, let handleViewActual = handleView else { 892 | return 893 | } 894 | view.accessibilityViewIsModal = isShadowEnabled 895 | handleViewActual.isAccessibilityElement = true 896 | let currentOffset = draggableContainerShownTopConstraint.constant 897 | let currentAnchorIndex = cachedAnchors.firstIndex(where: { isOffsetsEqual($0.value, currentOffset) }) 898 | var label = handleViewAccessibilityTitle ?? "TODO: Overlay controller" 899 | if cachedAnchors.count > 1 { 900 | if currentAnchorIndex == 0 { 901 | label.append(", ") 902 | label.append(handleViewAccessibilityMaximizedTitle ?? "Maximized") 903 | } else if currentAnchorIndex == cachedAnchors.count - 1 { 904 | label.append(", ") 905 | label.append("Minimized") 906 | } 907 | } 908 | label.append(", ") 909 | label.append(handleViewAccessibilitySubtitle ?? "Adjust the size of the overlay") 910 | handleViewActual.accessibilityLabel = label 911 | var actions: [UIAccessibilityCustomAction] = [] 912 | if cachedAnchors.count > 1 { 913 | let collapseTitle = handleViewAccessibilityCollapseTitle ?? "Collapse" 914 | let expandTitle = handleViewAccessibilityExpandTitle ?? "Expand" 915 | actions.append(UIAccessibilityCustomAction(name: collapseTitle, target: self, selector: #selector(collapseButtonPressed))) 916 | actions.append(UIAccessibilityCustomAction(name: expandTitle, target: self, selector: #selector(expandButtonPressed))) 917 | } 918 | if isDragOffScreenToHideEnabled { 919 | let hideTitle = handleViewAccessibilityHideTitle ?? "Hide" 920 | actions.append(UIAccessibilityCustomAction(name: hideTitle, target: self, selector: #selector(hideButtonPressed))) 921 | } 922 | handleViewActual.accessibilityCustomActions = actions 923 | } 924 | 925 | } 926 | -------------------------------------------------------------------------------- /Source/TouchTransparentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2022 Shakuro (https://shakuro.com/) 3 | // Sergey Laschuk 4 | // 5 | 6 | import UIKit 7 | 8 | /** 9 | View, that is transparent for touches except areas, used by its subviews. 10 | */ 11 | internal class TouchTransparentView: UIView { 12 | 13 | override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { 14 | for subview in subviews { 15 | let subviewPoint = subview.convert(point, from: self) 16 | if !subview.isHidden && subview.isUserInteractionEnabled && subview.point(inside: subviewPoint, with: event) { 17 | return true 18 | } 19 | } 20 | return false 21 | } 22 | 23 | } 24 | --------------------------------------------------------------------------------