├── .gitignore ├── .spi.yml ├── Demo ├── Demo.xcodeproj │ ├── project.pbxproj │ └── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── swiftpm │ │ └── Package.resolved └── Demo │ ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── Contents.json │ ├── ContentView.swift │ ├── DemoApp.swift │ └── RainbowView.swift ├── LICENSE ├── Package.swift ├── README.md └── Sources ├── PasscodeCore ├── API.swift ├── Environment │ ├── Configuration.swift │ ├── DismissPasscodeAction.swift │ └── PasscodeEnvironment.swift ├── Extensions │ └── UIWindow.Level+Extensions.swift ├── Helper │ ├── BackgroundMaterialViewModifier.swift │ ├── PasscodeBlurViewController.swift │ └── ViewModifier.swift ├── Model │ └── PasscodeMode.swift ├── PasscodeRootView.swift └── PasscodeViewModifier.swift └── PasscodeKit ├── API.swift ├── Environment ├── CodeViewConfiguration.swift ├── KeypadViewConfiguration.swift ├── PasscodeConfiguration.swift └── PasscodeSetupConfiguration.swift ├── Extension ├── BindingExtensions.swift ├── StringExtensions.swift ├── UIViewExtensions.swift └── ViewExtensions.swift ├── Model ├── Passcode.swift └── PasscodeType.swift ├── PasscodeChangeViewModifier.swift ├── PasscodeCheckViewModifier.swift ├── PasscodeManager.swift ├── PasscodeSetupViewModifier.swift ├── PasscodeViewModifier.swift ├── Resources └── Passcode.strings └── Views ├── CodeView.swift ├── Helper ├── PasscodeScrollView.swift ├── Shake.swift └── TaskButton.swift ├── KeypadView.swift ├── PasscodeChangeView.swift ├── PasscodeInputView.swift └── PasscodeSetupView.swift /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## User settings 6 | xcuserdata/ 7 | 8 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 9 | *.xcscmblueprint 10 | *.xccheckout 11 | 12 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 13 | build/ 14 | DerivedData/ 15 | *.moved-aside 16 | *.pbxuser 17 | !default.pbxuser 18 | *.mode1v3 19 | !default.mode1v3 20 | *.mode2v3 21 | !default.mode2v3 22 | *.perspectivev3 23 | !default.perspectivev3 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | 28 | ## App packaging 29 | *.ipa 30 | *.dSYM.zip 31 | *.dSYM 32 | 33 | ## Playgrounds 34 | timeline.xctimeline 35 | playground.xcworkspace 36 | 37 | # Swift Package Manager 38 | # 39 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 40 | # Packages/ 41 | # Package.pins 42 | Package.resolved 43 | # *.xcodeproj 44 | # 45 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 46 | # hence it is not needed unless you have added a package configuration file to your project 47 | .swiftpm 48 | 49 | .build/ 50 | 51 | # CocoaPods 52 | # 53 | # We recommend against adding the Pods directory to your .gitignore. However 54 | # you should judge for yourself, the pros and cons are mentioned at: 55 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 56 | # 57 | # Pods/ 58 | # 59 | # Add this line if you want to avoid checking in source code from the Xcode workspace 60 | # *.xcworkspace 61 | 62 | # Carthage 63 | # 64 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 65 | # Carthage/Checkouts 66 | 67 | Carthage/Build/ 68 | 69 | # Accio dependency management 70 | Dependencies/ 71 | .accio/ 72 | 73 | # fastlane 74 | # 75 | # It is recommended to not store the screenshots in the git repo. 76 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 77 | # For more information about the recommended setup visit: 78 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 79 | 80 | fastlane/report.xml 81 | fastlane/Preview.html 82 | fastlane/screenshots/**/*.png 83 | fastlane/test_output 84 | 85 | # Code Injection 86 | # 87 | # After new code Injection tools there's a generated folder /iOSInjectionProject 88 | # https://github.com/johnno1962/injectionforxcode 89 | 90 | iOSInjectionProject/ 91 | -------------------------------------------------------------------------------- /.spi.yml: -------------------------------------------------------------------------------- 1 | version: 1 2 | builder: 3 | configs: 4 | - documentation_targets: [PasscodeKit, PasscodeCore] 5 | custom_documentation_parameters: [--include-extended-types] 6 | platform: ios -------------------------------------------------------------------------------- /Demo/Demo.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 56; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 633B75BE2A8A118C00CD66E3 /* DemoApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 633B75BD2A8A118C00CD66E3 /* DemoApp.swift */; }; 11 | 633B75C02A8A118C00CD66E3 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 633B75BF2A8A118C00CD66E3 /* ContentView.swift */; }; 12 | 633B75C22A8A118D00CD66E3 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 633B75C12A8A118D00CD66E3 /* Assets.xcassets */; }; 13 | 633B75CF2A8A11E000CD66E3 /* PasscodeKit in Frameworks */ = {isa = PBXBuildFile; productRef = 633B75CE2A8A11E000CD66E3 /* PasscodeKit */; }; 14 | 63540F5B2A8A6B5C00C127EF /* RainbowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63540F5A2A8A6B5C00C127EF /* RainbowView.swift */; }; 15 | 63540F5E2A8A6CEE00C127EF /* SimulatorStatusMagic in Frameworks */ = {isa = PBXBuildFile; productRef = 63540F5D2A8A6CEE00C127EF /* SimulatorStatusMagic */; }; 16 | /* End PBXBuildFile section */ 17 | 18 | /* Begin PBXFileReference section */ 19 | 633B75BA2A8A118C00CD66E3 /* Demo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Demo.app; sourceTree = BUILT_PRODUCTS_DIR; }; 20 | 633B75BD2A8A118C00CD66E3 /* DemoApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DemoApp.swift; sourceTree = ""; }; 21 | 633B75BF2A8A118C00CD66E3 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 22 | 633B75C12A8A118D00CD66E3 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 23 | 633B75CC2A8A119800CD66E3 /* PasscodeKit */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = PasscodeKit; path = ..; sourceTree = ""; }; 24 | 63540F5A2A8A6B5C00C127EF /* RainbowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RainbowView.swift; sourceTree = ""; }; 25 | /* End PBXFileReference section */ 26 | 27 | /* Begin PBXFrameworksBuildPhase section */ 28 | 633B75B72A8A118C00CD66E3 /* Frameworks */ = { 29 | isa = PBXFrameworksBuildPhase; 30 | buildActionMask = 2147483647; 31 | files = ( 32 | 633B75CF2A8A11E000CD66E3 /* PasscodeKit in Frameworks */, 33 | 63540F5E2A8A6CEE00C127EF /* SimulatorStatusMagic in Frameworks */, 34 | ); 35 | runOnlyForDeploymentPostprocessing = 0; 36 | }; 37 | /* End PBXFrameworksBuildPhase section */ 38 | 39 | /* Begin PBXGroup section */ 40 | 633B75B12A8A118C00CD66E3 = { 41 | isa = PBXGroup; 42 | children = ( 43 | 633B75CB2A8A119800CD66E3 /* Packages */, 44 | 633B75BC2A8A118C00CD66E3 /* Demo */, 45 | 633B75BB2A8A118C00CD66E3 /* Products */, 46 | ); 47 | sourceTree = ""; 48 | }; 49 | 633B75BB2A8A118C00CD66E3 /* Products */ = { 50 | isa = PBXGroup; 51 | children = ( 52 | 633B75BA2A8A118C00CD66E3 /* Demo.app */, 53 | ); 54 | name = Products; 55 | sourceTree = ""; 56 | }; 57 | 633B75BC2A8A118C00CD66E3 /* Demo */ = { 58 | isa = PBXGroup; 59 | children = ( 60 | 633B75BD2A8A118C00CD66E3 /* DemoApp.swift */, 61 | 63540F5A2A8A6B5C00C127EF /* RainbowView.swift */, 62 | 633B75BF2A8A118C00CD66E3 /* ContentView.swift */, 63 | 633B75C12A8A118D00CD66E3 /* Assets.xcassets */, 64 | ); 65 | path = Demo; 66 | sourceTree = ""; 67 | }; 68 | 633B75CB2A8A119800CD66E3 /* Packages */ = { 69 | isa = PBXGroup; 70 | children = ( 71 | 633B75CC2A8A119800CD66E3 /* PasscodeKit */, 72 | ); 73 | name = Packages; 74 | sourceTree = ""; 75 | }; 76 | /* End PBXGroup section */ 77 | 78 | /* Begin PBXNativeTarget section */ 79 | 633B75B92A8A118C00CD66E3 /* Demo */ = { 80 | isa = PBXNativeTarget; 81 | buildConfigurationList = 633B75C82A8A118D00CD66E3 /* Build configuration list for PBXNativeTarget "Demo" */; 82 | buildPhases = ( 83 | 633B75B62A8A118C00CD66E3 /* Sources */, 84 | 633B75B72A8A118C00CD66E3 /* Frameworks */, 85 | 633B75B82A8A118C00CD66E3 /* Resources */, 86 | ); 87 | buildRules = ( 88 | ); 89 | dependencies = ( 90 | ); 91 | name = Demo; 92 | packageProductDependencies = ( 93 | 633B75CE2A8A11E000CD66E3 /* PasscodeKit */, 94 | 63540F5D2A8A6CEE00C127EF /* SimulatorStatusMagic */, 95 | ); 96 | productName = Demo; 97 | productReference = 633B75BA2A8A118C00CD66E3 /* Demo.app */; 98 | productType = "com.apple.product-type.application"; 99 | }; 100 | /* End PBXNativeTarget section */ 101 | 102 | /* Begin PBXProject section */ 103 | 633B75B22A8A118C00CD66E3 /* Project object */ = { 104 | isa = PBXProject; 105 | attributes = { 106 | BuildIndependentTargetsInParallel = 1; 107 | LastSwiftUpdateCheck = 1430; 108 | LastUpgradeCheck = 1430; 109 | TargetAttributes = { 110 | 633B75B92A8A118C00CD66E3 = { 111 | CreatedOnToolsVersion = 14.3.1; 112 | }; 113 | }; 114 | }; 115 | buildConfigurationList = 633B75B52A8A118C00CD66E3 /* Build configuration list for PBXProject "Demo" */; 116 | compatibilityVersion = "Xcode 14.0"; 117 | developmentRegion = en; 118 | hasScannedForEncodings = 0; 119 | knownRegions = ( 120 | en, 121 | Base, 122 | ); 123 | mainGroup = 633B75B12A8A118C00CD66E3; 124 | packageReferences = ( 125 | 63540F5C2A8A6CEE00C127EF /* XCRemoteSwiftPackageReference "SimulatorStatusMagic" */, 126 | ); 127 | productRefGroup = 633B75BB2A8A118C00CD66E3 /* Products */; 128 | projectDirPath = ""; 129 | projectRoot = ""; 130 | targets = ( 131 | 633B75B92A8A118C00CD66E3 /* Demo */, 132 | ); 133 | }; 134 | /* End PBXProject section */ 135 | 136 | /* Begin PBXResourcesBuildPhase section */ 137 | 633B75B82A8A118C00CD66E3 /* Resources */ = { 138 | isa = PBXResourcesBuildPhase; 139 | buildActionMask = 2147483647; 140 | files = ( 141 | 633B75C22A8A118D00CD66E3 /* Assets.xcassets in Resources */, 142 | ); 143 | runOnlyForDeploymentPostprocessing = 0; 144 | }; 145 | /* End PBXResourcesBuildPhase section */ 146 | 147 | /* Begin PBXSourcesBuildPhase section */ 148 | 633B75B62A8A118C00CD66E3 /* Sources */ = { 149 | isa = PBXSourcesBuildPhase; 150 | buildActionMask = 2147483647; 151 | files = ( 152 | 63540F5B2A8A6B5C00C127EF /* RainbowView.swift in Sources */, 153 | 633B75C02A8A118C00CD66E3 /* ContentView.swift in Sources */, 154 | 633B75BE2A8A118C00CD66E3 /* DemoApp.swift in Sources */, 155 | ); 156 | runOnlyForDeploymentPostprocessing = 0; 157 | }; 158 | /* End PBXSourcesBuildPhase section */ 159 | 160 | /* Begin XCBuildConfiguration section */ 161 | 633B75C62A8A118D00CD66E3 /* Debug */ = { 162 | isa = XCBuildConfiguration; 163 | buildSettings = { 164 | ALWAYS_SEARCH_USER_PATHS = NO; 165 | CLANG_ANALYZER_NONNULL = YES; 166 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 167 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 168 | CLANG_ENABLE_MODULES = YES; 169 | CLANG_ENABLE_OBJC_ARC = YES; 170 | CLANG_ENABLE_OBJC_WEAK = YES; 171 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 172 | CLANG_WARN_BOOL_CONVERSION = YES; 173 | CLANG_WARN_COMMA = YES; 174 | CLANG_WARN_CONSTANT_CONVERSION = YES; 175 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 176 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 177 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 178 | CLANG_WARN_EMPTY_BODY = YES; 179 | CLANG_WARN_ENUM_CONVERSION = YES; 180 | CLANG_WARN_INFINITE_RECURSION = YES; 181 | CLANG_WARN_INT_CONVERSION = YES; 182 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 183 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 184 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 185 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 186 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 187 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 188 | CLANG_WARN_STRICT_PROTOTYPES = YES; 189 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 190 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 191 | CLANG_WARN_UNREACHABLE_CODE = YES; 192 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 193 | COPY_PHASE_STRIP = NO; 194 | DEBUG_INFORMATION_FORMAT = dwarf; 195 | ENABLE_STRICT_OBJC_MSGSEND = YES; 196 | ENABLE_TESTABILITY = YES; 197 | GCC_C_LANGUAGE_STANDARD = gnu11; 198 | GCC_DYNAMIC_NO_PIC = NO; 199 | GCC_NO_COMMON_BLOCKS = YES; 200 | GCC_OPTIMIZATION_LEVEL = 0; 201 | GCC_PREPROCESSOR_DEFINITIONS = ( 202 | "DEBUG=1", 203 | "$(inherited)", 204 | ); 205 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 206 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 207 | GCC_WARN_UNDECLARED_SELECTOR = YES; 208 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 209 | GCC_WARN_UNUSED_FUNCTION = YES; 210 | GCC_WARN_UNUSED_VARIABLE = YES; 211 | IPHONEOS_DEPLOYMENT_TARGET = 16.4; 212 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 213 | MTL_FAST_MATH = YES; 214 | ONLY_ACTIVE_ARCH = YES; 215 | SDKROOT = iphoneos; 216 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 217 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 218 | }; 219 | name = Debug; 220 | }; 221 | 633B75C72A8A118D00CD66E3 /* Release */ = { 222 | isa = XCBuildConfiguration; 223 | buildSettings = { 224 | ALWAYS_SEARCH_USER_PATHS = NO; 225 | CLANG_ANALYZER_NONNULL = YES; 226 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 227 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 228 | CLANG_ENABLE_MODULES = YES; 229 | CLANG_ENABLE_OBJC_ARC = YES; 230 | CLANG_ENABLE_OBJC_WEAK = YES; 231 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 232 | CLANG_WARN_BOOL_CONVERSION = YES; 233 | CLANG_WARN_COMMA = YES; 234 | CLANG_WARN_CONSTANT_CONVERSION = YES; 235 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 236 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 237 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 238 | CLANG_WARN_EMPTY_BODY = YES; 239 | CLANG_WARN_ENUM_CONVERSION = YES; 240 | CLANG_WARN_INFINITE_RECURSION = YES; 241 | CLANG_WARN_INT_CONVERSION = YES; 242 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 243 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 244 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 245 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 246 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 247 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 248 | CLANG_WARN_STRICT_PROTOTYPES = YES; 249 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 250 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 251 | CLANG_WARN_UNREACHABLE_CODE = YES; 252 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 253 | COPY_PHASE_STRIP = NO; 254 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 255 | ENABLE_NS_ASSERTIONS = NO; 256 | ENABLE_STRICT_OBJC_MSGSEND = YES; 257 | GCC_C_LANGUAGE_STANDARD = gnu11; 258 | GCC_NO_COMMON_BLOCKS = YES; 259 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 260 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 261 | GCC_WARN_UNDECLARED_SELECTOR = YES; 262 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 263 | GCC_WARN_UNUSED_FUNCTION = YES; 264 | GCC_WARN_UNUSED_VARIABLE = YES; 265 | IPHONEOS_DEPLOYMENT_TARGET = 16.4; 266 | MTL_ENABLE_DEBUG_INFO = NO; 267 | MTL_FAST_MATH = YES; 268 | SDKROOT = iphoneos; 269 | SWIFT_COMPILATION_MODE = wholemodule; 270 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 271 | VALIDATE_PRODUCT = YES; 272 | }; 273 | name = Release; 274 | }; 275 | 633B75C92A8A118D00CD66E3 /* Debug */ = { 276 | isa = XCBuildConfiguration; 277 | buildSettings = { 278 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 279 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 280 | CODE_SIGN_STYLE = Automatic; 281 | CURRENT_PROJECT_VERSION = 1; 282 | ENABLE_PREVIEWS = YES; 283 | GENERATE_INFOPLIST_FILE = YES; 284 | INFOPLIST_KEY_NSFaceIDUsageDescription = "Face ID to unlock the app."; 285 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 286 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 287 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 288 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 289 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 290 | IPHONEOS_DEPLOYMENT_TARGET = 16.0; 291 | LD_RUNPATH_SEARCH_PATHS = ( 292 | "$(inherited)", 293 | "@executable_path/Frameworks", 294 | ); 295 | MARKETING_VERSION = 1.0; 296 | PRODUCT_BUNDLE_IDENTIFIER = at.davidwalter.PasscodeKit.Demo; 297 | PRODUCT_NAME = "$(TARGET_NAME)"; 298 | SWIFT_EMIT_LOC_STRINGS = YES; 299 | SWIFT_VERSION = 5.0; 300 | TARGETED_DEVICE_FAMILY = "1,2"; 301 | }; 302 | name = Debug; 303 | }; 304 | 633B75CA2A8A118D00CD66E3 /* Release */ = { 305 | isa = XCBuildConfiguration; 306 | buildSettings = { 307 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 308 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 309 | CODE_SIGN_STYLE = Automatic; 310 | CURRENT_PROJECT_VERSION = 1; 311 | ENABLE_PREVIEWS = YES; 312 | GENERATE_INFOPLIST_FILE = YES; 313 | INFOPLIST_KEY_NSFaceIDUsageDescription = "Face ID to unlock the app."; 314 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 315 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 316 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 317 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 318 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 319 | IPHONEOS_DEPLOYMENT_TARGET = 16.0; 320 | LD_RUNPATH_SEARCH_PATHS = ( 321 | "$(inherited)", 322 | "@executable_path/Frameworks", 323 | ); 324 | MARKETING_VERSION = 1.0; 325 | PRODUCT_BUNDLE_IDENTIFIER = at.davidwalter.PasscodeKit.Demo; 326 | PRODUCT_NAME = "$(TARGET_NAME)"; 327 | SWIFT_EMIT_LOC_STRINGS = YES; 328 | SWIFT_VERSION = 5.0; 329 | TARGETED_DEVICE_FAMILY = "1,2"; 330 | }; 331 | name = Release; 332 | }; 333 | /* End XCBuildConfiguration section */ 334 | 335 | /* Begin XCConfigurationList section */ 336 | 633B75B52A8A118C00CD66E3 /* Build configuration list for PBXProject "Demo" */ = { 337 | isa = XCConfigurationList; 338 | buildConfigurations = ( 339 | 633B75C62A8A118D00CD66E3 /* Debug */, 340 | 633B75C72A8A118D00CD66E3 /* Release */, 341 | ); 342 | defaultConfigurationIsVisible = 0; 343 | defaultConfigurationName = Release; 344 | }; 345 | 633B75C82A8A118D00CD66E3 /* Build configuration list for PBXNativeTarget "Demo" */ = { 346 | isa = XCConfigurationList; 347 | buildConfigurations = ( 348 | 633B75C92A8A118D00CD66E3 /* Debug */, 349 | 633B75CA2A8A118D00CD66E3 /* Release */, 350 | ); 351 | defaultConfigurationIsVisible = 0; 352 | defaultConfigurationName = Release; 353 | }; 354 | /* End XCConfigurationList section */ 355 | 356 | /* Begin XCRemoteSwiftPackageReference section */ 357 | 63540F5C2A8A6CEE00C127EF /* XCRemoteSwiftPackageReference "SimulatorStatusMagic" */ = { 358 | isa = XCRemoteSwiftPackageReference; 359 | repositoryURL = "https://github.com/shinydevelopment/SimulatorStatusMagic.git"; 360 | requirement = { 361 | kind = upToNextMajorVersion; 362 | minimumVersion = 2.0.0; 363 | }; 364 | }; 365 | /* End XCRemoteSwiftPackageReference section */ 366 | 367 | /* Begin XCSwiftPackageProductDependency section */ 368 | 633B75CE2A8A11E000CD66E3 /* PasscodeKit */ = { 369 | isa = XCSwiftPackageProductDependency; 370 | productName = PasscodeKit; 371 | }; 372 | 63540F5D2A8A6CEE00C127EF /* SimulatorStatusMagic */ = { 373 | isa = XCSwiftPackageProductDependency; 374 | package = 63540F5C2A8A6CEE00C127EF /* XCRemoteSwiftPackageReference "SimulatorStatusMagic" */; 375 | productName = SimulatorStatusMagic; 376 | }; 377 | /* End XCSwiftPackageProductDependency section */ 378 | }; 379 | rootObject = 633B75B22A8A118C00CD66E3 /* Project object */; 380 | } 381 | -------------------------------------------------------------------------------- /Demo/Demo.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Demo/Demo.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Demo/Demo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "keychain-swift", 5 | "kind" : "remoteSourceControl", 6 | "location" : "https://github.com/evgenyneu/keychain-swift.git", 7 | "state" : { 8 | "revision" : "265806607b45687a3d646e4c9837c31c90f202e8", 9 | "version" : "21.0.0" 10 | } 11 | }, 12 | { 13 | "identity" : "simulatorstatusmagic", 14 | "kind" : "remoteSourceControl", 15 | "location" : "https://github.com/shinydevelopment/SimulatorStatusMagic.git", 16 | "state" : { 17 | "revision" : "76095ec820674457a11dc3cc621d8a5c369eea0e", 18 | "version" : "2.7.0" 19 | } 20 | }, 21 | { 22 | "identity" : "swiftui-introspect", 23 | "kind" : "remoteSourceControl", 24 | "location" : "https://github.com/siteline/swiftui-introspect.git", 25 | "state" : { 26 | "revision" : "0cd2a5a5895306bc21d54a2254302d24a9a571e4", 27 | "version" : "1.1.3" 28 | } 29 | }, 30 | { 31 | "identity" : "windowreader", 32 | "kind" : "remoteSourceControl", 33 | "location" : "https://github.com/divadretlaw/WindowReader", 34 | "state" : { 35 | "revision" : "7e1f994df0ed26a7d7a139f1fce2537d2c931059", 36 | "version" : "2.0.0" 37 | } 38 | }, 39 | { 40 | "identity" : "windowscenereader", 41 | "kind" : "remoteSourceControl", 42 | "location" : "https://github.com/divadretlaw/WindowSceneReader.git", 43 | "state" : { 44 | "revision" : "0779fbf801a723570f90158aa5e618ff885b6c47", 45 | "version" : "3.0.0" 46 | } 47 | } 48 | ], 49 | "version" : 2 50 | } 51 | -------------------------------------------------------------------------------- /Demo/Demo/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 | -------------------------------------------------------------------------------- /Demo/Demo/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "platform" : "ios", 6 | "size" : "1024x1024" 7 | } 8 | ], 9 | "info" : { 10 | "author" : "xcode", 11 | "version" : 1 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Demo/Demo/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Demo/Demo/ContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentView.swift 3 | // Demo 4 | // 5 | // Created by David Walter on 08.08.23. 6 | // 7 | 8 | import SwiftUI 9 | import PasscodeKit 10 | import KeychainSwift 11 | 12 | struct ContentView: View { 13 | @Environment(\.passcode.manager) private var passcodeManager 14 | 15 | @State private var setupPasscode = false 16 | @State private var changePasscode = false 17 | @State private var checkPasscodeOrBiometrics = false 18 | @State private var checkPasscode = false 19 | 20 | @State private var hasPasscode = false 21 | 22 | var body: some View { 23 | NavigationStack { 24 | List { 25 | Section { 26 | Button { 27 | setupPasscode = true 28 | } label: { 29 | Text("Setup Passcode") 30 | } 31 | } header: { 32 | Text("Setup") 33 | } 34 | .disabled(hasPasscode) 35 | 36 | Section { 37 | Button { 38 | changePasscode = true 39 | } label: { 40 | Text("Change Passcode") 41 | } 42 | } header: { 43 | Text("Change") 44 | } 45 | .disabled(!hasPasscode) 46 | 47 | Button { 48 | checkPasscodeOrBiometrics = true 49 | } label: { 50 | Text("Check Passcode (Biometrics Enabled)") 51 | } 52 | .disabled(!hasPasscode) 53 | 54 | Button { 55 | checkPasscode = true 56 | } label: { 57 | Text("Check Passcode (Biometrics Disabled)") 58 | } 59 | .disabled(!hasPasscode) 60 | 61 | Button(role: .destructive) { 62 | passcodeManager.delete() 63 | evaluatePasscode() 64 | } label: { 65 | Text("Delete Passcode") 66 | } 67 | .disabled(!hasPasscode) 68 | 69 | Section { 70 | NavigationLink { 71 | VStack(spacing: 0) { 72 | RainbowView() 73 | } 74 | .ignoresSafeArea() 75 | } label: { 76 | Text("Rainbow") 77 | } 78 | 79 | Group { 80 | Color.red 81 | Color.orange 82 | Color.yellow 83 | Color.green 84 | Color.blue 85 | Color.purple 86 | } 87 | .listRowInsets(EdgeInsets()) 88 | } header: { 89 | Text("Colors") 90 | } 91 | } 92 | .navigationTitle("Demo") 93 | .onAppear { 94 | evaluatePasscode() 95 | } 96 | .setupPasscode(isPresented: $setupPasscode, types: passcodes) { _ in 97 | evaluatePasscode() 98 | } 99 | .changePasscode(isPresented: $changePasscode, types: passcodes) { _ in 100 | evaluatePasscode() 101 | } 102 | .checkPasscode(isPresented: $checkPasscode) { _ in 103 | evaluatePasscode() 104 | } 105 | .checkPasscode(isPresented: $checkPasscodeOrBiometrics, allowBiometrics: true) { _ in 106 | evaluatePasscode() 107 | } 108 | } 109 | } 110 | 111 | var passcodes: [PasscodeType] { 112 | [.numeric(4), .numeric(6), .alphanumeric] 113 | } 114 | 115 | func evaluatePasscode() { 116 | hasPasscode = passcodeManager.isSetup 117 | } 118 | } 119 | 120 | struct ContentView_Previews: PreviewProvider { 121 | static var previews: some View { 122 | ContentView() 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /Demo/Demo/DemoApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DemoApp.swift 3 | // Demo 4 | // 5 | // Created by David Walter on 08.08.23. 6 | // 7 | 8 | import SwiftUI 9 | import PasscodeKit 10 | import SimulatorStatusMagic 11 | 12 | @main 13 | struct DemoApp: App { 14 | var body: some Scene { 15 | WindowGroup { 16 | ContentView() 17 | .passcode("Enter Passcode") 18 | .onAppear { 19 | // SDStatusBarManager.sharedInstance().enableOverrides() 20 | } 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Demo/Demo/RainbowView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RainbowView.swift 3 | // Demo 4 | // 5 | // Created by David Walter on 14.08.23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct RainbowView: View { 11 | var body: some View { 12 | Group { 13 | Color.red 14 | Color.orange 15 | Color.yellow 16 | Color.green 17 | Color.blue 18 | Color.purple 19 | } 20 | } 21 | } 22 | 23 | struct RainbowView_Previews: PreviewProvider { 24 | static var previews: some View { 25 | VStack { 26 | RainbowView() 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 David Walter 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.9 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "PasscodeKit", 7 | platforms: [ 8 | .iOS(.v15), 9 | .visionOS(.v1) 10 | ], 11 | products: [ 12 | .library( 13 | name: "PasscodeKit", 14 | targets: ["PasscodeKit"] 15 | ), 16 | .library( 17 | name: "PasscodeCore", 18 | targets: ["PasscodeCore"] 19 | ) 20 | ], 21 | dependencies: [ 22 | .package(url: "https://github.com/divadretlaw/WindowSceneReader.git", from: "3.0.0"), 23 | .package(url: "https://github.com/evgenyneu/keychain-swift.git", from: "21.0.0"), 24 | .package(url: "https://github.com/siteline/swiftui-introspect.git", from: "1.0.0") 25 | ], 26 | targets: [ 27 | .target( 28 | name: "PasscodeKit", 29 | dependencies: [ 30 | "PasscodeCore", 31 | .product(name: "SwiftUIIntrospect", package: "swiftui-introspect"), 32 | .product(name: "KeychainSwift", package: "keychain-swift") 33 | ], 34 | resources: [.process("Resources")] 35 | ), 36 | .target( 37 | name: "PasscodeCore", 38 | dependencies: ["WindowSceneReader"] 39 | ) 40 | ] 41 | ) 42 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PasscodeKit 2 | 3 | [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fdivadretlaw%2FPasscodeKit%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/divadretlaw/PasscodeKit) 4 | [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fdivadretlaw%2FPasscodeKit%2Fbadge%3Ftype%3Dplatforms)](https://swiftpackageindex.com/divadretlaw/PasscodeKit) 5 | 6 | Easily add a passcode to your iOS app 7 | 8 | ## Usage 9 | 10 | PasscodeKit is split into two modules and depending on what you need you can use the main or core module. 11 | 12 | ### PasscodeKit 13 | 14 | Screenshot 15 | 16 | The default module, with UI and handling already setup. Simply add `.passcode(title:hint:)`, with an optional title and hint view, to your root view. 17 | 18 | ```swift 19 | @main 20 | struct MyApp: App { 21 | var body: some Scene { 22 | WindowGroup { 23 | ContentView() 24 | .passcode("Enter Passcode") { 25 | // Optional view as a hint above the code view 26 | } 27 | } 28 | } 29 | } 30 | ``` 31 | 32 | The passcode has to be setup by the user in order to be used, you can add this modifier to any view to start the setup. 33 | 34 | ```swift 35 | .setupPasscode(isPresented: $setupPasscode, type: .numeric(6)) 36 | ``` 37 | 38 | To remove the passcode again, use the environment variables to access the . You can also set the manager environment variable to override the used Keychain instance and key where the key that is used to store the data. 39 | 40 | ```swift 41 | @Environment(\.passcode.manager) private var passcodeManager 42 | ``` 43 | 44 | Then simply remove the entry for the passcode 45 | 46 | ```swift 47 | passcodeManager.delete() 48 | ``` 49 | 50 | ### PasscodeCore 51 | 52 | The core module, that handles displaying the passcode window. By default it has no UI to enter a passcode or setting up and storing the passcode, but you can use this as a base to implement your own UI. 53 | 54 | ```swift 55 | .passcode(mode: PasscodeMode) { dismiss in 56 | // some Passcode input UI, call `dismiss(animated:)` once finished 57 | } background: { 58 | // some optional background view 59 | } 60 | ``` 61 | 62 | #### Localization 63 | 64 | Customize / Localize the `PasscodeKit` by providing a `Passcode.strings` file in your main app bundle. See the default [Passcode.strings](Sources/PasscodeKit/Resources/Passcode.strings) file for English Strings. 65 | 66 | ## Installation 67 | 68 | ### Xcode 69 | 70 | Add the following package URL to Xcode 71 | 72 | ``` 73 | https://github.com/divadretlaw/PasscodeKit 74 | ``` 75 | 76 | Select the module you need 77 | 78 | ![Xcode](https://github.com/divadretlaw/PasscodeKit/assets/6899256/081ca701-deb1-4230-9e8e-25d9fe24e803) 79 | 80 | ### Swift Package Manager 81 | 82 | ```swift 83 | let package = Package( 84 | dependencies: [ 85 | .package(url: "https://github.com/divadretlaw/PasscodeKit.git", from: "0.7.0") 86 | ], 87 | targets: [ 88 | .target( 89 | name: <#Target Name#>, 90 | dependencies: [ 91 | .product(name: "PasscodeKit", package: "PasscodeKit") 92 | ] 93 | ) 94 | ] 95 | ) 96 | ``` 97 | 98 | ## License 99 | 100 | See [LICENSE](LICENSE) 101 | -------------------------------------------------------------------------------- /Sources/PasscodeCore/API.swift: -------------------------------------------------------------------------------- 1 | // 2 | // API.swift 3 | // PasscodeCore 4 | // 5 | // Created by David Walter on 12.08.23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | public extension View { 11 | /// Automatially present a passcode 12 | /// 13 | /// - Parameters: 14 | /// - mode: The mode to present the passcode. 15 | /// - material: The background material of the passcode view. 16 | /// - input: The passcode input view. Call the given `dismiss` action when the passcode was validated. 17 | /// 18 | /// - Returns: A view with a passcode enabled. 19 | @MainActor func passcode( 20 | mode: PasscodeMode = .default, 21 | background material: Material? = .regular, 22 | @ViewBuilder input: @escaping (_ dismiss: DismissPasscodeAction) -> I 23 | ) -> some View where I: View { 24 | modifier { 25 | PasscodeViewModifierHelper(mode: mode, input: input) { 26 | Color(uiColor: .systemBackground) 27 | .modifier(BackgroundMaterialViewModifier(material: material)) 28 | } 29 | } 30 | } 31 | 32 | /// Automatially present a passcode 33 | /// 34 | /// - Parameters: 35 | /// - mode: The mode to present the passcode. 36 | /// - input: The passcode input view. Call the given `dismiss` action when the passcode was validated. 37 | /// - background: The background view of the passcode view. 38 | /// 39 | /// - Returns: A view with a passcode enabled. 40 | @MainActor func passcode( 41 | mode: PasscodeMode = .default, 42 | @ViewBuilder input: @escaping (_ dismiss: DismissPasscodeAction) -> I, 43 | @ViewBuilder background: @escaping () -> B 44 | ) -> some View where I: View, B: View { 45 | modifier { 46 | PasscodeViewModifierHelper(mode: mode, input: input, background: background) 47 | } 48 | } 49 | 50 | // MARK: Privacy View 51 | 52 | /// Automatially present a privacy view 53 | /// 54 | /// - Parameters: 55 | /// - material: The background material of the privacy vew. 56 | /// 57 | /// - Returns: A view with a privacy view enabled. 58 | @MainActor func privacyView( 59 | background material: Material? = .regular 60 | ) -> some View { 61 | passcode(mode: .hideInAppSwitcher, background: material) { _ in 62 | EmptyView() 63 | } 64 | } 65 | 66 | /// Automatially present a privacy view 67 | /// 68 | /// - Parameters: 69 | /// - windowScene: The window scene to present the privacy vew on. 70 | /// - background: The background view of the privacy view. 71 | /// 72 | /// - Returns: A view with a privacy view enabled. 73 | @MainActor func privacyView( 74 | @ViewBuilder background: @escaping () -> B 75 | ) -> some View where B: View { 76 | passcode(mode: .hideInAppSwitcher) { _ in 77 | EmptyView() 78 | } background: { 79 | background() 80 | } 81 | } 82 | } 83 | 84 | // MARK: - UIWindowScene 85 | 86 | public extension View { 87 | /// Automatially present a passcode 88 | /// 89 | /// - Parameters: 90 | /// - windowScene: The window scene to present the passcode on. 91 | /// - mode: The mode to present the passcode. 92 | /// - material: The background material of the passcode view. 93 | /// - input: The passcode input view. Call the given `dismiss` action when the passcode was validated. 94 | /// 95 | /// - Returns: A view with a passcode enabled. 96 | @MainActor func passcode( 97 | on windowScene: UIWindowScene, 98 | mode: PasscodeMode = .default, 99 | background material: Material? = .regular, 100 | @ViewBuilder input: @escaping (_ dismiss: DismissPasscodeAction) -> I 101 | ) -> some View where I: View { 102 | modifier { 103 | PasscodeViewModifier(windowScene: windowScene, mode: mode, input: input) { 104 | Color(uiColor: .systemBackground) 105 | .modifier(BackgroundMaterialViewModifier(material: material)) 106 | } 107 | } 108 | } 109 | 110 | /// Automatially present a passcode 111 | /// 112 | /// - Parameters: 113 | /// - windowScene: The window scene to present the passcode on. 114 | /// - mode: The mode to present the passcode. 115 | /// - material: The background material of the passcode view. 116 | /// - input: The passcode input view. Call the given `dismiss` action when the passcode was validated. 117 | /// - background: The background view of the passcode view. 118 | /// 119 | /// - Returns: A view with a passcode enabled. 120 | @MainActor func passcode( 121 | on windowScene: UIWindowScene, 122 | mode: PasscodeMode = .default, 123 | @ViewBuilder input: @escaping (_ dismiss: DismissPasscodeAction) -> I, 124 | @ViewBuilder background: @escaping () -> B 125 | ) -> some View where I: View, B: View { 126 | modifier { 127 | PasscodeViewModifier(windowScene: windowScene, mode: mode, input: input, background: background) 128 | } 129 | } 130 | 131 | // MARK: Privacy View 132 | 133 | /// Automatially present a privacy view 134 | /// 135 | /// - Parameters: 136 | /// - windowScene: The window scene to present the privacy vew on. 137 | /// - material: The background material of the privacy vew. 138 | /// 139 | /// - Returns: A view with a privacy view enabled. 140 | @MainActor func privacyView( 141 | on windowScene: UIWindowScene, 142 | background material: Material? = .regular 143 | ) -> some View { 144 | passcode(on: windowScene, mode: .hideInAppSwitcher, background: material) { _ in 145 | EmptyView() 146 | } 147 | } 148 | 149 | /// Automatially present a privacy view 150 | /// 151 | /// - Parameters: 152 | /// - windowScene: The window scene to present the privacy vew on. 153 | /// - background: The background view of the privacy view. 154 | /// 155 | /// - Returns: A view with a privacy view enabled. 156 | @MainActor func privacyView( 157 | on windowScene: UIWindowScene, 158 | @ViewBuilder background: @escaping () -> B 159 | ) -> some View where B: View { 160 | passcode(on: windowScene, mode: .hideInAppSwitcher) { _ in 161 | EmptyView() 162 | } background: { 163 | background() 164 | } 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /Sources/PasscodeCore/Environment/Configuration.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Configuration.swift 3 | // PasscodeCore 4 | // 5 | // Created by David Walter on 02.09.23. 6 | // 7 | 8 | import SwiftUI 9 | import UIKit 10 | 11 | public extension PasscodeEnvironmentValues { 12 | /// The duration of the dismiss animation. 13 | /// 14 | /// > Info: Defaults to `0.3` 15 | var animatedDismissDuration: TimeInterval { 16 | get { self[AnimatedDismissDurationKey.self] } 17 | set { self[AnimatedDismissDurationKey.self] = newValue } 18 | } 19 | 20 | /// The `ColorScheme` of the passcode overlay. 21 | var colorScheme: ColorScheme? { 22 | get { self[ColorSchemeKey.self] } 23 | set { self[ColorSchemeKey.self] = newValue } 24 | } 25 | 26 | /// The `UIWindow.Level` of the passcode overlay. 27 | /// 28 | /// > Info: Defaults to `UIWindow.Level.passcode` 29 | /// 30 | /// If you need the passcode window to be at a higher (or lower) level 31 | /// simply set this to the desired `UIWindow.Level` 32 | var windowLevel: UIWindow.Level { 33 | get { self[UIWindowLevelKey.self] } 34 | set { self[UIWindowLevelKey.self] = newValue } 35 | } 36 | } 37 | 38 | private struct AnimatedDismissDurationKey: EnvironmentKey { 39 | static var defaultValue: TimeInterval { 0.3 } 40 | } 41 | 42 | private struct ColorSchemeKey: EnvironmentKey { 43 | static var defaultValue: ColorScheme? { nil } 44 | } 45 | 46 | private struct UIWindowLevelKey: EnvironmentKey { 47 | static var defaultValue: UIWindow.Level { .passcode } 48 | } 49 | -------------------------------------------------------------------------------- /Sources/PasscodeCore/Environment/DismissPasscodeAction.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DismissPasscodeAction.swift 3 | // PasscodeCore 4 | // 5 | // Created by David Walter on 12.08.23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /// An action that dismisses the current passcode presentation. 11 | /// 12 | /// Use the ``EnvironmentValues/dismiss`` environment value to get the instance 13 | /// of this structure for a given ``Environment``. Then call the instance 14 | /// to perform the dismissal. You call the instance directly because 15 | /// it defines a ``DismissPasscodeAction/callAsFunction()`` 16 | /// method that Swift calls when you call the instance. 17 | public struct DismissPasscodeAction { 18 | var action: (_ animated: Bool) -> Void 19 | 20 | /// Dismisses the passcode if it is currently presented. 21 | /// 22 | /// Don't call this method directly. SwiftUI calls it for you when you 23 | /// call the ``DismissPasscodeAction`` structure that you get from the 24 | /// ``Environment``: 25 | /// 26 | /// ```swift 27 | /// private struct PasscodeView: View { 28 | /// @Environment(\.dismissPasscode) private var dismissPasscode 29 | /// 30 | /// var body: some View { 31 | /// Button("Dismiss Passcode") { 32 | /// dismissPasscode(animated: true) // Implicitly calls dismissPasscode.callAsFunction(animated:) 33 | /// } 34 | /// } 35 | /// } 36 | /// ``` 37 | /// 38 | /// For information about how Swift uses the `callAsFunction()` method to 39 | /// simplify call site syntax, see 40 | /// [Methods with Special Names](https://docs.swift.org/swift-book/ReferenceManual/Declarations.html#ID622) 41 | /// in *The Swift Programming Language*. 42 | public func callAsFunction(animated: Bool) { 43 | action(animated) 44 | } 45 | } 46 | 47 | public extension PasscodeEnvironmentValues { 48 | /// An action that dismisses the current passcode presentation. 49 | var dismiss: DismissPasscodeAction { 50 | get { self[DismissPasscodeActionKey.self] } 51 | set { self[DismissPasscodeActionKey.self] = newValue } 52 | } 53 | } 54 | 55 | extension View { 56 | /// Performs an action when the passcode shoudl dismiss. 57 | /// 58 | /// - Parameters: 59 | /// - action: A closure to run when passcode should dismiss. The closure 60 | /// takes a `animated` parameter that indicates whether the dismiss should be animated 61 | /// or not. 62 | /// 63 | /// - Returns: A view that runs an action when the passcode should dismiss. 64 | func onDismissPasscode(perform action: @escaping (_ animated: Bool) -> Void) -> some View { 65 | environment(\.passcode.dismiss, DismissPasscodeAction(action: action)) 66 | } 67 | 68 | /// Performs an action when the passcode shoudl dismiss. 69 | /// 70 | /// - Parameters: 71 | /// - action: The ``DismissPasscodeAction`` to run when the passcode should dismiss. 72 | /// 73 | /// - Returns: A view that runs an action when the passcode should dismiss. 74 | func onDismissPasscode(action: DismissPasscodeAction) -> some View { 75 | environment(\.passcode.dismiss, action) 76 | } 77 | } 78 | 79 | struct DismissPasscodeActionKey: EnvironmentKey { 80 | static var defaultValue: DismissPasscodeAction { 81 | DismissPasscodeAction { _ in 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /Sources/PasscodeCore/Environment/PasscodeEnvironment.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PasscodeEnvironment.swift 3 | // PasscodeCore 4 | // 5 | // Created by David Walter on 02.09.23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /// A collection of passcode environment values propagated through a view hierarchy. 11 | public struct PasscodeEnvironmentValues: CustomStringConvertible { 12 | private var values: EnvironmentValues 13 | 14 | /// Creates a passcode environment values instance. 15 | /// 16 | /// You don't typically create an instance of ``EnvironmentValues`` 17 | /// directly. Doing so would provide access only to default values that 18 | /// don't update based on system settings or device characteristics. 19 | /// Instead, you rely on an environment values' instance 20 | /// that SwiftUI manages for you when you use the ``Environment`` 21 | /// property wrapper and the ``View/environment(_:_:)`` view modifier. 22 | public init() { 23 | self.values = EnvironmentValues() 24 | } 25 | 26 | /// Accesses the environment value associated with a custom key. 27 | /// 28 | /// Create custom environment values by defining a key 29 | /// that conforms to the ``EnvironmentKey`` protocol, and then using that 30 | /// key with the subscript operator of the ``EnvironmentValues`` structure 31 | /// to get and set a value for that key: 32 | /// 33 | /// ```swift 34 | /// private struct MyEnvironmentKey: EnvironmentKey { 35 | /// static let defaultValue: String = "Default value" 36 | /// } 37 | /// 38 | /// extension PasscodeEnvironmentValues { 39 | /// var myCustomValue: String { 40 | /// get { self[MyEnvironmentKey.self] } 41 | /// set { self[MyEnvironmentKey.self] = newValue } 42 | /// } 43 | /// } 44 | /// ``` 45 | /// 46 | /// You use custom environment values the same way you use system-provided 47 | /// values, setting a value with the ``View/environment(_:_:)`` view 48 | /// modifier, and reading values with the ``Environment`` property wrapper. 49 | /// You can also provide a dedicated view modifier as a convenience for 50 | /// setting the value: 51 | /// 52 | /// ```swift 53 | /// extension View { 54 | /// func myCustomValue(_ myCustomValue: String) -> some View { 55 | /// environment(\.passcode.myCustomValue, myCustomValue) 56 | /// } 57 | /// } 58 | /// ``` 59 | public subscript(key: K.Type) -> K.Value where K: EnvironmentKey { 60 | get { values[key] } 61 | set { values[key] = newValue } 62 | } 63 | 64 | /// A string that represents the contents of the environment values instance. 65 | public var description: String { 66 | values.description 67 | } 68 | } 69 | 70 | private struct PasscodeEnvironmentKey: EnvironmentKey { 71 | static var defaultValue: PasscodeEnvironmentValues { 72 | PasscodeEnvironmentValues() 73 | } 74 | } 75 | 76 | extension EnvironmentValues { 77 | /// The passcode environment values. A subset of `SwiftUI.EnvironmentValues` 78 | /// that only contains values related to passcode. 79 | public var passcode: PasscodeEnvironmentValues { 80 | get { self[PasscodeEnvironmentKey.self] } 81 | set { self[PasscodeEnvironmentKey.self] = newValue } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /Sources/PasscodeCore/Extensions/UIWindow.Level+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIWindow.Level+Extensions.swift 3 | // PasscodeCore 4 | // 5 | // Created by David Walter on 13.10.23. 6 | // 7 | 8 | import UIKit 9 | 10 | extension UIWindow.Level { 11 | // Windows at this level appear on top of your app's main window and alerts. 12 | static var passcode: UIWindow.Level { 13 | /// `UIWindow.Level.alert` is `20_000` but in case this value is changed in the future 14 | /// we still want to be 1000 above alert, in order to fit potential other windows 15 | /// in between 16 | return UIWindow.Level(max(21_000, UIWindow.Level.alert.rawValue + 1000)) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Sources/PasscodeCore/Helper/BackgroundMaterialViewModifier.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BackgroundMaterialViewModifier.swift 3 | // PasscodeCore 4 | // 5 | // Created by David Walter on 12.08.23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct BackgroundMaterialViewModifier: ViewModifier { 11 | var material: Material? 12 | 13 | func body(content: Content) -> some View { 14 | if let material = material { 15 | content 16 | .opacity(0) 17 | .background(material, ignoresSafeAreaEdges: .all) 18 | } else { 19 | content 20 | .ignoresSafeArea(edges: .all) 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Sources/PasscodeCore/Helper/PasscodeBlurViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PasscodeBlurViewController.swift 3 | // PasscodeCore 4 | // 5 | // Created by David Walter on 12.08.23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | final class PasscodeBlurViewController: UIHostingController where Content: View { 11 | override func viewDidLoad() { 12 | super.viewDidLoad() 13 | view.backgroundColor = .clear 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Sources/PasscodeCore/Helper/ViewModifier.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewModifier.swift 3 | // PasscodeCore 4 | // 5 | // Created by David Walter on 12.08.23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | extension View { 11 | @inlinable func modifier(_ modifier: () -> T) -> ModifiedContent { 12 | self.modifier(modifier()) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Sources/PasscodeCore/Model/PasscodeMode.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PasscodeMode.swift 3 | // PasscodeCore 4 | // 5 | // Created by David Walter on 12.08.23. 6 | // 7 | 8 | import Foundation 9 | 10 | /// The behavior of the passcode view 11 | public enum PasscodeMode: Int, Hashable, Equatable, Codable, Identifiable, Sendable { 12 | /// Only the provided background is visible in the app switcher. 13 | case hideInAppSwitcher 14 | /// The passcode input view is always visble. 15 | case alwaysVisible 16 | /// The passcode view will hide itself when re-entering the app. 17 | case autohide 18 | /// The passcode view will never appear. 19 | case disabled 20 | 21 | /// The default mode. The default mode is ``hideInAppSwitcher`` 22 | public static var `default`: PasscodeMode { .hideInAppSwitcher } 23 | 24 | // MARK: - Sendable 25 | 26 | public var id: Int { rawValue } 27 | } 28 | -------------------------------------------------------------------------------- /Sources/PasscodeCore/PasscodeRootView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PasscodeRootView.swift 3 | // PasscodeCore 4 | // 5 | // Created by David Walter on 12.08.23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct PasscodeRootView: View where P: View, B: View { 11 | @Environment(\.passcode.dismiss) private var dismiss 12 | 13 | @Binding var isShowingPasscode: Bool 14 | 15 | @ViewBuilder var view: (_ dismiss: DismissPasscodeAction) -> P 16 | @ViewBuilder var background: () -> B 17 | 18 | var body: some View { 19 | ZStack { 20 | background() 21 | 22 | if isShowingPasscode { 23 | view(dismiss) 24 | } 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Sources/PasscodeCore/PasscodeViewModifier.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PasscodeViewModifier.swift 3 | // PasscodeCore 4 | // 5 | // Created by David Walter on 12.08.23. 6 | // 7 | 8 | import SwiftUI 9 | import WindowSceneReader 10 | 11 | /// Helper to read the current `UIWindowScene` without having to provide one 12 | @MainActor struct PasscodeViewModifierHelper: ViewModifier where I: View, B: View { 13 | var mode: PasscodeMode 14 | @ViewBuilder var input: (_ dismiss: DismissPasscodeAction) -> I 15 | @ViewBuilder var background: () -> B 16 | 17 | func body(content: Content) -> some View { 18 | content 19 | .background { 20 | WindowSceneReader { windowScene in 21 | Color.clear 22 | .modifier(PasscodeViewModifier(windowScene: windowScene, mode: mode, input: input, background: background)) 23 | } 24 | } 25 | } 26 | } 27 | 28 | /// Handle the automatic passcode presentation 29 | @MainActor struct PasscodeViewModifier: ViewModifier where I: View, B: View { 30 | var windowScene: UIWindowScene 31 | var mode: PasscodeMode 32 | @ViewBuilder var input: (_ dismiss: DismissPasscodeAction) -> I 33 | @ViewBuilder var background: () -> B 34 | 35 | @Environment(\.colorScheme) private var colorScheme 36 | @Environment(\.passcode.colorScheme) private var passcodeColorScheme 37 | @Environment(\.passcode.animatedDismissDuration) private var animatedDismissDuration 38 | @Environment(\.passcode.windowLevel) private var windowLevel 39 | 40 | @State private var isShowingPasscode = false 41 | @State private var window: UIWindow? 42 | 43 | init(windowScene: UIWindowScene, mode: PasscodeMode, input: @escaping (DismissPasscodeAction) -> I, background: @escaping () -> B) { 44 | self.windowScene = windowScene 45 | self.mode = mode 46 | self.input = input 47 | self.background = background 48 | } 49 | 50 | func body(content: Content) -> some View { 51 | content 52 | .onAppear { 53 | switch mode { 54 | case .alwaysVisible, .hideInAppSwitcher: 55 | showWindow() 56 | isShowingPasscode = true 57 | case .autohide, .disabled: 58 | isShowingPasscode = false 59 | } 60 | } 61 | .onReceive(NotificationCenter.default.publisher(for: UIApplication.willResignActiveNotification)) { _ in 62 | switch mode { 63 | case .alwaysVisible: 64 | isShowingPasscode = true 65 | case .hideInAppSwitcher: 66 | guard window == nil else { return } 67 | isShowingPasscode = false 68 | case .autohide, .disabled: 69 | isShowingPasscode = false 70 | } 71 | } 72 | .onReceive(NotificationCenter.default.publisher(for: UIApplication.didEnterBackgroundNotification)) { _ in 73 | switch mode { 74 | case .disabled: 75 | return 76 | default: 77 | showWindow() 78 | } 79 | } 80 | .onReceive(NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification)) { _ in 81 | switch mode { 82 | case .hideInAppSwitcher, .alwaysVisible: 83 | isShowingPasscode = true 84 | case .disabled: 85 | isShowingPasscode = false 86 | case .autohide: 87 | hideWindow(animated: false) 88 | } 89 | } 90 | } 91 | 92 | private func showWindow() { 93 | guard window == nil else { return } 94 | 95 | let rootView = PasscodeRootView( 96 | isShowingPasscode: $isShowingPasscode, 97 | view: input, 98 | background: background 99 | ) 100 | .onDismissPasscode { animated in 101 | hideWindow(animated: animated) 102 | } 103 | 104 | let window = UIWindow(windowScene: windowScene) 105 | window.overrideUserInterfaceStyle = userInterfaceStyle 106 | window.rootViewController = PasscodeBlurViewController(rootView: rootView) 107 | window.windowLevel = windowLevel 108 | window.makeKeyAndVisible() 109 | 110 | self.window = window 111 | } 112 | 113 | private func hideWindow(animated: Bool) { 114 | guard let window = window else { return } 115 | 116 | if animated { 117 | UIView.animate(withDuration: animatedDismissDuration) { 118 | window.alpha = 0 119 | } completion: { _ in 120 | window.resignKey() 121 | self.window = nil 122 | } 123 | } else { 124 | window.resignKey() 125 | self.window = nil 126 | } 127 | } 128 | 129 | private var userInterfaceStyle: UIUserInterfaceStyle { 130 | switch passcodeColorScheme ?? colorScheme { 131 | case .light: 132 | return .light 133 | case .dark: 134 | return .dark 135 | @unknown default: 136 | return .unspecified 137 | } 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /Sources/PasscodeKit/API.swift: -------------------------------------------------------------------------------- 1 | // 2 | // API.swift 3 | // PasscodeKit 4 | // 5 | // Created by David Walter on 12.08.23. 6 | // 7 | 8 | import SwiftUI 9 | @_exported import PasscodeCore 10 | import KeychainSwift 11 | 12 | public extension View { 13 | /// Add passcode protection to your app. 14 | /// 15 | /// - Parameters: 16 | /// - mode: The ``PasscodeMode`` to use when the passcode is setup. 17 | /// - fallbackMode: The fallback ``PasscodeMode`` to use when the passcode is not yet setup by the user. 18 | /// - Returns: The view with passcode protection enabled 19 | func passcode( 20 | _ text: Text? = nil, 21 | mode: PasscodeMode = .default, 22 | fallbackMode: PasscodeMode = .autohide 23 | ) -> some View { 24 | modifier(PasscodeViewModifier(title: text, mode: mode, fallbackMode: fallbackMode)) 25 | } 26 | 27 | /// Add passcode protection to your app. 28 | /// 29 | /// - Parameters: 30 | /// - mode: The ``PasscodeMode`` to use when the passcode is setup. 31 | /// - fallbackMode: The fallback ``PasscodeMode`` to use when the passcode is not yet setup by the user. 32 | /// - Returns: The view with passcode protection enabled 33 | func passcode( 34 | _ title: LocalizedStringKey, 35 | mode: PasscodeMode = .default, 36 | fallbackMode: PasscodeMode = .autohide 37 | ) -> some View { 38 | modifier(PasscodeViewModifier(title: Text(title), mode: mode, fallbackMode: fallbackMode)) 39 | } 40 | 41 | /// Add passcode protection to your app. 42 | /// 43 | /// - Parameters: 44 | /// - mode: The ``PasscodeMode`` to use when the passcode is setup. 45 | /// - fallbackMode: The fallback ``PasscodeMode`` to use when the passcode is not yet setup by the user. 46 | /// - Returns: The view with passcode protection enabled 47 | func passcode( 48 | _ title: S, mode: PasscodeMode = .default, 49 | fallbackMode: PasscodeMode = .autohide 50 | ) -> some View where S: StringProtocol { 51 | modifier(PasscodeViewModifier(title: Text(title), mode: mode, fallbackMode: fallbackMode)) 52 | } 53 | 54 | /// Add passcode protection to your app. 55 | /// 56 | /// - Parameters: 57 | /// - mode: The ``PasscodeMode`` to use when the passcode is setup. 58 | /// - fallbackMode: The fallback ``PasscodeMode`` to use when the passcode is not yet setup by the user. 59 | /// - Returns: The view with passcode protection enabled 60 | func passcode( 61 | _ text: Text? = nil, 62 | mode: PasscodeMode = .default, 63 | fallbackMode: PasscodeMode = .autohide, 64 | @ViewBuilder hint: () -> Hint 65 | ) -> some View where Hint: View { 66 | modifier(PasscodeViewModifier(title: text, mode: mode, fallbackMode: fallbackMode, hint: hint())) 67 | } 68 | 69 | /// Add passcode protection to your app. 70 | /// 71 | /// - Parameters: 72 | /// - mode: The ``PasscodeMode`` to use when the passcode is setup. 73 | /// - fallbackMode: The fallback ``PasscodeMode`` to use when the passcode is not yet setup by the user. 74 | /// - Returns: The view with passcode protection enabled 75 | func passcode( 76 | _ title: LocalizedStringKey, 77 | mode: PasscodeMode = .default, 78 | fallbackMode: PasscodeMode = .autohide, 79 | @ViewBuilder hint: () -> Hint 80 | ) -> some View where Hint: View { 81 | modifier(PasscodeViewModifier(title: Text(title), mode: mode, fallbackMode: fallbackMode, hint: hint())) 82 | } 83 | 84 | /// Add passcode protection to your app. 85 | /// 86 | /// - Parameters: 87 | /// - mode: The ``PasscodeMode`` to use when the passcode is setup. 88 | /// - fallbackMode: The fallback ``PasscodeMode`` to use when the passcode is not yet setup by the user. 89 | /// - Returns: The view with passcode protection enabled 90 | func passcode( 91 | _ title: S, mode: PasscodeMode = .default, 92 | fallbackMode: PasscodeMode = .autohide, 93 | @ViewBuilder hint: () -> Hint 94 | ) -> some View where S: StringProtocol, Hint: View { 95 | modifier(PasscodeViewModifier(title: Text(title), mode: mode, fallbackMode: fallbackMode, hint: hint())) 96 | } 97 | 98 | /// Manually check the passcode 99 | /// 100 | /// - Parameters: 101 | /// - isPresented: A binding to a Boolean value that determines whether to present the passcode view. 102 | /// - allowBiometrics: Allow to identify using biometrics. 103 | /// - onCompletion: The closure to execute when the passcode was checked. 104 | func checkPasscode( 105 | isPresented: Binding, 106 | allowBiometrics: Bool = true, 107 | onCompletion: @escaping (Bool) -> Void 108 | ) -> some View { 109 | modifier(PasscodeCheckViewModifier(isPresented: isPresented, allowBiometrics: allowBiometrics, onCompletion: onCompletion)) 110 | } 111 | 112 | /// Setup a passcode 113 | /// 114 | /// - Parameters: 115 | /// - isPresented: A binding to a Boolean value that determines whether to present the passcode view. 116 | /// - type: The ``PasscodeType`` to setup. 117 | func setupPasscode( 118 | isPresented: Binding, 119 | type: PasscodeType = .numeric(4), 120 | onCompletion: ((Bool) -> Void)? = nil 121 | ) -> some View { 122 | modifier(PasscodeSetupViewModifier(isPresented: isPresented, type: type, onCompletion: onCompletion)) 123 | } 124 | 125 | /// Setup a passcode 126 | /// 127 | /// - Parameters: 128 | /// - isPresented: A binding to a Boolean value that determines whether to present the passcode view. 129 | /// - type: The allowed ``PasscodeType``s to setup. 130 | func setupPasscode( 131 | isPresented: Binding, 132 | types: [PasscodeType] = [.numeric(4)], 133 | onCompletion: ((Bool) -> Void)? = nil 134 | ) -> some View { 135 | modifier(PasscodeSetupViewModifier(isPresented: isPresented, types: types, onCompletion: onCompletion)) 136 | } 137 | 138 | /// Change the currently setup passcode 139 | /// 140 | /// - Parameters: 141 | /// - isPresented: A binding to a Boolean value that determines whether to present the passcode view. 142 | /// - type: The ``PasscodeType`` to setup. 143 | func changePasscode( 144 | isPresented: Binding, 145 | type: PasscodeType = .numeric(4), 146 | onCompletion: ((Bool) -> Void)? = nil 147 | ) -> some View { 148 | modifier(PasscodeChangeViewModifier(isPresented: isPresented, type: type, onCompletion: onCompletion)) 149 | } 150 | 151 | /// Change the currently setup passcode 152 | /// 153 | /// - Parameters: 154 | /// - isPresented: A binding to a Boolean value that determines whether to present the passcode view. 155 | /// - types: The allowed ``PasscodeType``s to setup. 156 | func changePasscode( 157 | isPresented: Binding, 158 | types: [PasscodeType] = [.numeric(4)], 159 | onCompletion: ((Bool) -> Void)? = nil 160 | ) -> some View { 161 | modifier(PasscodeChangeViewModifier(isPresented: isPresented, types: types, onCompletion: onCompletion)) 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /Sources/PasscodeKit/Environment/CodeViewConfiguration.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CodeViewConfiguration.swift 3 | // PasscodeKit 4 | // 5 | // Created by David Walter on 02.09.23. 6 | // 7 | 8 | import PasscodeCore 9 | import SwiftUI 10 | 11 | /// Customize the code view 12 | public struct CodeViewConfiguration: Sendable { 13 | var foregroundColor: Color = .primary 14 | var emptyImage = Image(systemName: "circle") 15 | var filledImage = Image(systemName: "circle.fill") 16 | var spacing: CGFloat = 20 17 | 18 | /// Configures the foreground color. 19 | /// - Parameter color: The color to use. 20 | /// - Returns: The configuration with the foreground color configured. 21 | func foregroundColor(_ color: Color) -> Self { 22 | var configuration = self 23 | configuration.foregroundColor = color 24 | return configuration 25 | } 26 | 27 | /// Configures the empty image code point. 28 | /// - Parameter image: The name of the system symbol image to use as empty code point. 29 | /// - Returns: The configuration with the empty image configured. 30 | func empty(systemName: String) -> Self { 31 | var configuration = self 32 | configuration.emptyImage = Image(systemName: systemName) 33 | return configuration 34 | } 35 | 36 | /// Configures the empty image code point. 37 | /// - Parameter image: The name of the system symbol image to use as empty code point. 38 | /// - Returns: The configuration with the empty image configured. 39 | func empty(image: Image) -> Self { 40 | var configuration = self 41 | configuration.emptyImage = image 42 | return configuration 43 | } 44 | 45 | /// Configures the filled image code point. 46 | /// - Parameter image: The name of the system symbol image to use as filled code point. 47 | /// - Returns: The configuration with the filled image configured. 48 | func filled(systemName: String) -> Self { 49 | var configuration = self 50 | configuration.filledImage = Image(systemName: systemName) 51 | return configuration 52 | } 53 | 54 | /// Configures the filled image code point. 55 | /// - Parameter image: The image to use as filled code point. 56 | /// - Returns: The configuration with the filled image configured. 57 | func filled(image: Image) -> Self { 58 | var configuration = self 59 | configuration.filledImage = image 60 | return configuration 61 | } 62 | 63 | /// Configures the spacing between the code points. 64 | /// - Parameter value: The spacing to use. 65 | /// - Returns: The configuration with the spacing configured. 66 | func spacing(_ value: CGFloat) -> Self { 67 | var configuration = self 68 | configuration.spacing = value 69 | return configuration 70 | } 71 | } 72 | 73 | public extension PasscodeEnvironmentValues { 74 | var codeViewConfiguration: CodeViewConfiguration { 75 | get { self[CodeViewConfigurationKey.self] } 76 | set { self[CodeViewConfigurationKey.self] = newValue } 77 | } 78 | } 79 | 80 | private struct CodeViewConfigurationKey: EnvironmentKey { 81 | static var defaultValue: CodeViewConfiguration { 82 | CodeViewConfiguration() 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /Sources/PasscodeKit/Environment/KeypadViewConfiguration.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KeypadViewConfiguration.swift 3 | // PasscodeKit 4 | // 5 | // Created by David Walter on 02.09.23. 6 | // 7 | 8 | import SwiftUI 9 | import PasscodeCore 10 | 11 | /// Customize the keypad view 12 | public struct KeypadViewConfiguration: Sendable { 13 | var foregroundColor: Color = .primary 14 | var hSpacing: CGFloat = 25 15 | var vSpacing: CGFloat = 15 16 | var deleteImage = Image(systemName: "delete.left") 17 | 18 | /// Configures the foreground color. 19 | /// - Parameter color: The color to use. 20 | /// - Returns: The configuration with the foreground color configured. 21 | func foregroundColor(_ color: Color) -> Self { 22 | var configuration = self 23 | configuration.foregroundColor = color 24 | return configuration 25 | } 26 | 27 | /// Sets the spacing between the button. 28 | /// - Parameters: 29 | /// - vertical: The vertical spacing. 30 | /// - horizontal: The horizontal spacing. 31 | /// - Returns: The configuration with the spacing configured. 32 | func spacing(vertical: CGFloat? = nil, horizontal: CGFloat? = nil) -> Self { 33 | var configuration = self 34 | if let vertical = vertical { 35 | configuration.vSpacing = vertical 36 | } 37 | if let horizontal = horizontal { 38 | configuration.hSpacing = horizontal 39 | } 40 | return configuration 41 | } 42 | 43 | /// Configures the image for the delete button. 44 | /// - Parameter image: The name of the system symbol image to use. 45 | /// - Returns: The configuration with the delete image configured. 46 | func delete(systemName: String) -> Self { 47 | var configuration = self 48 | configuration.deleteImage = Image(systemName: systemName) 49 | return configuration 50 | } 51 | 52 | /// Configures the image for the delete button. 53 | /// - Parameter image: The name of the system symbol image to use. 54 | /// - Returns: The configuration with the delete image configured. 55 | func delete(image: Image) -> Self { 56 | var configuration = self 57 | configuration.deleteImage = image 58 | return configuration 59 | } 60 | } 61 | 62 | public extension PasscodeEnvironmentValues { 63 | var keypadViewConfiguration: KeypadViewConfiguration { 64 | get { self[KeypadViewConfigurationKey.self] } 65 | set { self[KeypadViewConfigurationKey.self] = newValue } 66 | } 67 | } 68 | 69 | private struct KeypadViewConfigurationKey: EnvironmentKey { 70 | static var defaultValue: KeypadViewConfiguration { 71 | KeypadViewConfiguration() 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /Sources/PasscodeKit/Environment/PasscodeConfiguration.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PasscodeConfiguration.swift 3 | // PasscodeKit 4 | // 5 | // Created by David Walter on 02.09.23. 6 | // 7 | 8 | import SwiftUI 9 | import PasscodeCore 10 | 11 | public extension PasscodeEnvironmentValues { 12 | var tintColor: Color? { 13 | get { self[PasscodeTintColorKey.self] } 14 | set { self[PasscodeTintColorKey.self] = newValue } 15 | } 16 | 17 | /// The background material to use when presenting the passcode view. 18 | var backgroundMaterial: Material? { 19 | get { self[PasscodeBackgroundMaterialKey.self] } 20 | set { self[PasscodeBackgroundMaterialKey.self] = newValue } 21 | } 22 | } 23 | 24 | private struct PasscodeTintColorKey: EnvironmentKey { 25 | static var defaultValue: Color? { 26 | .accentColor 27 | } 28 | } 29 | 30 | private struct PasscodeBackgroundMaterialKey: EnvironmentKey { 31 | static var defaultValue: Material? { 32 | .regular 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Sources/PasscodeKit/Environment/PasscodeSetupConfiguration.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PasscodeSetupConfiguration.swift 3 | // PasscodeKit 4 | // 5 | // Created by David Walter on 22.10.23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | public extension PasscodeEnvironmentValues { 11 | /// The transition when the setup/change wizard moves forward 12 | var setupForwardTransition: AnyTransition { 13 | get { self[PasscodeSetupForwardTransitionKey.self] } 14 | set { self[PasscodeSetupForwardTransitionKey.self] = newValue } 15 | } 16 | 17 | /// The transition when the setup/change wizard moves backward 18 | var setupBackwardTransition: AnyTransition { 19 | get { self[PasscodeSetupBackwardTransitionKey.self] } 20 | set { self[PasscodeSetupBackwardTransitionKey.self] = newValue } 21 | } 22 | } 23 | 24 | private struct PasscodeSetupForwardTransitionKey: EnvironmentKey { 25 | static var defaultValue: AnyTransition { 26 | .move(edge: .leading) 27 | } 28 | } 29 | 30 | private struct PasscodeSetupBackwardTransitionKey: EnvironmentKey { 31 | static var defaultValue: AnyTransition { 32 | .move(edge: .trailing) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Sources/PasscodeKit/Extension/BindingExtensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BindingExtensions.swift 3 | // PasscodeKit 4 | // 5 | // Created by David Walter on 12.08.23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | extension Binding where Value == String { 11 | @MainActor func max(_ limit: Int) -> Self { 12 | if wrappedValue.count > limit { 13 | Task { @MainActor in 14 | self.wrappedValue = String(self.wrappedValue.prefix(limit)) 15 | } 16 | } 17 | 18 | return self 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Sources/PasscodeKit/Extension/StringExtensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StringExtensions.swift 3 | // PasscodeKit 4 | // 5 | // Created by David Walter on 13.08.23. 6 | // 7 | 8 | import Foundation 9 | 10 | extension String { 11 | func localized(comment: String = "") -> String { 12 | let bundle = Bundle.main.path(forResource: "Passcode", ofType: "strings") != nil ? Bundle.main : Bundle.module 13 | return NSLocalizedString(self, tableName: "Passcode", bundle: bundle, comment: comment) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Sources/PasscodeKit/Extension/UIViewExtensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIViewExtensions.swift 3 | // PasscodeKit 4 | // 5 | // Created by David Walter on 14.08.23. 6 | // 7 | 8 | import UIKit 9 | 10 | extension UIView { 11 | func findViewController() -> UIViewController? { 12 | if let nextResponder = next as? UIViewController { 13 | return nextResponder 14 | } else if let nextResponder = next as? UIView { 15 | return nextResponder.findViewController() 16 | } else { 17 | return nil 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Sources/PasscodeKit/Extension/ViewExtensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewExtensions.swift 3 | // PasscodeKit 4 | // 5 | // Created by David Walter on 13.08.23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | public extension View { 11 | func transparentBackground(_ isTransparent: Bool = true) -> some View { 12 | background(TransparentBackground(isTransparent: isTransparent)) 13 | } 14 | } 15 | 16 | private struct TransparentBackground: UIViewRepresentable { 17 | var isTransparent: Bool 18 | 19 | func makeUIView(context: Context) -> UIView { 20 | let view = UIView(frame: .zero) 21 | view.isUserInteractionEnabled = false 22 | view.alpha = 0 23 | 24 | DispatchQueue.main.async { 25 | guard let viewController = view.findViewController() else { return } 26 | viewController.view.backgroundColor = isTransparent ? .clear : .systemBackground 27 | } 28 | 29 | return view 30 | } 31 | 32 | func updateUIView(_ uiView: UIView, context: Context) { 33 | guard let viewController = uiView.findViewController() else { return } 34 | viewController.view.backgroundColor = isTransparent ? .clear : .systemBackground 35 | } 36 | } 37 | 38 | extension View { 39 | func readSize(onChange: @escaping (CGSize) -> Void) -> some View { 40 | background { 41 | GeometryReader { geometryProxy in 42 | Color.clear 43 | .preference(key: SizePreferenceKey.self, value: geometryProxy.size) 44 | } 45 | } 46 | .onPreferenceChange(SizePreferenceKey.self, perform: onChange) 47 | } 48 | 49 | func readSize(into size: Binding) -> some View { 50 | readSize { 51 | size.wrappedValue = $0 52 | } 53 | } 54 | } 55 | 56 | private struct SizePreferenceKey: PreferenceKey { 57 | static var defaultValue: CGSize { 58 | .zero 59 | } 60 | 61 | static func reduce(value: inout CGSize, nextValue: () -> CGSize) { 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Sources/PasscodeKit/Model/Passcode.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Passcode.swift 3 | // PasscodeKit 4 | // 5 | // Created by David Walter on 13.08.23. 6 | // 7 | 8 | import Foundation 9 | import LocalAuthentication 10 | 11 | /// A type that represents a passcode 12 | public struct Passcode: Equatable, Hashable, Codable, Identifiable, Sendable { 13 | /// The code of the passcode 14 | public var code: String 15 | /// The type of the passcode 16 | public var type: PasscodeType 17 | 18 | private var allowBiometrics: Bool? 19 | 20 | /// Create a new ``Passcode`` 21 | /// 22 | /// - Parameters: 23 | /// - code: The code to use. 24 | /// - type: The type of the passcode. 25 | /// - allowBiometrics: Whether the passcode allows biometrics. 26 | public init(_ code: String, type: PasscodeType, allowBiometrics: Bool = true) { 27 | self.code = code 28 | self.type = type 29 | self.allowBiometrics = allowBiometrics 30 | } 31 | 32 | /// Checks if the code is empty. 33 | public var isEmpty: Bool { 34 | code.isEmpty 35 | } 36 | 37 | /// Checks if the passcode is able to autocomplete 38 | public var canAutocomplete: Bool { 39 | switch type { 40 | case .numeric: 41 | return true 42 | case .customNumeric, .alphanumeric: 43 | return false 44 | } 45 | } 46 | 47 | /// Checks if the passcode is complete 48 | /// 49 | /// Only works if ``canAutocomplete`` is also `true` 50 | /// 51 | /// - Returns: Whether the code is complete 52 | public var isComplete: Bool? { 53 | switch type { 54 | case let .numeric(count): 55 | return code.count == count 56 | case .customNumeric, .alphanumeric: 57 | return nil 58 | } 59 | } 60 | 61 | /// Checks if biometrics are enabled 62 | public var isBiometricsEnabled: Bool { 63 | guard allowBiometrics == true else { return false } 64 | let context = LAContext() 65 | var error: NSError? 66 | return context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) 67 | } 68 | 69 | // MARK: - Identifiable 70 | 71 | public var id: String { 72 | "\(type.id):\(code.hashValue)" 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /Sources/PasscodeKit/Model/PasscodeType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PasscodeType.swift 3 | // PasscodeKit 4 | // 5 | // Created by David Walter on 13.08.23. 6 | // 7 | 8 | import Foundation 9 | 10 | /// Type of the passcode 11 | public enum PasscodeType: Hashable, Equatable, Codable, Identifiable, Sendable { 12 | /// A fixed width numeric code 13 | case numeric(_ digits: Int) 14 | /// A custom width numeric code 15 | case customNumeric 16 | /// A custom width alphanumeric code 17 | case alphanumeric 18 | 19 | // MARK: - Helper 20 | 21 | /// Checks if the passcode is numeric 22 | public var isNumeric: Bool { 23 | switch self { 24 | case .numeric, .customNumeric: 25 | return true 26 | default: 27 | return false 28 | } 29 | } 30 | 31 | /// Checks if the passcode is alphanumeric 32 | public var isAlphaNumeric: Bool { 33 | switch self { 34 | case .alphanumeric: 35 | return true 36 | default: 37 | return false 38 | } 39 | } 40 | 41 | /// Checks if the passcode is able to autocomplete 42 | public var canAutoComplete: Bool { 43 | switch self { 44 | case .numeric: 45 | return true 46 | default: 47 | return false 48 | } 49 | } 50 | 51 | /// Returns the max. input length 52 | public var maxInputLength: Int { 53 | switch self { 54 | case let .numeric(count): 55 | return count 56 | case .customNumeric: 57 | return .max 58 | case .alphanumeric: 59 | return .max 60 | } 61 | } 62 | 63 | public var localized: String { 64 | switch self { 65 | case let .numeric(count): 66 | return String(format: "passcode.type.numeric".localized(), count.description) 67 | case .customNumeric: 68 | return "passcode.type.numeric.custom".localized() 69 | case .alphanumeric: 70 | return "passcode.type.alphanumeric.custom".localized() 71 | } 72 | } 73 | 74 | // MARK: - Identifiable 75 | 76 | public var id: String { 77 | switch self { 78 | case let .numeric(count): 79 | return "numeric.\(count)" 80 | case .customNumeric: 81 | return "numeric.custom" 82 | case .alphanumeric: 83 | return "alphanumeric.custom" 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /Sources/PasscodeKit/PasscodeChangeViewModifier.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PasscodeChangeViewModifier.swift 3 | // PasscodeKit 4 | // 5 | // Created by David Walter on 19.10.23. 6 | // 7 | 8 | import SwiftUI 9 | import PasscodeCore 10 | 11 | struct PasscodeChangeViewModifier: ViewModifier { 12 | @Environment(\.passcode.manager) private var passcodeManager 13 | 14 | @Binding var isPresented: Bool 15 | var types: [PasscodeType] 16 | var onCompletion: ((Bool) -> Void)? 17 | 18 | init(isPresented: Binding, type: PasscodeType, onCompletion: ((Bool) -> Void)? = nil) { 19 | self._isPresented = isPresented 20 | self.types = [type] 21 | self.onCompletion = onCompletion 22 | } 23 | 24 | init(isPresented: Binding, types: [PasscodeType], onCompletion: ((Bool) -> Void)? = nil) { 25 | self._isPresented = isPresented 26 | self.types = types 27 | self.onCompletion = onCompletion 28 | } 29 | 30 | func body(content: Content) -> some View { 31 | content 32 | .fullScreenCover(isPresented: $isPresented) { 33 | NavigationView { 34 | PasscodeChangeView(types: types) { code in 35 | defer { self.isPresented = false } 36 | 37 | let result = passcodeManager.setPasscode(code) 38 | onCompletion?(result) 39 | } 40 | } 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Sources/PasscodeKit/PasscodeCheckViewModifier.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PasscodeCheckViewModifier.swift 3 | // PasscodeKit 4 | // 5 | // Created by David Walter on 13.08.23. 6 | // 7 | 8 | import SwiftUI 9 | import PasscodeCore 10 | 11 | struct PasscodeCheckViewModifier: ViewModifier { 12 | @Environment(\.passcode.manager) private var passcodeManager 13 | @Environment(\.passcode.backgroundMaterial) private var backgroundMaterial 14 | 15 | @Binding var isPresented: Bool 16 | var allowBiometrics: Bool 17 | var onCompletion: (Bool) -> Void 18 | @State private var item: Passcode? 19 | 20 | func body(content: Content) -> some View { 21 | content 22 | .fullScreenCover(item: $item) { passcode in 23 | NavigationView { 24 | ZStack { 25 | if let material = backgroundMaterial { 26 | Color.clear 27 | .background(material, ignoresSafeAreaEdges: .all) 28 | } 29 | 30 | PasscodeInputView(passcode: passcode, allowBiometrics: allowBiometrics, canCancel: true) { success in 31 | onCompletion(success) 32 | isPresented = false 33 | } 34 | .transparentBackground(hasBackground) 35 | } 36 | } 37 | .transparentBackground(hasBackground) 38 | } 39 | .onAppear { 40 | item = isPresented ? passcodeManager.passcode : nil 41 | } 42 | .onChange(of: isPresented) { isPresented in 43 | item = isPresented ? passcodeManager.passcode : nil 44 | } 45 | } 46 | 47 | var hasBackground: Bool { 48 | backgroundMaterial != nil 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Sources/PasscodeKit/PasscodeManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PasscodeManager.swift 3 | // PasscodeKit 4 | // 5 | // Created by David Walter on 01.09.23. 6 | // 7 | 8 | import SwiftUI 9 | import PasscodeCore 10 | import KeychainSwift 11 | 12 | /// Manage the passcode of your app 13 | public final class PasscodeManager { 14 | /// The key of the stored passcode item in the keychain. 15 | public private(set) var key: String 16 | /// The keychain implementation where the passcode item is stored in. 17 | public private(set) var keychain: KeychainSwift 18 | /// These options are used to determine when the passcode item should be readable. 19 | private var keychainAccess: KeychainSwiftAccessOptions? 20 | 21 | public init( 22 | key: String = "PasscodeKit.Passcode", 23 | keychain: KeychainSwift = KeychainSwift(), 24 | access: KeychainSwiftAccessOptions? = nil 25 | ) { 26 | self.key = key 27 | self.keychain = keychain 28 | } 29 | 30 | /// The passcode or `nil` if none is set. 31 | public var passcode: Passcode? { 32 | do { 33 | guard let data = keychain.getData(key) else { return nil } 34 | return try JSONDecoder().decode(Passcode.self, from: data) 35 | } catch { 36 | return nil 37 | } 38 | } 39 | 40 | /// Checks if the passcode is setup. 41 | public var isSetup: Bool { 42 | keychain.get(key) != nil 43 | } 44 | 45 | /// Sets a new passcode 46 | /// 47 | /// - Parameters: 48 | /// - code: The code of the passcode. 49 | /// - type: The type of the code. 50 | /// - allowBiometrics: Whether to allow biometrics or not. 51 | /// - Returns: True if the passcode was successfully stored. 52 | public func setPasscode(code: String, type: PasscodeType, allowBiometrics: Bool) -> Bool { 53 | let passcode = Passcode(code, type: type, allowBiometrics: allowBiometrics) 54 | return setPasscode(passcode) 55 | } 56 | 57 | /// Sets a new passcode 58 | /// 59 | /// - Parameters: 60 | /// - passcode: The passcode to set. 61 | /// - Returns: True if the passcode was successfully stored. 62 | public func setPasscode(_ passcode: Passcode) -> Bool { 63 | do { 64 | let data = try JSONEncoder().encode(passcode) 65 | keychain.set(data, forKey: key, withAccess: keychainAccess) 66 | return true 67 | } catch { 68 | return false 69 | } 70 | } 71 | 72 | /// Enabled (or disables) biometrics on the current passcode. 73 | /// 74 | /// - Parameters: 75 | /// - enabled: Whether to allow biometrics or not. 76 | /// - Returns: True if the passcode was successfully stored. 77 | @discardableResult public func setBiometrics(_ enabled: Bool) -> Bool { 78 | guard let passcode = passcode else { return false } 79 | return setPasscode(code: passcode.code, type: passcode.type, allowBiometrics: enabled) 80 | } 81 | 82 | /// Deletes the current passcode. 83 | /// 84 | /// - Returns: True if the passcode was successfully deleted. 85 | @discardableResult public func delete() -> Bool{ 86 | keychain.delete(key) 87 | } 88 | } 89 | 90 | public extension PasscodeEnvironmentValues { 91 | /// The ``PasscodeManager`` to manage the passcode. 92 | var manager: PasscodeManager { 93 | get { self[PasscodeManagerKey.self] } 94 | set { self[PasscodeManagerKey.self] = newValue } 95 | } 96 | } 97 | 98 | struct PasscodeManagerKey: EnvironmentKey { 99 | static var defaultValue: PasscodeManager { 100 | PasscodeManager() 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /Sources/PasscodeKit/PasscodeSetupViewModifier.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PasscodeSetupViewModifier.swift 3 | // PasscodeKit 4 | // 5 | // Created by David Walter on 13.08.23. 6 | // 7 | 8 | import SwiftUI 9 | import PasscodeCore 10 | 11 | struct PasscodeSetupViewModifier: ViewModifier { 12 | @Environment(\.passcode.manager) private var passcodeManager 13 | 14 | @Binding var isPresented: Bool 15 | var types: [PasscodeType] 16 | var onCompletion: ((Bool) -> Void)? 17 | 18 | init(isPresented: Binding, type: PasscodeType, onCompletion: ((Bool) -> Void)? = nil) { 19 | self._isPresented = isPresented 20 | self.types = [type] 21 | self.onCompletion = onCompletion 22 | } 23 | 24 | init(isPresented: Binding, types: [PasscodeType], onCompletion: ((Bool) -> Void)? = nil) { 25 | self._isPresented = isPresented 26 | self.types = types 27 | self.onCompletion = onCompletion 28 | } 29 | 30 | func body(content: Content) -> some View { 31 | content 32 | .fullScreenCover(isPresented: $isPresented) { 33 | NavigationView { 34 | PasscodeSetupView(types: types) { code in 35 | defer { self.isPresented = false } 36 | 37 | let result = passcodeManager.setPasscode(code) 38 | onCompletion?(result) 39 | } 40 | } 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Sources/PasscodeKit/PasscodeViewModifier.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PasscodeViewModifier.swift 3 | // PasscodeKit 4 | // 5 | // Created by David Walter on 13.08.23. 6 | // 7 | 8 | import SwiftUI 9 | import PasscodeCore 10 | 11 | struct PasscodeViewModifier: ViewModifier where Hint: View { 12 | @Environment(\.passcode.manager) private var passcodeManager 13 | @Environment(\.passcode.backgroundMaterial) private var backgroundMaterial 14 | 15 | var title: Text? 16 | var mode: PasscodeMode 17 | var fallbackMode: PasscodeMode 18 | var hint: Hint 19 | 20 | func body(content: Content) -> some View { 21 | content 22 | .passcode(mode: computedMode, background: backgroundMaterial) { dismiss in 23 | if let passcode = passcodeManager.passcode { 24 | VStack { 25 | if let title = title { 26 | title 27 | .font(.headline) 28 | .padding(.top) 29 | } 30 | 31 | PasscodeInputView(passcode: passcode, canCancel: false) { _ in 32 | dismiss(animated: true) 33 | } hint: { 34 | hint 35 | } 36 | } 37 | } 38 | } 39 | } 40 | 41 | private var computedMode: PasscodeMode { 42 | guard passcodeManager.isSetup else { return fallbackMode } 43 | return mode 44 | } 45 | } 46 | 47 | extension PasscodeViewModifier where Hint == EmptyView { 48 | init(title: Text? = nil, mode: PasscodeMode, fallbackMode: PasscodeMode) { 49 | self.title = title 50 | self.mode = mode 51 | self.fallbackMode = fallbackMode 52 | self.hint = EmptyView() 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Sources/PasscodeKit/Resources/Passcode.strings: -------------------------------------------------------------------------------- 1 | "passcode.enter.title" = "Enter Passcode"; 2 | "passcode.create.title" = "Create Passcode"; 3 | "passcode.change.title" = "Change Passcode"; 4 | 5 | "passcode.enter.current.hint" = "Enter current passcode"; 6 | "passcode.enter.hint" = "Enter a new passcode"; 7 | "passcode.enter.again.hint" = "Please re-enter your passcode"; 8 | "passcode.enter.failed.hint" = "Passcodes did not match. Try again"; 9 | 10 | "passcode.biometrics.hint" = "Face ID is needed to unlock the app."; 11 | "passcode.biometrics.setup.button" = "Continue"; 12 | "passcode.biometrics.setup.message" = "Do you want to enable %@ to unlock the app?"; 13 | "passcode.biometrics.setup.cancel" = "Setup later"; 14 | 15 | "ok" = "OK"; 16 | "cancel" = "Cancel"; 17 | 18 | "passcode.type.options" = "Passcode Options"; 19 | "passcode.type.numeric" = "%@-Digit Numeric Code"; 20 | "passcode.type.numeric.custom" = "Custom Numeric Code"; 21 | "passcode.type.alphanumeric.custom" = "Custom Alphanumeric Code"; 22 | -------------------------------------------------------------------------------- /Sources/PasscodeKit/Views/CodeView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CodeView.swift 3 | // PasscodeKit 4 | // 5 | // Created by David Walter on 12.08.23. 6 | // 7 | 8 | import SwiftUI 9 | import PasscodeCore 10 | 11 | struct CodeView: View { 12 | @Environment(\.passcode.codeViewConfiguration) private var configuration 13 | @Environment(\.dynamicTypeSize) private var dynamicTypeSize 14 | 15 | var passcode: Passcode 16 | 17 | @State private var parentSize: CGSize = .zero 18 | @State private var inputSize: CGSize = .zero 19 | 20 | init(passcode: Passcode) { 21 | self.passcode = passcode 22 | } 23 | 24 | init(text: String, type: PasscodeType) { 25 | self.passcode = Passcode(text, type: type) 26 | } 27 | 28 | var body: some View { 29 | ZStack { 30 | // Size helper 31 | Color.clear 32 | .frame(maxWidth: .infinity, maxHeight: 0) 33 | .readSize(into: $parentSize) 34 | 35 | VStack { 36 | switch passcode.type { 37 | case let .numeric(count): 38 | if count <= 6 { 39 | bulletView(for: count) 40 | } else { 41 | fieldView 42 | } 43 | default: 44 | fieldView 45 | } 46 | } 47 | } 48 | .dynamicTypeSize(computedDynamicTypeSize) 49 | } 50 | 51 | func bulletView(for count: Int) -> some View { 52 | HStack(spacing: configuration.spacing) { 53 | ForEach(0 ..< count, id: \.self) { index in 54 | if index < passcode.code.count { 55 | configuration.filledImage 56 | } else { 57 | configuration.emptyImage 58 | } 59 | } 60 | } 61 | .foregroundColor(configuration.foregroundColor) 62 | } 63 | 64 | var fieldView: some View { 65 | HStack { 66 | if passcode.isEmpty { 67 | configuration.emptyImage.hidden() 68 | } else { 69 | ForEach(0 ..< passcode.code.count, id: \.self) { index in 70 | if index < passcode.code.count { 71 | configuration.filledImage 72 | } else { 73 | configuration.emptyImage 74 | } 75 | } 76 | } 77 | } 78 | .foregroundColor(configuration.foregroundColor) 79 | .readSize(into: $inputSize) 80 | .padding(.horizontal, 8) 81 | .frame(width: parentSize.width, alignment: alignment) 82 | .padding(.vertical, 16) 83 | .clipped(antialiased: true) 84 | .overlay { 85 | RoundedRectangle(cornerRadius: 10) 86 | .stroke(configuration.foregroundColor, lineWidth: 2) 87 | .frame(width: parentSize.width - 4) 88 | } 89 | } 90 | 91 | // The accessibility sizes easily clip the code view out of 92 | // the view and provide no real benefit by having a larger 93 | // size, as there is no real content to read. 94 | // 95 | // Therefore we limit the `dynamicTypeSize` to `.xxxLarge` 96 | private var computedDynamicTypeSize: DynamicTypeSize { 97 | if dynamicTypeSize.isAccessibilitySize { 98 | return .xxxLarge 99 | } else { 100 | return dynamicTypeSize 101 | } 102 | } 103 | 104 | private var isContentBiggerThanParent: Bool { 105 | inputSize.width > parentSize.width 106 | } 107 | 108 | private var alignment: Alignment { 109 | if isContentBiggerThanParent { 110 | return .trailing 111 | } else { 112 | return .center 113 | } 114 | } 115 | } 116 | 117 | struct CodeView_Previews: PreviewProvider { 118 | static var previews: some View { 119 | ScrollView { 120 | VStack(spacing: 30) { 121 | Section { 122 | CodeView(passcode: Passcode("", type: .numeric(4))) 123 | CodeView(passcode: Passcode("1", type: .numeric(4))) 124 | CodeView(passcode: Passcode("12", type: .numeric(4))) 125 | CodeView(passcode: Passcode("123", type: .numeric(4))) 126 | CodeView(passcode: Passcode("1234", type: .numeric(4))) 127 | } header: { 128 | Text("Fixed Numeric") 129 | } 130 | Section { 131 | CodeView(text: "123", type: .numeric(123)) 132 | CodeView(text: "123456789", type: .customNumeric) 133 | } header: { 134 | Text("Custom Numeric") 135 | } 136 | Section { 137 | CodeView(text: "", type: .alphanumeric) 138 | CodeView(text: "asdf", type: .alphanumeric) 139 | CodeView(text: "abcdefghijklmnopqrstuvwxyz", type: .alphanumeric) 140 | } header: { 141 | Text("Alphanumeric") 142 | } 143 | } 144 | .padding(.horizontal, 40) 145 | } 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /Sources/PasscodeKit/Views/Helper/PasscodeScrollView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PasscodeScrollView.swift 3 | // PasscodeKit 4 | // 5 | // Created by David Walter on 12.08.23. 6 | // 7 | 8 | import SwiftUI 9 | import SwiftUIIntrospect 10 | 11 | /// Wrapper for `ScrollView` 12 | struct PasscodeScrollView: View where Content: View { 13 | var content: Content 14 | 15 | init(@ViewBuilder content: () -> Content) { 16 | self.content = content() 17 | } 18 | 19 | var body: some View { 20 | GeometryReader { proxy in 21 | if #unavailable(iOS 16.4) { 22 | scrollView(proxy: proxy) 23 | .introspect(.scrollView, on: .iOS(.v15, .v16), scope: .ancestor) { scrollView in 24 | scrollView.alwaysBounceVertical = false 25 | scrollView.alwaysBounceHorizontal = false 26 | } 27 | } else { 28 | scrollView(proxy: proxy) 29 | .scrollBounceBehavior(.basedOnSize) 30 | } 31 | } 32 | } 33 | 34 | func scrollView(proxy: GeometryProxy) -> some View { 35 | ScrollView(.vertical) { 36 | VStack { 37 | content 38 | } 39 | .frame(maxWidth: .infinity, minHeight: proxy.size.height) 40 | } 41 | } 42 | } 43 | 44 | struct PasscodeScrollView_Previews: PreviewProvider { 45 | static var previews: some View { 46 | PasscodeScrollView { 47 | Text("Test") 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Sources/PasscodeKit/Views/Helper/Shake.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Shake.swift 3 | // PasscodeKit 4 | // 5 | // Created by David Walter on 12.08.23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct Shake: GeometryEffect { 11 | var amount: CGFloat 12 | var shakesPerUnit: CGFloat 13 | var animatableData: CGFloat 14 | 15 | init(amount: Int = 10, shakesPerUnit: Int = 3, animatableData: Int) { 16 | self.amount = CGFloat(amount) 17 | self.shakesPerUnit = CGFloat(shakesPerUnit) 18 | self.animatableData = CGFloat(animatableData) 19 | } 20 | 21 | func effectValue(size: CGSize) -> ProjectionTransform { 22 | let value = amount * sin(animatableData * .pi * shakesPerUnit) 23 | return ProjectionTransform(CGAffineTransform(translationX: value, y: 0)) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Sources/PasscodeKit/Views/Helper/TaskButton.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TaskButton.swift 3 | // PasscodeKit 4 | // 5 | // Created by David Walter on 13.10.23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct TaskButton