├── .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 | 
2 |
3 | # DraggableOverlay
4 | 
5 | 
6 | 
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 | 
18 |
19 | `DraggableOverlay` example with enabled shadow (red color) and container shadow (green color), customized draggable container height (30 px):
20 |
21 | 
22 |
23 | `DraggableOverlay` example with custom handle corner and custom handle container corner radius, customized handle color (yellow) and changed top inset:
24 |
25 | 
26 |
27 | `DraggableOverlay` example with bounce animation:
28 |
29 | 
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 |
--------------------------------------------------------------------------------