├── .gitignore
├── .swiftpm
└── xcode
│ └── package.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ └── IDEWorkspaceChecks.plist
├── Docs
├── preview1.gif
├── preview2.gif
└── preview3.gif
├── Examples
├── Examples.xcodeproj
│ ├── project.pbxproj
│ └── project.xcworkspace
│ │ ├── contents.xcworkspacedata
│ │ └── xcshareddata
│ │ └── IDEWorkspaceChecks.plist
└── Examples
│ ├── AppDelegate.swift
│ ├── Assets.xcassets
│ ├── AccentColor.colorset
│ │ └── Contents.json
│ ├── AppIcon.appiconset
│ │ └── Contents.json
│ └── Contents.json
│ ├── Base.lproj
│ └── LaunchScreen.storyboard
│ ├── CapsuleShape.swift
│ ├── ContentView.swift
│ ├── Default.swift
│ ├── Info.plist
│ ├── Preview Content
│ └── Preview Assets.xcassets
│ │ └── Contents.json
│ ├── RoundedRect.swift
│ └── SceneDelegate.swift
├── LICENSE
├── Package.swift
├── README.md
├── Sources
└── Unlocker
│ └── Unlocker.swift
└── Tests
├── LinuxMain.swift
└── UnlockerTests
├── UnlockerTests.swift
└── XCTestManifests.swift
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | /.build
3 | /Packages
4 | /*.xcodeproj
5 | xcuserdata/
6 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Docs/preview1.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aaronlab/Unlocker/b6d1e5e679bc055b40b829ce2b42e16dd039ec87/Docs/preview1.gif
--------------------------------------------------------------------------------
/Docs/preview2.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aaronlab/Unlocker/b6d1e5e679bc055b40b829ce2b42e16dd039ec87/Docs/preview2.gif
--------------------------------------------------------------------------------
/Docs/preview3.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aaronlab/Unlocker/b6d1e5e679bc055b40b829ce2b42e16dd039ec87/Docs/preview3.gif
--------------------------------------------------------------------------------
/Examples/Examples.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 52;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | A45635C625D169F80056FC11 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A45635C525D169F80056FC11 /* AppDelegate.swift */; };
11 | A45635C825D169F80056FC11 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A45635C725D169F80056FC11 /* SceneDelegate.swift */; };
12 | A45635CA25D169F80056FC11 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A45635C925D169F80056FC11 /* ContentView.swift */; };
13 | A45635CC25D169FB0056FC11 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A45635CB25D169FB0056FC11 /* Assets.xcassets */; };
14 | A45635CF25D169FB0056FC11 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A45635CE25D169FB0056FC11 /* Preview Assets.xcassets */; };
15 | A45635D225D169FB0056FC11 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = A45635D025D169FB0056FC11 /* LaunchScreen.storyboard */; };
16 | A45635DE25D16A280056FC11 /* CapsuleShape.swift in Sources */ = {isa = PBXBuildFile; fileRef = A45635DB25D16A270056FC11 /* CapsuleShape.swift */; };
17 | A45635DF25D16A280056FC11 /* Default.swift in Sources */ = {isa = PBXBuildFile; fileRef = A45635DC25D16A270056FC11 /* Default.swift */; };
18 | A45635E025D16A280056FC11 /* RoundedRect.swift in Sources */ = {isa = PBXBuildFile; fileRef = A45635DD25D16A280056FC11 /* RoundedRect.swift */; };
19 | A45635E825D16A940056FC11 /* Unlocker in Frameworks */ = {isa = PBXBuildFile; productRef = A45635E725D16A940056FC11 /* Unlocker */; };
20 | /* End PBXBuildFile section */
21 |
22 | /* Begin PBXFileReference section */
23 | A45635C225D169F80056FC11 /* Examples.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Examples.app; sourceTree = BUILT_PRODUCTS_DIR; };
24 | A45635C525D169F80056FC11 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
25 | A45635C725D169F80056FC11 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; };
26 | A45635C925D169F80056FC11 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; };
27 | A45635CB25D169FB0056FC11 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
28 | A45635CE25D169FB0056FC11 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; };
29 | A45635D125D169FB0056FC11 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; };
30 | A45635D325D169FB0056FC11 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
31 | A45635DB25D16A270056FC11 /* CapsuleShape.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CapsuleShape.swift; sourceTree = ""; };
32 | A45635DC25D16A270056FC11 /* Default.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Default.swift; sourceTree = ""; };
33 | A45635DD25D16A280056FC11 /* RoundedRect.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RoundedRect.swift; sourceTree = ""; };
34 | A45635EC25D16AF20056FC11 /* Unlocker */ = {isa = PBXFileReference; lastKnownFileType = folder; name = Unlocker; path = ../..; sourceTree = ""; };
35 | /* End PBXFileReference section */
36 |
37 | /* Begin PBXFrameworksBuildPhase section */
38 | A45635BF25D169F80056FC11 /* Frameworks */ = {
39 | isa = PBXFrameworksBuildPhase;
40 | buildActionMask = 2147483647;
41 | files = (
42 | A45635E825D16A940056FC11 /* Unlocker in Frameworks */,
43 | );
44 | runOnlyForDeploymentPostprocessing = 0;
45 | };
46 | /* End PBXFrameworksBuildPhase section */
47 |
48 | /* Begin PBXGroup section */
49 | A45635B925D169F80056FC11 = {
50 | isa = PBXGroup;
51 | children = (
52 | A45635C425D169F80056FC11 /* Examples */,
53 | A45635C325D169F80056FC11 /* Products */,
54 | A45635E625D16A940056FC11 /* Frameworks */,
55 | );
56 | sourceTree = "";
57 | };
58 | A45635C325D169F80056FC11 /* Products */ = {
59 | isa = PBXGroup;
60 | children = (
61 | A45635C225D169F80056FC11 /* Examples.app */,
62 | );
63 | name = Products;
64 | sourceTree = "";
65 | };
66 | A45635C425D169F80056FC11 /* Examples */ = {
67 | isa = PBXGroup;
68 | children = (
69 | A45635C525D169F80056FC11 /* AppDelegate.swift */,
70 | A45635C725D169F80056FC11 /* SceneDelegate.swift */,
71 | A45635C925D169F80056FC11 /* ContentView.swift */,
72 | A45635DB25D16A270056FC11 /* CapsuleShape.swift */,
73 | A45635DC25D16A270056FC11 /* Default.swift */,
74 | A45635DD25D16A280056FC11 /* RoundedRect.swift */,
75 | A45635CB25D169FB0056FC11 /* Assets.xcassets */,
76 | A45635D025D169FB0056FC11 /* LaunchScreen.storyboard */,
77 | A45635D325D169FB0056FC11 /* Info.plist */,
78 | A45635EC25D16AF20056FC11 /* Unlocker */,
79 | A45635CD25D169FB0056FC11 /* Preview Content */,
80 | );
81 | path = Examples;
82 | sourceTree = "";
83 | };
84 | A45635CD25D169FB0056FC11 /* Preview Content */ = {
85 | isa = PBXGroup;
86 | children = (
87 | A45635CE25D169FB0056FC11 /* Preview Assets.xcassets */,
88 | );
89 | path = "Preview Content";
90 | sourceTree = "";
91 | };
92 | A45635E625D16A940056FC11 /* Frameworks */ = {
93 | isa = PBXGroup;
94 | children = (
95 | );
96 | name = Frameworks;
97 | sourceTree = "";
98 | };
99 | /* End PBXGroup section */
100 |
101 | /* Begin PBXNativeTarget section */
102 | A45635C125D169F80056FC11 /* Examples */ = {
103 | isa = PBXNativeTarget;
104 | buildConfigurationList = A45635D625D169FB0056FC11 /* Build configuration list for PBXNativeTarget "Examples" */;
105 | buildPhases = (
106 | A45635BE25D169F80056FC11 /* Sources */,
107 | A45635BF25D169F80056FC11 /* Frameworks */,
108 | A45635C025D169F80056FC11 /* Resources */,
109 | );
110 | buildRules = (
111 | );
112 | dependencies = (
113 | );
114 | name = Examples;
115 | packageProductDependencies = (
116 | A45635E725D16A940056FC11 /* Unlocker */,
117 | );
118 | productName = Examples;
119 | productReference = A45635C225D169F80056FC11 /* Examples.app */;
120 | productType = "com.apple.product-type.application";
121 | };
122 | /* End PBXNativeTarget section */
123 |
124 | /* Begin PBXProject section */
125 | A45635BA25D169F80056FC11 /* Project object */ = {
126 | isa = PBXProject;
127 | attributes = {
128 | LastSwiftUpdateCheck = 1230;
129 | LastUpgradeCheck = 1230;
130 | TargetAttributes = {
131 | A45635C125D169F80056FC11 = {
132 | CreatedOnToolsVersion = 12.3;
133 | };
134 | };
135 | };
136 | buildConfigurationList = A45635BD25D169F80056FC11 /* Build configuration list for PBXProject "Examples" */;
137 | compatibilityVersion = "Xcode 9.3";
138 | developmentRegion = en;
139 | hasScannedForEncodings = 0;
140 | knownRegions = (
141 | en,
142 | Base,
143 | );
144 | mainGroup = A45635B925D169F80056FC11;
145 | productRefGroup = A45635C325D169F80056FC11 /* Products */;
146 | projectDirPath = "";
147 | projectRoot = "";
148 | targets = (
149 | A45635C125D169F80056FC11 /* Examples */,
150 | );
151 | };
152 | /* End PBXProject section */
153 |
154 | /* Begin PBXResourcesBuildPhase section */
155 | A45635C025D169F80056FC11 /* Resources */ = {
156 | isa = PBXResourcesBuildPhase;
157 | buildActionMask = 2147483647;
158 | files = (
159 | A45635D225D169FB0056FC11 /* LaunchScreen.storyboard in Resources */,
160 | A45635CF25D169FB0056FC11 /* Preview Assets.xcassets in Resources */,
161 | A45635CC25D169FB0056FC11 /* Assets.xcassets in Resources */,
162 | );
163 | runOnlyForDeploymentPostprocessing = 0;
164 | };
165 | /* End PBXResourcesBuildPhase section */
166 |
167 | /* Begin PBXSourcesBuildPhase section */
168 | A45635BE25D169F80056FC11 /* Sources */ = {
169 | isa = PBXSourcesBuildPhase;
170 | buildActionMask = 2147483647;
171 | files = (
172 | A45635DF25D16A280056FC11 /* Default.swift in Sources */,
173 | A45635C625D169F80056FC11 /* AppDelegate.swift in Sources */,
174 | A45635C825D169F80056FC11 /* SceneDelegate.swift in Sources */,
175 | A45635CA25D169F80056FC11 /* ContentView.swift in Sources */,
176 | A45635DE25D16A280056FC11 /* CapsuleShape.swift in Sources */,
177 | A45635E025D16A280056FC11 /* RoundedRect.swift in Sources */,
178 | );
179 | runOnlyForDeploymentPostprocessing = 0;
180 | };
181 | /* End PBXSourcesBuildPhase section */
182 |
183 | /* Begin PBXVariantGroup section */
184 | A45635D025D169FB0056FC11 /* LaunchScreen.storyboard */ = {
185 | isa = PBXVariantGroup;
186 | children = (
187 | A45635D125D169FB0056FC11 /* Base */,
188 | );
189 | name = LaunchScreen.storyboard;
190 | sourceTree = "";
191 | };
192 | /* End PBXVariantGroup section */
193 |
194 | /* Begin XCBuildConfiguration section */
195 | A45635D425D169FB0056FC11 /* Debug */ = {
196 | isa = XCBuildConfiguration;
197 | buildSettings = {
198 | ALWAYS_SEARCH_USER_PATHS = NO;
199 | CLANG_ANALYZER_NONNULL = YES;
200 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
201 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
202 | CLANG_CXX_LIBRARY = "libc++";
203 | CLANG_ENABLE_MODULES = YES;
204 | CLANG_ENABLE_OBJC_ARC = YES;
205 | CLANG_ENABLE_OBJC_WEAK = YES;
206 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
207 | CLANG_WARN_BOOL_CONVERSION = YES;
208 | CLANG_WARN_COMMA = YES;
209 | CLANG_WARN_CONSTANT_CONVERSION = YES;
210 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
211 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
212 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
213 | CLANG_WARN_EMPTY_BODY = YES;
214 | CLANG_WARN_ENUM_CONVERSION = YES;
215 | CLANG_WARN_INFINITE_RECURSION = YES;
216 | CLANG_WARN_INT_CONVERSION = YES;
217 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
218 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
219 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
220 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
221 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
222 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
223 | CLANG_WARN_STRICT_PROTOTYPES = YES;
224 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
225 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
226 | CLANG_WARN_UNREACHABLE_CODE = YES;
227 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
228 | COPY_PHASE_STRIP = NO;
229 | DEBUG_INFORMATION_FORMAT = dwarf;
230 | ENABLE_STRICT_OBJC_MSGSEND = YES;
231 | ENABLE_TESTABILITY = YES;
232 | GCC_C_LANGUAGE_STANDARD = gnu11;
233 | GCC_DYNAMIC_NO_PIC = NO;
234 | GCC_NO_COMMON_BLOCKS = YES;
235 | GCC_OPTIMIZATION_LEVEL = 0;
236 | GCC_PREPROCESSOR_DEFINITIONS = (
237 | "DEBUG=1",
238 | "$(inherited)",
239 | );
240 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
241 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
242 | GCC_WARN_UNDECLARED_SELECTOR = YES;
243 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
244 | GCC_WARN_UNUSED_FUNCTION = YES;
245 | GCC_WARN_UNUSED_VARIABLE = YES;
246 | IPHONEOS_DEPLOYMENT_TARGET = 14.3;
247 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
248 | MTL_FAST_MATH = YES;
249 | ONLY_ACTIVE_ARCH = YES;
250 | SDKROOT = iphoneos;
251 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
252 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
253 | };
254 | name = Debug;
255 | };
256 | A45635D525D169FB0056FC11 /* Release */ = {
257 | isa = XCBuildConfiguration;
258 | buildSettings = {
259 | ALWAYS_SEARCH_USER_PATHS = NO;
260 | CLANG_ANALYZER_NONNULL = YES;
261 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
262 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
263 | CLANG_CXX_LIBRARY = "libc++";
264 | CLANG_ENABLE_MODULES = YES;
265 | CLANG_ENABLE_OBJC_ARC = YES;
266 | CLANG_ENABLE_OBJC_WEAK = YES;
267 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
268 | CLANG_WARN_BOOL_CONVERSION = YES;
269 | CLANG_WARN_COMMA = YES;
270 | CLANG_WARN_CONSTANT_CONVERSION = YES;
271 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
272 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
273 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
274 | CLANG_WARN_EMPTY_BODY = YES;
275 | CLANG_WARN_ENUM_CONVERSION = YES;
276 | CLANG_WARN_INFINITE_RECURSION = YES;
277 | CLANG_WARN_INT_CONVERSION = YES;
278 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
279 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
280 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
281 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
282 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
283 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
284 | CLANG_WARN_STRICT_PROTOTYPES = YES;
285 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
286 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
287 | CLANG_WARN_UNREACHABLE_CODE = YES;
288 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
289 | COPY_PHASE_STRIP = NO;
290 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
291 | ENABLE_NS_ASSERTIONS = NO;
292 | ENABLE_STRICT_OBJC_MSGSEND = YES;
293 | GCC_C_LANGUAGE_STANDARD = gnu11;
294 | GCC_NO_COMMON_BLOCKS = YES;
295 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
296 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
297 | GCC_WARN_UNDECLARED_SELECTOR = YES;
298 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
299 | GCC_WARN_UNUSED_FUNCTION = YES;
300 | GCC_WARN_UNUSED_VARIABLE = YES;
301 | IPHONEOS_DEPLOYMENT_TARGET = 14.3;
302 | MTL_ENABLE_DEBUG_INFO = NO;
303 | MTL_FAST_MATH = YES;
304 | SDKROOT = iphoneos;
305 | SWIFT_COMPILATION_MODE = wholemodule;
306 | SWIFT_OPTIMIZATION_LEVEL = "-O";
307 | VALIDATE_PRODUCT = YES;
308 | };
309 | name = Release;
310 | };
311 | A45635D725D169FB0056FC11 /* Debug */ = {
312 | isa = XCBuildConfiguration;
313 | buildSettings = {
314 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
315 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
316 | CODE_SIGN_STYLE = Automatic;
317 | DEVELOPMENT_ASSET_PATHS = "\"Examples/Preview Content\"";
318 | DEVELOPMENT_TEAM = 6U769Q36UT;
319 | ENABLE_PREVIEWS = YES;
320 | INFOPLIST_FILE = Examples/Info.plist;
321 | IPHONEOS_DEPLOYMENT_TARGET = 13.0;
322 | LD_RUNPATH_SEARCH_PATHS = (
323 | "$(inherited)",
324 | "@executable_path/Frameworks",
325 | );
326 | PRODUCT_BUNDLE_IDENTIFIER = net.aaronlab.examples;
327 | PRODUCT_NAME = "$(TARGET_NAME)";
328 | SWIFT_VERSION = 5.0;
329 | TARGETED_DEVICE_FAMILY = 1;
330 | };
331 | name = Debug;
332 | };
333 | A45635D825D169FB0056FC11 /* Release */ = {
334 | isa = XCBuildConfiguration;
335 | buildSettings = {
336 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
337 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
338 | CODE_SIGN_STYLE = Automatic;
339 | DEVELOPMENT_ASSET_PATHS = "\"Examples/Preview Content\"";
340 | DEVELOPMENT_TEAM = 6U769Q36UT;
341 | ENABLE_PREVIEWS = YES;
342 | INFOPLIST_FILE = Examples/Info.plist;
343 | IPHONEOS_DEPLOYMENT_TARGET = 13.0;
344 | LD_RUNPATH_SEARCH_PATHS = (
345 | "$(inherited)",
346 | "@executable_path/Frameworks",
347 | );
348 | PRODUCT_BUNDLE_IDENTIFIER = net.aaronlab.examples;
349 | PRODUCT_NAME = "$(TARGET_NAME)";
350 | SWIFT_VERSION = 5.0;
351 | TARGETED_DEVICE_FAMILY = 1;
352 | };
353 | name = Release;
354 | };
355 | /* End XCBuildConfiguration section */
356 |
357 | /* Begin XCConfigurationList section */
358 | A45635BD25D169F80056FC11 /* Build configuration list for PBXProject "Examples" */ = {
359 | isa = XCConfigurationList;
360 | buildConfigurations = (
361 | A45635D425D169FB0056FC11 /* Debug */,
362 | A45635D525D169FB0056FC11 /* Release */,
363 | );
364 | defaultConfigurationIsVisible = 0;
365 | defaultConfigurationName = Release;
366 | };
367 | A45635D625D169FB0056FC11 /* Build configuration list for PBXNativeTarget "Examples" */ = {
368 | isa = XCConfigurationList;
369 | buildConfigurations = (
370 | A45635D725D169FB0056FC11 /* Debug */,
371 | A45635D825D169FB0056FC11 /* Release */,
372 | );
373 | defaultConfigurationIsVisible = 0;
374 | defaultConfigurationName = Release;
375 | };
376 | /* End XCConfigurationList section */
377 |
378 | /* Begin XCSwiftPackageProductDependency section */
379 | A45635E725D16A940056FC11 /* Unlocker */ = {
380 | isa = XCSwiftPackageProductDependency;
381 | productName = Unlocker;
382 | };
383 | /* End XCSwiftPackageProductDependency section */
384 | };
385 | rootObject = A45635BA25D169F80056FC11 /* Project object */;
386 | }
387 |
--------------------------------------------------------------------------------
/Examples/Examples.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Examples/Examples.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Examples/Examples/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.swift
3 | // Examples
4 | //
5 | // Created by Aaron Lee on 2021/02/08.
6 | //
7 |
8 | import UIKit
9 |
10 | @main
11 | class AppDelegate: UIResponder, UIApplicationDelegate {
12 |
13 |
14 |
15 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
16 | // Override point for customization after application launch.
17 | return true
18 | }
19 |
20 | // MARK: UISceneSession Lifecycle
21 |
22 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
23 | // Called when a new scene session is being created.
24 | // Use this method to select a configuration to create the new scene with.
25 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
26 | }
27 |
28 | func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) {
29 | // Called when the user discards a scene session.
30 | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
31 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return.
32 | }
33 |
34 |
35 | }
36 |
37 |
--------------------------------------------------------------------------------
/Examples/Examples/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 |
--------------------------------------------------------------------------------
/Examples/Examples/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 |
--------------------------------------------------------------------------------
/Examples/Examples/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Examples/Examples/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 |
--------------------------------------------------------------------------------
/Examples/Examples/CapsuleShape.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CapsuleShape.swift
3 | // Examples
4 | //
5 | // Created by Aaron Lee on 2021/02/08.
6 | //
7 |
8 | import SwiftUI
9 | import Unlocker
10 |
11 | struct CapsuleShape: View {
12 |
13 | @State private var disabled: Bool = false
14 | @State private var percentage: Float = 0.0
15 |
16 | var body: some View {
17 |
18 | ZStack {
19 |
20 | Color.gray
21 |
22 | // Background
23 | HStack {
24 | Spacer(minLength: 0)
25 | Text("Slide to Unlock")
26 | Image(systemName: "chevron.right.2")
27 | } //: H
28 | .padding(.trailing)
29 |
30 | // Slider
31 | Unlocker(disabled: $disabled, percentage: $percentage, minPercentage: 25, threshold: 50.0) { sliderWidth in
32 | ZStack {
33 |
34 | // Slider Background
35 | Rectangle()
36 | .foregroundColor(.primary)
37 |
38 | /*
39 | You can choose when you are going to show your content
40 | with the escaping parameter(CGFloat), which is the slider's width.
41 | */
42 | if sliderWidth > UIScreen.main.bounds.width / 3.0 {
43 |
44 | // Slider Content
45 | HStack {
46 |
47 | Text("Slide to Unlock")
48 | .foregroundColor(.blue)
49 | .lineLimit(1)
50 | .padding(.leading)
51 |
52 | Image(systemName: "chevron.right.2")
53 | .foregroundColor(.blue)
54 |
55 | Spacer(minLength: 0)
56 |
57 | } //: H
58 |
59 | } else {
60 |
61 | /*
62 | Placeholder Image before slide
63 | */
64 | Image(systemName: "chevron.right.2")
65 | .foregroundColor(.blue)
66 |
67 | }
68 |
69 | }
70 | } completion: {
71 | // Your task here
72 | print("CapsuleShape Process Started")
73 |
74 | /*
75 | Since the slider will be "disabled" after fully swiped,
76 | you will need to toggle "disabled" parameter at the end of your process,
77 | so that you will be able to make the slider activated again.
78 | Or if you don't want to make it back,
79 | just leave it there.
80 | Or if you don't want to make it disabled ever,
81 | just use `.constant(false)`
82 | */
83 | DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
84 | print("CapsuleShape Process Done")
85 | disabled.toggle()
86 | }
87 | }
88 |
89 |
90 | } //: Z
91 | .font(.system(size: 12))
92 | .frame(height: 60)
93 | /*
94 | You can smiply make the slider RoundedRectangle
95 | with `.clipShape(Capsule())`
96 | */
97 | .clipShape(Capsule())
98 | .padding()
99 |
100 | }
101 |
102 | }
103 |
--------------------------------------------------------------------------------
/Examples/Examples/ContentView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ContentView.swift
3 | // Examples
4 | //
5 | // Created by Aaron Lee on 2021/02/08.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct ContentView: View {
11 | var body: some View {
12 | TabView {
13 |
14 | Default()
15 | .tabItem {
16 | Text("Default")
17 | }
18 |
19 | RoundedRect()
20 | .tabItem {
21 | Text("Rounded Rectangle")
22 | }
23 |
24 | CapsuleShape()
25 | .tabItem {
26 | Text("Capsule")
27 | }
28 |
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/Examples/Examples/Default.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Default.swift
3 | // Examples
4 | //
5 | // Created by Aaron Lee on 2021/02/07.
6 | //
7 |
8 | import SwiftUI
9 | import Unlocker
10 |
11 | struct Default: View {
12 |
13 | @State private var disabled: Bool = false
14 | @State private var percentage: Float = 0.0
15 |
16 | var body: some View {
17 |
18 | ZStack {
19 |
20 | Color.gray
21 |
22 | // Background
23 | HStack {
24 | Spacer(minLength: 0)
25 | Text("Slide to Unlock")
26 | Image(systemName: "chevron.right.2")
27 | } //: H
28 | .padding(.trailing)
29 |
30 | // Slider
31 | Unlocker(disabled: $disabled, percentage: $percentage, minPercentage: 25, threshold: 50.0, duration: 0.5) { sliderWidth in
32 | ZStack {
33 |
34 | // Slider Background
35 | Rectangle()
36 | .foregroundColor(.primary)
37 |
38 | /*
39 | You can choose when you are going to show your content
40 | with the escaping parameter(CGFloat), which is the slider's width.
41 | */
42 | if sliderWidth > UIScreen.main.bounds.width / 3.0 {
43 |
44 | // Slider Content
45 | HStack {
46 |
47 | Text("Slide to Unlock")
48 | .foregroundColor(.blue)
49 | .lineLimit(1)
50 | .padding(.leading)
51 |
52 | Image(systemName: "chevron.right.2")
53 | .foregroundColor(.blue)
54 |
55 | Spacer(minLength: 0)
56 |
57 | } //: H
58 |
59 | } else {
60 |
61 | /*
62 | Placeholder Image before slide
63 | */
64 | Image(systemName: "chevron.right.2")
65 | .foregroundColor(.blue)
66 |
67 | }
68 |
69 | }
70 | } completion: {
71 | // Your task here
72 | print("Default Process Started")
73 |
74 | /*
75 | Since the slider will be "disabled" after fully swiped,
76 | you will need to toggle "disabled" parameter at the end of your process,
77 | so that you will be able to make the slider activated again.
78 | Or if you don't want to make it back,
79 | just leave it there.
80 | Or if you don't want to make it disabled ever,
81 | just use `.constant(false)`
82 | */
83 | DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
84 | print("Default Process Done")
85 | disabled.toggle()
86 | }
87 | }
88 |
89 |
90 | } //: Z
91 | .font(.system(size: 12))
92 | .frame(height: 60)
93 | .padding()
94 |
95 | }
96 |
97 | }
98 |
--------------------------------------------------------------------------------
/Examples/Examples/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | 1
21 | LSRequiresIPhoneOS
22 |
23 | UIApplicationSceneManifest
24 |
25 | UIApplicationSupportsMultipleScenes
26 |
27 | UISceneConfigurations
28 |
29 | UIWindowSceneSessionRoleApplication
30 |
31 |
32 | UISceneConfigurationName
33 | Default Configuration
34 | UISceneDelegateClassName
35 | $(PRODUCT_MODULE_NAME).SceneDelegate
36 |
37 |
38 |
39 |
40 | UIApplicationSupportsIndirectInputEvents
41 |
42 | UILaunchStoryboardName
43 | LaunchScreen
44 | UIRequiredDeviceCapabilities
45 |
46 | armv7
47 |
48 | UISupportedInterfaceOrientations
49 |
50 | UIInterfaceOrientationPortrait
51 |
52 | UISupportedInterfaceOrientations~ipad
53 |
54 | UIInterfaceOrientationPortrait
55 | UIInterfaceOrientationPortraitUpsideDown
56 | UIInterfaceOrientationLandscapeLeft
57 | UIInterfaceOrientationLandscapeRight
58 |
59 |
60 |
61 |
--------------------------------------------------------------------------------
/Examples/Examples/Preview Content/Preview Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Examples/Examples/RoundedRect.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RoundedRect.swift
3 | // Examples
4 | //
5 | // Created by Aaron Lee on 2021/02/08.
6 | //
7 |
8 | import SwiftUI
9 | import Unlocker
10 |
11 | struct RoundedRect: View {
12 |
13 | @State private var disabled: Bool = false
14 | @State private var percentage: Float = 0.0
15 |
16 | var body: some View {
17 |
18 | ZStack {
19 |
20 | Color.gray
21 |
22 | // Background
23 | HStack {
24 | Spacer(minLength: 0)
25 | Text("Slide to Unlock")
26 | Image(systemName: "chevron.right.2")
27 | } //: H
28 | .padding(.trailing)
29 |
30 | // Slider
31 | Unlocker(disabled: $disabled, percentage: $percentage, minPercentage: 25, threshold: 50.0) { sliderWidth in
32 | ZStack {
33 |
34 | // Slider Background
35 | Rectangle()
36 | .foregroundColor(.primary)
37 |
38 | /*
39 | You can choose when you are going to show your content
40 | with the escaping parameter(CGFloat), which is the slider's width.
41 | */
42 | if sliderWidth > UIScreen.main.bounds.width / 2.8 {
43 |
44 | // Slider Content
45 | HStack {
46 |
47 | Text("Slide to Unlock")
48 | .foregroundColor(.blue)
49 | .lineLimit(1)
50 | .padding(.leading)
51 |
52 | Image(systemName: "chevron.right.2")
53 | .foregroundColor(.blue)
54 |
55 | Spacer(minLength: 0)
56 |
57 | } //: H
58 |
59 | } else {
60 |
61 | /*
62 | Placeholder Image before slide
63 | */
64 | Image(systemName: "chevron.right.2")
65 | .foregroundColor(.blue)
66 |
67 | }
68 |
69 | }
70 | } completion: {
71 | // Your task here
72 | print("RoundedRect Process Started")
73 |
74 | /*
75 | Since the slider will be "disabled" after fully swiped,
76 | you will need to toggle "disabled" parameter at the end of your process,
77 | so that you will be able to make the slider activated again.
78 | Or if you don't want to make it back,
79 | just leave it there.
80 | Or if you don't want to make it disabled ever,
81 | just use `.constant(false)`
82 | */
83 | DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
84 | print("RoundedRect Process Done")
85 | disabled.toggle()
86 | }
87 | }
88 |
89 |
90 | } //: Z
91 | .font(.system(size: 12))
92 | .frame(height: 60)
93 | /*
94 | You can smiply make the slider RoundedRectangle
95 | with `.clipShape(RoundedRectangle(cornerRadius: CGFloat))`
96 | */
97 | .clipShape(RoundedRectangle(cornerRadius: 8))
98 | .padding()
99 |
100 | }
101 |
102 | }
103 |
--------------------------------------------------------------------------------
/Examples/Examples/SceneDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SceneDelegate.swift
3 | // Examples
4 | //
5 | // Created by Aaron Lee on 2021/02/08.
6 | //
7 |
8 | import UIKit
9 | import SwiftUI
10 |
11 | class SceneDelegate: UIResponder, UIWindowSceneDelegate {
12 |
13 | var window: UIWindow?
14 |
15 |
16 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
17 | // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
18 | // If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
19 | // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
20 |
21 | // Create the SwiftUI view that provides the window contents.
22 | let contentView = ContentView()
23 |
24 | // Use a UIHostingController as window root view controller.
25 | if let windowScene = scene as? UIWindowScene {
26 | let window = UIWindow(windowScene: windowScene)
27 | window.rootViewController = UIHostingController(rootView: contentView)
28 | self.window = window
29 | window.makeKeyAndVisible()
30 | }
31 | }
32 |
33 | func sceneDidDisconnect(_ scene: UIScene) {
34 | // Called as the scene is being released by the system.
35 | // This occurs shortly after the scene enters the background, or when its session is discarded.
36 | // Release any resources associated with this scene that can be re-created the next time the scene connects.
37 | // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead).
38 | }
39 |
40 | func sceneDidBecomeActive(_ scene: UIScene) {
41 | // Called when the scene has moved from an inactive state to an active state.
42 | // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive.
43 | }
44 |
45 | func sceneWillResignActive(_ scene: UIScene) {
46 | // Called when the scene will move from an active state to an inactive state.
47 | // This may occur due to temporary interruptions (ex. an incoming phone call).
48 | }
49 |
50 | func sceneWillEnterForeground(_ scene: UIScene) {
51 | // Called as the scene transitions from the background to the foreground.
52 | // Use this method to undo the changes made on entering the background.
53 | }
54 |
55 | func sceneDidEnterBackground(_ scene: UIScene) {
56 | // Called as the scene transitions from the foreground to the background.
57 | // Use this method to save data, release shared resources, and store enough scene-specific state information
58 | // to restore the scene back to its current state.
59 | }
60 |
61 |
62 | }
63 |
64 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Aaron Lee
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:5.3
2 | // The swift-tools-version declares the minimum version of Swift required to build this package.
3 |
4 | import PackageDescription
5 |
6 | let package = Package(
7 | name: "Unlocker",
8 | platforms: [.iOS(.v13)],
9 | products: [
10 | // Products define the executables and libraries a package produces, and make them visible to other packages.
11 | .library(
12 | name: "Unlocker",
13 | targets: ["Unlocker"]),
14 | ],
15 | dependencies: [
16 | // Dependencies declare other packages that this package depends on.
17 | // .package(url: /* package url */, from: "1.0.0"),
18 | ],
19 | targets: [
20 | // Targets are the basic building blocks of a package. A target can define a module or a test suite.
21 | // Targets can depend on other targets in this package, and on products in packages this package depends on.
22 | .target(
23 | name: "Unlocker",
24 | dependencies: []),
25 | .testTarget(
26 | name: "UnlockerTests",
27 | dependencies: ["Unlocker"]),
28 | ]
29 | )
30 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://developer.apple.com/swift)
2 | 
3 | 
4 | [](https://github.com/aaronLab/SweetCardScanner/blob/main/LICENSE)
5 | [](https://github.com/aaronLab/SweetCardScanner/releases)
6 |
7 | # Unlocker
8 |
9 | Unlocker is a simple slider library for [SwiftUI](https://developer.apple.com/xcode/swiftui/), which can look like `Slide to Unlock`.
10 |
11 | You can use this library easily wherever you want, such as `Slide to Purchase` for the payment UX.
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | ## Requirements
24 |
25 | - iOS 13.0 + (due to SwiftUI)
26 |
27 | ## Installation
28 |
29 | - In Xcode, add the URL of this repository in SwiftPM:
30 |
31 | ```http
32 | https://github.com/aaronLab/Unlocker
33 | ```
34 |
35 | ## Usage
36 |
37 | - Import `Unlocker` on top of the file.
38 | - Put `Unlocker` with its parameters in the parenthesis.
39 | - Declare:
40 | ```Swift
41 | Unlocker: View where Content: View(
42 | disabled: Binding,
43 | percentage: Binding,
44 | minPercentage: Float = 25.0,
45 | threshold: Float = 50.0,
46 | duration: Double = 0.3,
47 | @ViewBuilder content: @escaping (_ sliderWidth: CGFloat) -> Content,
48 | completion: (() -> Void)? = nil
49 | )
50 | ```
51 |
52 | ## Parameter
53 |
54 | - `disabled: Binding`:
55 |
56 | - This is a flag to `prevent the slider works multiple times` during the process.
57 | - Since the slider will be "disabled" after fully swiped, `you will need to toggle "disabled"` parameter at the end of your process to make the slider activated again.
58 | - Or if you don't want to make it back, just leave it there.
59 | - Or if you don't want to make it disabled ever, just use `.constant(false)`
60 |
61 | - `percentage: Binding`:
62 |
63 | - This is the percentage of the slider.
64 | - 1.0 is 1 percent.
65 |
66 | - `minPercentage: Float = 25.0`:
67 |
68 | - The default value is `25.0`
69 | - This is `the minimum percentage`. When you set this not zero, the slider would be filled the percentage that you set.
70 | - This is a kind of `placeholders` for the slider.
71 | - You can make it a bit filled by using this parameter.
72 | - 25.0 means 25% of the slider will be filled.
73 |
74 | - `threshold: Float = 50.0`:
75 |
76 | - The default value is `50.0`
77 | - This is the `threshold` where `the completion would start`.
78 | - Also this will make your `slider disabled` to `prevent the slider works multiple times during the process`.
79 | - When the user swipe the slider more than the percentage of this value in the screen, the action closure will be triggered.
80 |
81 | - `duration: Double = 0.3`:
82 |
83 | - The default value is `0.3`
84 | - This is the duration for the `animation` to make `the slider fully filled`.
85 |
86 | - `content: (CGFloat) -> Content`:
87 |
88 | - This is the custom view inside of the slider, which escapes with the width of the slider drag gesture.
89 |
90 | - `completion: (() -> Void)? = nil`:
91 |
92 | - The default value is `nil`
93 | - This `closure action` will be triggered by the slider.
94 | - The action which `will be run` the percentage of the progress of the slider has passed the threshold like a completion.
95 |
96 | ## Example
97 |
98 | ```Swift
99 | import SwiftUI
100 | import Unlocker
101 |
102 | struct CapsuleShape: View {
103 |
104 | @State private var disabled: Bool = false
105 | @State private var percentage: Float = 0.0
106 |
107 | var body: some View {
108 |
109 | ZStack {
110 |
111 | Color.gray
112 |
113 | // Background
114 | HStack {
115 | Spacer(minLength: 0)
116 | Text("Slide to Unlock")
117 | Image(systemName: "chevron.right.2")
118 | } //: H
119 | .padding(.trailing)
120 |
121 | // Slider
122 | Unlocker(disabled: $disabled, percentage: $percentage, minPercentage: 25, threshold: 50.0) { sliderWidth in
123 | ZStack {
124 |
125 | // Slider Background
126 | Rectangle()
127 | .foregroundColor(.primary)
128 |
129 | /*
130 | You can choose when you are going to show your content
131 | with the escaping parameter(CGFloat), which is the slider's width.
132 | */
133 | if sliderWidth > UIScreen.main.bounds.width / 3.0 {
134 |
135 | // Slider Content
136 | HStack {
137 |
138 | Text("Slide to Unlock")
139 | .foregroundColor(.blue)
140 | .lineLimit(1)
141 | .padding(.leading)
142 |
143 | Image(systemName: "chevron.right.2")
144 | .foregroundColor(.blue)
145 |
146 | Spacer(minLength: 0)
147 |
148 | } //: H
149 |
150 | } else {
151 |
152 | /*
153 | Placeholder Image before slide
154 | */
155 | Image(systemName: "chevron.right.2")
156 | .foregroundColor(.blue)
157 |
158 | }
159 |
160 | }
161 | } completion: {
162 | // Your task here
163 | print("CapsuleShape Process Started")
164 |
165 | /*
166 | Since the slider will be "disabled" after fully swiped,
167 | you will need to toggle "disabled" parameter at the end of your process,
168 | so that you will be able to make the slider activated again.
169 | Or if you don't want to make it back,
170 | just leave it there.
171 | Or if you don't want to make it disabled ever,
172 | just use `.constant(false)`
173 | */
174 | DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
175 | print("CapsuleShape Process Done")
176 | disabled.toggle()
177 | }
178 | }
179 |
180 |
181 | } //: Z
182 | .font(.system(size: 12))
183 | .frame(height: 60)
184 | /*
185 | You can smiply make the slider RoundedRectangle
186 | with `.clipShape(Capsule())`
187 | */
188 | .clipShape(Capsule())
189 | .padding()
190 |
191 | }
192 |
193 | }
194 | ```
195 |
196 | ## More Examples
197 |
198 | - See more examples [here](https://github.com/aaronLab/Unlocker/tree/master/Examples/Examples)
199 |
200 | ## License
201 |
202 | Licensed under [MIT](https://github.com/aaronLab/Unlocker/blob/master/LICENSE) license.
203 |
--------------------------------------------------------------------------------
/Sources/Unlocker/Unlocker.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | /// Unlocker is a simple slider library for SwiftUI, which can look like `Slide to Unlock`.
4 | /// You can use this library easily wherever you want, such as `Slide to Purchase` for the payment UX.
5 | public struct Unlocker: View where Content: View {
6 |
7 | /// This is a flag to `prevent the slider works multiple times` during the process.
8 | @Binding var disabled: Bool
9 | /// This is the percentage of the slider.
10 | @Binding var percentage: Float
11 |
12 | /// This is the minimum percentage. When you set this not zero, the slider would be filled the percentage that you set.
13 | private let minPercentage: Float
14 | /// This is the threshold where the completion would start. Also this will make your slider disabled to prevent the slider works multiple times during the process.
15 | private let threshold: Float
16 | /// This is the duration for the animation to make the slider fully filled. The default value is 0.3 second.
17 | private let duration: Double
18 |
19 | /// This is the custom view inside of the slider.
20 | private let content: (_ sliderWidth: CGFloat) -> Content
21 |
22 | /// This is the completion, which will be run the percentage of the progress of the slider has passed the threshold.
23 | private let completion: (() -> Void)?
24 |
25 | /// Unlocker
26 | /// - Parameters:
27 | /// - disabled: This is a flag to `prevent the slider works multiple times` during the process.
28 | /// - percentage: This is the percentage of the slider.
29 | /// - minPercentage: This is the minimum percentage. When you set this not zero, the slider would be filled the percentage that you set.
30 | /// - threshold: This is the threshold where the completion would start. Also this will make your slider disabled to prevent the slider works multiple times during the process.
31 | /// - duration: /// This is the duration for the animation to make the slider fully filled. The default value is 0.3 second.
32 | /// - content: This is the custom view inside of the slider.
33 | /// - completion: This is the completion, which will be run the percentage of the progress of the slider has passed the threshold.
34 | public init(
35 | disabled: Binding,
36 | percentage: Binding,
37 | minPercentage: Float = 25.0,
38 | threshold: Float = 50.0,
39 | duration: Double = 0.3,
40 | @ViewBuilder content: @escaping (_ sliderWidth: CGFloat) -> Content,
41 | completion: (() -> Void)? = nil
42 | ) {
43 | self._disabled = disabled
44 | self._percentage = percentage
45 | self.minPercentage = minPercentage
46 | self.threshold = threshold
47 | self.duration = duration
48 | self.content = content
49 | self.completion = completion
50 | }
51 |
52 | public var body: some View {
53 | GeometryReader { geo in
54 |
55 | // Slider
56 | ZStack(alignment: .leading) {
57 |
58 | let sliderWidth = abs(geo.size.width * CGFloat(percentage / 100))
59 |
60 | // Custom View
61 | content(sliderWidth)
62 | .frame(width: sliderWidth)
63 |
64 | } //: Z
65 | .contentShape(Path(CGRect(origin: .zero, size: geo.size)))
66 | .gesture(
67 | DragGesture(minimumDistance: 0)
68 | .onChanged({ value in
69 | onChanged(with: value, geoProxy: geo)
70 | })
71 | .onEnded({ _ in
72 | onEnded()
73 | })
74 | )
75 | } //: G
76 | .disabled(disabled)
77 | .onAppear {
78 | initPercentage()
79 | }
80 | }
81 |
82 | /// Init Percentage
83 | private func initPercentage() {
84 | percentage = minPercentage
85 | }
86 |
87 | }
88 |
89 | // MARK: - Slider Gestures
90 |
91 | extension Unlocker {
92 |
93 |
94 | /// Drag gesture onChanged for the slider
95 | /// - Parameters:
96 | /// - value: The gesture value
97 | /// - geoProxy: The geometry proxy of the slider
98 | private func onChanged(with value: DragGesture.Value, geoProxy: GeometryProxy) {
99 |
100 | // Dragged to the right
101 | if value.translation.width > 0 {
102 | if percentage >= minPercentage {
103 | DispatchQueue.main.async {
104 | withAnimation(.easeOut) {
105 | // change percentage value
106 | percentage = min(max(minPercentage, Float(value.location.x / geoProxy.size.width * 100)), 100)
107 | }
108 | }
109 | } else {
110 | // Less than min
111 | DispatchQueue.main.async {
112 | withAnimation(.easeOut) {
113 | // reset
114 | percentage = minPercentage
115 | }
116 | }
117 | }
118 | }
119 |
120 | // Dragged to the left / shouldn't change
121 | if value.translation.width < 0 {
122 | DispatchQueue.main.async {
123 | withAnimation(.easeOut) {
124 | // reset
125 | percentage = minPercentage
126 | }
127 | }
128 | }
129 | }
130 |
131 | /// Drag gesture onEnded for the slider
132 | private func onEnded() {
133 | if percentage > threshold {
134 |
135 | disabled = true // prevent user interaction for the completion
136 |
137 | // Fill the slider
138 | DispatchQueue.main.async {
139 | withAnimation(.easeOut(duration: duration)) {
140 | percentage = 100
141 | }
142 | }
143 |
144 | DispatchQueue.main.asyncAfter(deadline: .now() + duration) {
145 | completion?()
146 | }
147 |
148 | // Reset slider
149 | DispatchQueue.main.asyncAfter(deadline: .now() + duration + 0.2) {
150 | withAnimation(.easeOut(duration: duration)) {
151 | percentage = minPercentage
152 | }
153 | }
154 |
155 | } else {
156 |
157 | // Reset slider
158 | DispatchQueue.main.async {
159 | withAnimation(.easeOut) {
160 | percentage = minPercentage
161 | }
162 | }
163 |
164 | }
165 | }
166 |
167 | }
168 |
--------------------------------------------------------------------------------
/Tests/LinuxMain.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 |
3 | import UnlockerTests
4 |
5 | var tests = [XCTestCaseEntry]()
6 | tests += UnlockerTests.allTests()
7 | XCTMain(tests)
8 |
--------------------------------------------------------------------------------
/Tests/UnlockerTests/UnlockerTests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 | @testable import Unlocker
3 |
4 | final class UnlockerTests: XCTestCase {
5 | func testExample() {
6 | // This is an example of a functional test case.
7 | // Use XCTAssert and related functions to verify your tests produce the correct
8 | // results.
9 | XCTAssertEqual(Unlocker().text, "Hello, World!")
10 | }
11 |
12 | static var allTests = [
13 | ("testExample", testExample),
14 | ]
15 | }
16 |
--------------------------------------------------------------------------------
/Tests/UnlockerTests/XCTestManifests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 |
3 | #if !canImport(ObjectiveC)
4 | public func allTests() -> [XCTestCaseEntry] {
5 | return [
6 | testCase(UnlockerTests.allTests),
7 | ]
8 | }
9 | #endif
10 |
--------------------------------------------------------------------------------