├── .gitignore ├── .swift-version ├── .travis.yml ├── GPassword.podspec ├── GPassword ├── GPassword.xcodeproj │ └── project.pbxproj └── GPassword │ ├── AppDelegate.swift │ ├── Assets.xcassets │ └── AppIcon.appiconset │ │ └── Contents.json │ ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard │ ├── Info.plist │ ├── PasswordViewController.swift │ ├── ViewController.swift │ ├── en.lproj │ ├── GPassword.strings │ ├── LaunchScreen.strings │ ├── Localizable.strings │ └── Main.strings │ └── zh-Hans.lproj │ ├── GPassword.strings │ ├── LaunchScreen.strings │ ├── Localizable.strings │ └── Main.strings ├── LICENSE ├── README.md ├── Resources ├── GPassword-logo.png ├── first.gif ├── second.gif └── third.gif └── Source ├── Core ├── Box.swift ├── EventDelegate.swift ├── LockManager.swift ├── LockOptions.swift └── Point.swift ├── Extension └── Extensions.swift ├── GPassword.swift └── Store ├── KeychainSwift.swift └── Storage.swift /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata/ 19 | 20 | ## Other 21 | *.moved-aside 22 | *.xccheckout 23 | *.xcscmblueprint 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | *.ipa 28 | *.dSYM.zip 29 | *.dSYM 30 | 31 | ## Playgrounds 32 | timeline.xctimeline 33 | playground.xcworkspace 34 | 35 | # Swift Package Manager 36 | # 37 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 38 | # Packages/ 39 | # Package.pins 40 | .build/ 41 | 42 | # CocoaPods 43 | # 44 | # We recommend against adding the Pods directory to your .gitignore. However 45 | # you should judge for yourself, the pros and cons are mentioned at: 46 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 47 | # 48 | # Pods/ 49 | 50 | # Carthage 51 | # 52 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 53 | # Carthage/Checkouts 54 | 55 | Carthage/Build 56 | 57 | # fastlane 58 | # 59 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 60 | # screenshots whenever they are needed. 61 | # For more information about the recommended setup visit: 62 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 63 | 64 | fastlane/report.xml 65 | fastlane/Preview.html 66 | fastlane/screenshots 67 | fastlane/test_output 68 | -------------------------------------------------------------------------------- /.swift-version: -------------------------------------------------------------------------------- 1 | 4.0 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | os: osx 2 | language: swift 3 | osx_image: xcode9.2 4 | branches: 5 | only: 6 | - master 7 | 8 | script: 9 | - set -o pipefail 10 | - xcodebuild -project GPassword/GPassword.xcodeproj -scheme GPassword -sdk iphonesimulator11.2 -destination 'platform=iOS Simulator,name=iPhone 7,OS=11.2' ONLY_ACTIVE_ARCH=NO | xcpretty -------------------------------------------------------------------------------- /GPassword.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = "GPassword" 3 | s.version = "1.0.9" 4 | s.summary = "Simple gesture password in swift." 5 | s.description = <<-DESC 6 | This framework help you build your gesture password easily! 7 | DESC 8 | s.homepage = "https://github.com/hackjie/GPassword" 9 | s.license = "MIT" 10 | s.author = { "leoli" => "codelijie@gmail.com" } 11 | s.source = { :git => "https://github.com/hackjie/GPassword.git", :tag => "#{s.version}" } 12 | 13 | # ――― Platform Specifics ――――――――――――――――――――――――――――――――――――――――――――――――――――――― # 14 | # 15 | # If this Pod runs only on iOS or OS X, then specify the platform and 16 | # the deployment target. You can optionally include the target after the platform. 17 | # 18 | 19 | # When using multiple platforms 20 | s.ios.deployment_target = "8.0" 21 | # s.osx.deployment_target = "10.7" 22 | # s.watchos.deployment_target = "2.0" 23 | # s.tvos.deployment_target = "9.0" 24 | 25 | s.source_files = "Source/**/*.swift" 26 | # s.public_header_files = "Classes/**/*.h" 27 | 28 | end 29 | -------------------------------------------------------------------------------- /GPassword/GPassword.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 48; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | B612008F20A594AD003906D1 /* KeychainSwift.swift in Sources */ = {isa = PBXBuildFile; fileRef = B612008E20A594AD003906D1 /* KeychainSwift.swift */; }; 11 | B612009120A59C0E003906D1 /* LockManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = B612009020A59C0E003906D1 /* LockManager.swift */; }; 12 | B612009820A93CB4003906D1 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = B612009A20A93CB4003906D1 /* Localizable.strings */; }; 13 | B615A3AD20AED54700BF1FE6 /* Storage.swift in Sources */ = {isa = PBXBuildFile; fileRef = B615A3AC20AED54700BF1FE6 /* Storage.swift */; }; 14 | B64A2C1C20A0449B00B17B43 /* Point.swift in Sources */ = {isa = PBXBuildFile; fileRef = B64A2C1B20A0449B00B17B43 /* Point.swift */; }; 15 | B65C003620A4262C008388A1 /* EventDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B65C003520A4262C008388A1 /* EventDelegate.swift */; }; 16 | B65C004C20A43BF9008388A1 /* PasswordViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B65C004B20A43BF9008388A1 /* PasswordViewController.swift */; }; 17 | B662A308209339E100C7418F /* Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = B662A307209339E100C7418F /* Extensions.swift */; }; 18 | B685CBF52091B9470072C4E7 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B685CBF42091B9470072C4E7 /* AppDelegate.swift */; }; 19 | B685CBF72091B9470072C4E7 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B685CBF62091B9470072C4E7 /* ViewController.swift */; }; 20 | B685CBFA2091B9470072C4E7 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = B685CBF82091B9470072C4E7 /* Main.storyboard */; }; 21 | B685CBFC2091B9470072C4E7 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B685CBFB2091B9470072C4E7 /* Assets.xcassets */; }; 22 | B685CBFF2091B9470072C4E7 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = B685CBFD2091B9470072C4E7 /* LaunchScreen.storyboard */; }; 23 | B685CC082091B9550072C4E7 /* GPassword.swift in Sources */ = {isa = PBXBuildFile; fileRef = B685CC072091B9550072C4E7 /* GPassword.swift */; }; 24 | B685CC0E2091CF050072C4E7 /* LockOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = B685CC0D2091CF050072C4E7 /* LockOptions.swift */; }; 25 | B6E79DD82094567C00DB69B5 /* Box.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6E79DD72094567C00DB69B5 /* Box.swift */; }; 26 | /* End PBXBuildFile section */ 27 | 28 | /* Begin PBXFileReference section */ 29 | B612008E20A594AD003906D1 /* KeychainSwift.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainSwift.swift; sourceTree = ""; }; 30 | B612009020A59C0E003906D1 /* LockManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LockManager.swift; sourceTree = ""; }; 31 | B612009220A93C6D003906D1 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Main.strings"; sourceTree = ""; }; 32 | B612009320A93C6D003906D1 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/LaunchScreen.strings"; sourceTree = ""; }; 33 | B612009420A93C73003906D1 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Main.strings; sourceTree = ""; }; 34 | B612009520A93C73003906D1 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/LaunchScreen.strings; sourceTree = ""; }; 35 | B612009920A93CB4003906D1 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; 36 | B612009B20A93CB7003906D1 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Localizable.strings"; sourceTree = ""; }; 37 | B615A3AC20AED54700BF1FE6 /* Storage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Storage.swift; sourceTree = ""; }; 38 | B64A2C1B20A0449B00B17B43 /* Point.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Point.swift; sourceTree = ""; }; 39 | B65C003520A4262C008388A1 /* EventDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventDelegate.swift; sourceTree = ""; }; 40 | B65C004B20A43BF9008388A1 /* PasswordViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasswordViewController.swift; sourceTree = ""; }; 41 | B662A307209339E100C7418F /* Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Extensions.swift; sourceTree = ""; }; 42 | B685CBF12091B9470072C4E7 /* GPassword.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = GPassword.app; sourceTree = BUILT_PRODUCTS_DIR; }; 43 | B685CBF42091B9470072C4E7 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 44 | B685CBF62091B9470072C4E7 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 45 | B685CBF92091B9470072C4E7 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 46 | B685CBFB2091B9470072C4E7 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 47 | B685CBFE2091B9470072C4E7 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 48 | B685CC002091B9470072C4E7 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 49 | B685CC072091B9550072C4E7 /* GPassword.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GPassword.swift; sourceTree = ""; }; 50 | B685CC0D2091CF050072C4E7 /* LockOptions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LockOptions.swift; sourceTree = ""; }; 51 | B6E79DD72094567C00DB69B5 /* Box.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Box.swift; sourceTree = ""; }; 52 | /* End PBXFileReference section */ 53 | 54 | /* Begin PBXFrameworksBuildPhase section */ 55 | B685CBEE2091B9470072C4E7 /* Frameworks */ = { 56 | isa = PBXFrameworksBuildPhase; 57 | buildActionMask = 2147483647; 58 | files = ( 59 | ); 60 | runOnlyForDeploymentPostprocessing = 0; 61 | }; 62 | /* End PBXFrameworksBuildPhase section */ 63 | 64 | /* Begin PBXGroup section */ 65 | B615A3AB20AED53100BF1FE6 /* Store */ = { 66 | isa = PBXGroup; 67 | children = ( 68 | B612008E20A594AD003906D1 /* KeychainSwift.swift */, 69 | B615A3AC20AED54700BF1FE6 /* Storage.swift */, 70 | ); 71 | path = Store; 72 | sourceTree = ""; 73 | }; 74 | B662A3042093393F00C7418F /* Extension */ = { 75 | isa = PBXGroup; 76 | children = ( 77 | B662A307209339E100C7418F /* Extensions.swift */, 78 | ); 79 | path = Extension; 80 | sourceTree = ""; 81 | }; 82 | B685CBE82091B9470072C4E7 = { 83 | isa = PBXGroup; 84 | children = ( 85 | B685CC062091B9550072C4E7 /* Source */, 86 | B685CBF32091B9470072C4E7 /* GPassword */, 87 | B685CBF22091B9470072C4E7 /* Products */, 88 | ); 89 | sourceTree = ""; 90 | }; 91 | B685CBF22091B9470072C4E7 /* Products */ = { 92 | isa = PBXGroup; 93 | children = ( 94 | B685CBF12091B9470072C4E7 /* GPassword.app */, 95 | ); 96 | name = Products; 97 | sourceTree = ""; 98 | }; 99 | B685CBF32091B9470072C4E7 /* GPassword */ = { 100 | isa = PBXGroup; 101 | children = ( 102 | B685CBF42091B9470072C4E7 /* AppDelegate.swift */, 103 | B685CBF62091B9470072C4E7 /* ViewController.swift */, 104 | B65C004B20A43BF9008388A1 /* PasswordViewController.swift */, 105 | B612009A20A93CB4003906D1 /* Localizable.strings */, 106 | B685CBF82091B9470072C4E7 /* Main.storyboard */, 107 | B685CBFB2091B9470072C4E7 /* Assets.xcassets */, 108 | B685CBFD2091B9470072C4E7 /* LaunchScreen.storyboard */, 109 | B685CC002091B9470072C4E7 /* Info.plist */, 110 | ); 111 | path = GPassword; 112 | sourceTree = ""; 113 | }; 114 | B685CC062091B9550072C4E7 /* Source */ = { 115 | isa = PBXGroup; 116 | children = ( 117 | B685CC0A2091C5CD0072C4E7 /* Core */, 118 | B615A3AB20AED53100BF1FE6 /* Store */, 119 | B685CC072091B9550072C4E7 /* GPassword.swift */, 120 | B662A3042093393F00C7418F /* Extension */, 121 | ); 122 | name = Source; 123 | path = ../Source; 124 | sourceTree = ""; 125 | }; 126 | B685CC0A2091C5CD0072C4E7 /* Core */ = { 127 | isa = PBXGroup; 128 | children = ( 129 | B612009020A59C0E003906D1 /* LockManager.swift */, 130 | B685CC0D2091CF050072C4E7 /* LockOptions.swift */, 131 | B6E79DD72094567C00DB69B5 /* Box.swift */, 132 | B64A2C1B20A0449B00B17B43 /* Point.swift */, 133 | B65C003520A4262C008388A1 /* EventDelegate.swift */, 134 | ); 135 | path = Core; 136 | sourceTree = ""; 137 | }; 138 | /* End PBXGroup section */ 139 | 140 | /* Begin PBXNativeTarget section */ 141 | B685CBF02091B9470072C4E7 /* GPassword */ = { 142 | isa = PBXNativeTarget; 143 | buildConfigurationList = B685CC032091B9470072C4E7 /* Build configuration list for PBXNativeTarget "GPassword" */; 144 | buildPhases = ( 145 | B685CBED2091B9470072C4E7 /* Sources */, 146 | B685CBEE2091B9470072C4E7 /* Frameworks */, 147 | B685CBEF2091B9470072C4E7 /* Resources */, 148 | ); 149 | buildRules = ( 150 | ); 151 | dependencies = ( 152 | ); 153 | name = GPassword; 154 | productName = GPassword; 155 | productReference = B685CBF12091B9470072C4E7 /* GPassword.app */; 156 | productType = "com.apple.product-type.application"; 157 | }; 158 | /* End PBXNativeTarget section */ 159 | 160 | /* Begin PBXProject section */ 161 | B685CBE92091B9470072C4E7 /* Project object */ = { 162 | isa = PBXProject; 163 | attributes = { 164 | LastSwiftUpdateCheck = 0920; 165 | LastUpgradeCheck = 0920; 166 | ORGANIZATIONNAME = leoli; 167 | TargetAttributes = { 168 | B685CBF02091B9470072C4E7 = { 169 | CreatedOnToolsVersion = 9.2; 170 | ProvisioningStyle = Automatic; 171 | }; 172 | }; 173 | }; 174 | buildConfigurationList = B685CBEC2091B9470072C4E7 /* Build configuration list for PBXProject "GPassword" */; 175 | compatibilityVersion = "Xcode 8.0"; 176 | developmentRegion = en; 177 | hasScannedForEncodings = 0; 178 | knownRegions = ( 179 | Base, 180 | "zh-Hans", 181 | ); 182 | mainGroup = B685CBE82091B9470072C4E7; 183 | productRefGroup = B685CBF22091B9470072C4E7 /* Products */; 184 | projectDirPath = ""; 185 | projectRoot = ""; 186 | targets = ( 187 | B685CBF02091B9470072C4E7 /* GPassword */, 188 | ); 189 | }; 190 | /* End PBXProject section */ 191 | 192 | /* Begin PBXResourcesBuildPhase section */ 193 | B685CBEF2091B9470072C4E7 /* Resources */ = { 194 | isa = PBXResourcesBuildPhase; 195 | buildActionMask = 2147483647; 196 | files = ( 197 | B685CBFF2091B9470072C4E7 /* LaunchScreen.storyboard in Resources */, 198 | B612009820A93CB4003906D1 /* Localizable.strings in Resources */, 199 | B685CBFC2091B9470072C4E7 /* Assets.xcassets in Resources */, 200 | B685CBFA2091B9470072C4E7 /* Main.storyboard in Resources */, 201 | ); 202 | runOnlyForDeploymentPostprocessing = 0; 203 | }; 204 | /* End PBXResourcesBuildPhase section */ 205 | 206 | /* Begin PBXSourcesBuildPhase section */ 207 | B685CBED2091B9470072C4E7 /* Sources */ = { 208 | isa = PBXSourcesBuildPhase; 209 | buildActionMask = 2147483647; 210 | files = ( 211 | B65C004C20A43BF9008388A1 /* PasswordViewController.swift in Sources */, 212 | B685CC082091B9550072C4E7 /* GPassword.swift in Sources */, 213 | B662A308209339E100C7418F /* Extensions.swift in Sources */, 214 | B685CBF72091B9470072C4E7 /* ViewController.swift in Sources */, 215 | B612009120A59C0E003906D1 /* LockManager.swift in Sources */, 216 | B615A3AD20AED54700BF1FE6 /* Storage.swift in Sources */, 217 | B64A2C1C20A0449B00B17B43 /* Point.swift in Sources */, 218 | B6E79DD82094567C00DB69B5 /* Box.swift in Sources */, 219 | B685CC0E2091CF050072C4E7 /* LockOptions.swift in Sources */, 220 | B612008F20A594AD003906D1 /* KeychainSwift.swift in Sources */, 221 | B685CBF52091B9470072C4E7 /* AppDelegate.swift in Sources */, 222 | B65C003620A4262C008388A1 /* EventDelegate.swift in Sources */, 223 | ); 224 | runOnlyForDeploymentPostprocessing = 0; 225 | }; 226 | /* End PBXSourcesBuildPhase section */ 227 | 228 | /* Begin PBXVariantGroup section */ 229 | B612009A20A93CB4003906D1 /* Localizable.strings */ = { 230 | isa = PBXVariantGroup; 231 | children = ( 232 | B612009920A93CB4003906D1 /* en */, 233 | B612009B20A93CB7003906D1 /* zh-Hans */, 234 | ); 235 | name = Localizable.strings; 236 | sourceTree = ""; 237 | }; 238 | B685CBF82091B9470072C4E7 /* Main.storyboard */ = { 239 | isa = PBXVariantGroup; 240 | children = ( 241 | B685CBF92091B9470072C4E7 /* Base */, 242 | B612009220A93C6D003906D1 /* zh-Hans */, 243 | B612009420A93C73003906D1 /* en */, 244 | ); 245 | name = Main.storyboard; 246 | sourceTree = ""; 247 | }; 248 | B685CBFD2091B9470072C4E7 /* LaunchScreen.storyboard */ = { 249 | isa = PBXVariantGroup; 250 | children = ( 251 | B685CBFE2091B9470072C4E7 /* Base */, 252 | B612009320A93C6D003906D1 /* zh-Hans */, 253 | B612009520A93C73003906D1 /* en */, 254 | ); 255 | name = LaunchScreen.storyboard; 256 | sourceTree = ""; 257 | }; 258 | /* End PBXVariantGroup section */ 259 | 260 | /* Begin XCBuildConfiguration section */ 261 | B685CC012091B9470072C4E7 /* Debug */ = { 262 | isa = XCBuildConfiguration; 263 | buildSettings = { 264 | ALWAYS_SEARCH_USER_PATHS = NO; 265 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; 266 | CLANG_ANALYZER_NONNULL = YES; 267 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 268 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 269 | CLANG_CXX_LIBRARY = "libc++"; 270 | CLANG_ENABLE_MODULES = YES; 271 | CLANG_ENABLE_OBJC_ARC = YES; 272 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 273 | CLANG_WARN_BOOL_CONVERSION = YES; 274 | CLANG_WARN_COMMA = YES; 275 | CLANG_WARN_CONSTANT_CONVERSION = YES; 276 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 277 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 278 | CLANG_WARN_EMPTY_BODY = YES; 279 | CLANG_WARN_ENUM_CONVERSION = YES; 280 | CLANG_WARN_INFINITE_RECURSION = YES; 281 | CLANG_WARN_INT_CONVERSION = YES; 282 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 283 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 284 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 285 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 286 | CLANG_WARN_STRICT_PROTOTYPES = YES; 287 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 288 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 289 | CLANG_WARN_UNREACHABLE_CODE = YES; 290 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 291 | CODE_SIGN_IDENTITY = "iPhone Developer"; 292 | COPY_PHASE_STRIP = NO; 293 | DEBUG_INFORMATION_FORMAT = dwarf; 294 | ENABLE_STRICT_OBJC_MSGSEND = YES; 295 | ENABLE_TESTABILITY = YES; 296 | GCC_C_LANGUAGE_STANDARD = gnu11; 297 | GCC_DYNAMIC_NO_PIC = NO; 298 | GCC_NO_COMMON_BLOCKS = YES; 299 | GCC_OPTIMIZATION_LEVEL = 0; 300 | GCC_PREPROCESSOR_DEFINITIONS = ( 301 | "DEBUG=1", 302 | "$(inherited)", 303 | ); 304 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 305 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 306 | GCC_WARN_UNDECLARED_SELECTOR = YES; 307 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 308 | GCC_WARN_UNUSED_FUNCTION = YES; 309 | GCC_WARN_UNUSED_VARIABLE = YES; 310 | IPHONEOS_DEPLOYMENT_TARGET = 11.2; 311 | MTL_ENABLE_DEBUG_INFO = YES; 312 | ONLY_ACTIVE_ARCH = YES; 313 | SDKROOT = iphoneos; 314 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 315 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 316 | }; 317 | name = Debug; 318 | }; 319 | B685CC022091B9470072C4E7 /* Release */ = { 320 | isa = XCBuildConfiguration; 321 | buildSettings = { 322 | ALWAYS_SEARCH_USER_PATHS = NO; 323 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; 324 | CLANG_ANALYZER_NONNULL = YES; 325 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 326 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 327 | CLANG_CXX_LIBRARY = "libc++"; 328 | CLANG_ENABLE_MODULES = YES; 329 | CLANG_ENABLE_OBJC_ARC = YES; 330 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 331 | CLANG_WARN_BOOL_CONVERSION = YES; 332 | CLANG_WARN_COMMA = YES; 333 | CLANG_WARN_CONSTANT_CONVERSION = YES; 334 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 335 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 336 | CLANG_WARN_EMPTY_BODY = YES; 337 | CLANG_WARN_ENUM_CONVERSION = YES; 338 | CLANG_WARN_INFINITE_RECURSION = YES; 339 | CLANG_WARN_INT_CONVERSION = YES; 340 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 341 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 342 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 343 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 344 | CLANG_WARN_STRICT_PROTOTYPES = YES; 345 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 346 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 347 | CLANG_WARN_UNREACHABLE_CODE = YES; 348 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 349 | CODE_SIGN_IDENTITY = "iPhone Developer"; 350 | COPY_PHASE_STRIP = NO; 351 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 352 | ENABLE_NS_ASSERTIONS = NO; 353 | ENABLE_STRICT_OBJC_MSGSEND = YES; 354 | GCC_C_LANGUAGE_STANDARD = gnu11; 355 | GCC_NO_COMMON_BLOCKS = YES; 356 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 357 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 358 | GCC_WARN_UNDECLARED_SELECTOR = YES; 359 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 360 | GCC_WARN_UNUSED_FUNCTION = YES; 361 | GCC_WARN_UNUSED_VARIABLE = YES; 362 | IPHONEOS_DEPLOYMENT_TARGET = 11.2; 363 | MTL_ENABLE_DEBUG_INFO = NO; 364 | SDKROOT = iphoneos; 365 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 366 | VALIDATE_PRODUCT = YES; 367 | }; 368 | name = Release; 369 | }; 370 | B685CC042091B9470072C4E7 /* Debug */ = { 371 | isa = XCBuildConfiguration; 372 | buildSettings = { 373 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 374 | CODE_SIGN_STYLE = Automatic; 375 | INFOPLIST_FILE = GPassword/Info.plist; 376 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 377 | PRODUCT_BUNDLE_IDENTIFIER = com.leoli.GPassword; 378 | PRODUCT_NAME = "$(TARGET_NAME)"; 379 | SWIFT_VERSION = 4.0; 380 | TARGETED_DEVICE_FAMILY = "1,2"; 381 | }; 382 | name = Debug; 383 | }; 384 | B685CC052091B9470072C4E7 /* Release */ = { 385 | isa = XCBuildConfiguration; 386 | buildSettings = { 387 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 388 | CODE_SIGN_STYLE = Automatic; 389 | INFOPLIST_FILE = GPassword/Info.plist; 390 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 391 | PRODUCT_BUNDLE_IDENTIFIER = com.leoli.GPassword; 392 | PRODUCT_NAME = "$(TARGET_NAME)"; 393 | SWIFT_VERSION = 4.0; 394 | TARGETED_DEVICE_FAMILY = "1,2"; 395 | }; 396 | name = Release; 397 | }; 398 | /* End XCBuildConfiguration section */ 399 | 400 | /* Begin XCConfigurationList section */ 401 | B685CBEC2091B9470072C4E7 /* Build configuration list for PBXProject "GPassword" */ = { 402 | isa = XCConfigurationList; 403 | buildConfigurations = ( 404 | B685CC012091B9470072C4E7 /* Debug */, 405 | B685CC022091B9470072C4E7 /* Release */, 406 | ); 407 | defaultConfigurationIsVisible = 0; 408 | defaultConfigurationName = Release; 409 | }; 410 | B685CC032091B9470072C4E7 /* Build configuration list for PBXNativeTarget "GPassword" */ = { 411 | isa = XCConfigurationList; 412 | buildConfigurations = ( 413 | B685CC042091B9470072C4E7 /* Debug */, 414 | B685CC052091B9470072C4E7 /* Release */, 415 | ); 416 | defaultConfigurationIsVisible = 0; 417 | defaultConfigurationName = Release; 418 | }; 419 | /* End XCConfigurationList section */ 420 | }; 421 | rootObject = B685CBE92091B9470072C4E7 /* Project object */; 422 | } 423 | -------------------------------------------------------------------------------- /GPassword/GPassword/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // GPassword 4 | // 5 | // Created by Jie Li on 10/5/18. 6 | // 7 | // Copyright (c) 2018 Jie Li 8 | // 9 | // Permission is hereby granted, free of charge, to any person obtaining a copy 10 | // of this software and associated documentation files (the "Software"), to deal 11 | // in the Software without restriction, including without limitation the rights 12 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | // copies of the Software, and to permit persons to whom the Software is 14 | // furnished to do so, subject to the following conditions: 15 | // 16 | // The above copyright notice and this permission notice shall be included in 17 | // all copies or substantial portions of the Software. 18 | // 19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 | // THE SOFTWARE. 26 | 27 | import UIKit 28 | 29 | @UIApplicationMain 30 | class AppDelegate: UIResponder, UIApplicationDelegate { 31 | 32 | var window: UIWindow? 33 | 34 | #if swift(>=4.2) 35 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 36 | 37 | let vc = ViewController() 38 | let nav = UINavigationController(rootViewController: vc) 39 | window = UIWindow(frame: UIScreen.main.bounds) 40 | window?.backgroundColor = .white 41 | window?.rootViewController = nav 42 | window?.makeKeyAndVisible() 43 | return true 44 | } 45 | #else 46 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { 47 | 48 | let vc = ViewController() 49 | let nav = UINavigationController(rootViewController: vc) 50 | window = UIWindow(frame: UIScreen.main.bounds) 51 | window?.backgroundColor = .white 52 | window?.rootViewController = nav 53 | window?.makeKeyAndVisible() 54 | return true 55 | } 56 | #endif 57 | } 58 | 59 | -------------------------------------------------------------------------------- /GPassword/GPassword/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | } 88 | ], 89 | "info" : { 90 | "version" : 1, 91 | "author" : "xcode" 92 | } 93 | } -------------------------------------------------------------------------------- /GPassword/GPassword/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /GPassword/GPassword/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /GPassword/GPassword/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIMainStoryboardFile 26 | Main 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /GPassword/GPassword/PasswordViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PasswordViewController.swift 3 | // GPassword 4 | // 5 | // Created by Jie Li on 10/5/18. 6 | // 7 | // Copyright (c) 2018 Jie Li 8 | // 9 | // Permission is hereby granted, free of charge, to any person obtaining a copy 10 | // of this software and associated documentation files (the "Software"), to deal 11 | // in the Software without restriction, including without limitation the rights 12 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | // copies of the Software, and to permit persons to whom the Software is 14 | // furnished to do so, subject to the following conditions: 15 | // 16 | // The above copyright notice and this permission notice shall be included in 17 | // all copies or substantial portions of the Software. 18 | // 19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 | // THE SOFTWARE. 26 | 27 | import UIKit 28 | 29 | let lessConnectPointsNum: Int = 3 30 | let passwordKey = "gesture_password" 31 | let errorPasswordKey = "error_gesture_password" 32 | 33 | enum GType { 34 | case set 35 | case verify 36 | case modify 37 | } 38 | 39 | class WarnLabel: UILabel { 40 | override init(frame: CGRect) { 41 | super.init(frame: frame) 42 | textAlignment = .center 43 | } 44 | 45 | required init?(coder aDecoder: NSCoder) { 46 | fatalError("init(coder:) has not been implemented") 47 | } 48 | 49 | func showNormal(with message: String) { 50 | text = message 51 | textColor = UIColor.black 52 | } 53 | 54 | func showWarn(with message: String) { 55 | text = message 56 | textColor = UIColor(gpRGB: 0xC94349) 57 | layer.gp_shake() 58 | } 59 | } 60 | 61 | class PasswordViewController: UIViewController { 62 | 63 | // MARK: - Properies 64 | fileprivate lazy var passwordBox: Box = { 65 | let box = Box(frame: CGRect(x: 50, y: 200, width: GWidth - 2 * 50, height: 400)) 66 | box.delegate = self 67 | return box 68 | }() 69 | 70 | fileprivate lazy var warnLabel: WarnLabel = { 71 | let label = WarnLabel(frame: CGRect(x: 50, y: 140, width: GWidth - 2 * 50, height: 20)) 72 | label.text = String.gp_localized("normal_title") 73 | return label 74 | }() 75 | 76 | fileprivate lazy var LoginBtn: UIButton = { 77 | let button = UIButton(frame: CGRect(x: (GWidth - 200)/2, y: GHeight - 60, width: 200, height: 20)) 78 | button.setTitle(String.gp_localized("forget_password_title"), for: .normal) 79 | button.setTitleColor(UIColor(gpRGB: 0x1D80FC), for: .normal) 80 | return button 81 | }() 82 | 83 | var password: String = "" 84 | var firstPassword: String = "" 85 | var secondPassword: String = "" 86 | var canModify: Bool = false 87 | var type: GType? { 88 | didSet { 89 | if type == .set { 90 | warnLabel.text = String.gp_localized("setting_password_title") 91 | } else if type == .verify { 92 | warnLabel.text = String.gp_localized("normal_title") 93 | } else { 94 | warnLabel.text = String.gp_localized("input_origin_password") 95 | } 96 | } 97 | } 98 | 99 | // MARK: - Lifecycle 100 | override func viewDidLoad() { 101 | super.viewDidLoad() 102 | 103 | setupSubviews() 104 | } 105 | 106 | override func didReceiveMemoryWarning() { 107 | super.didReceiveMemoryWarning() 108 | // Dispose of any resources that can be recreated. 109 | } 110 | 111 | deinit { 112 | print("deinit") 113 | } 114 | 115 | // MARK: - layout 116 | func setupSubviews() { 117 | view.backgroundColor = .white 118 | title = String.gp_localized("gesture_password") 119 | GPassword.config { (options) in 120 | options.connectLineStart = .border 121 | options.normalstyle = .outerStroke 122 | options.isDrawTriangle = false 123 | options.connectLineWidth = 4 124 | options.matrixNum = 3 125 | options.keySuffix = "leoli" 126 | } 127 | 128 | view.addSubview(passwordBox) 129 | view.addSubview(warnLabel) 130 | view.addSubview(LoginBtn) 131 | 132 | print(GPassword.getPassword() ?? "") 133 | } 134 | } 135 | 136 | extension PasswordViewController: GPasswordEventDelegate { 137 | func sendTouchPoint(with tag: String) { 138 | print(tag) 139 | password += tag 140 | } 141 | 142 | func touchesEnded() { 143 | print("gesture end") 144 | 145 | if password.count < lessConnectPointsNum { 146 | warnLabel.showWarn(with: String.gp_localized("less_connect_point_num")) 147 | warnLabel.textColor = UIColor(gpRGB: 0xC94349) 148 | } else { 149 | if type == .set { 150 | setPassword() 151 | } else if type == .modify { 152 | let savePassword = GPassword.getPassword() 153 | if let pass = savePassword { 154 | if canModify { 155 | setPassword() 156 | } else { 157 | if password == pass { 158 | warnLabel.showNormal(with: String.gp_localized("setting_password_title")) 159 | canModify = true 160 | } else { 161 | verifyPasswordError() 162 | } 163 | } 164 | } 165 | } else { 166 | let savePassword = GPassword.getPassword() ?? "" 167 | if password == savePassword { 168 | navigationController?.popViewController(animated: true) 169 | } else { 170 | verifyPasswordError() 171 | } 172 | } 173 | } 174 | 175 | password = "" 176 | } 177 | } 178 | 179 | extension PasswordViewController { 180 | func setPassword() { 181 | if firstPassword.isEmpty { 182 | firstPassword = password 183 | warnLabel.showNormal(with: String.gp_localized("once_input_confirm")) 184 | } else { 185 | secondPassword = password 186 | if firstPassword == secondPassword { 187 | GPassword.save(password: firstPassword) 188 | navigationController?.popViewController(animated: true) 189 | } else { 190 | warnLabel.showWarn(with: String.gp_localized("two_input_different")) 191 | DispatchQueue.main.asyncAfter(deadline: .now()+1) { 192 | self.warnLabel.showNormal(with: String.gp_localized("setting_password_title")) 193 | } 194 | firstPassword = "" 195 | secondPassword = "" 196 | } 197 | } 198 | } 199 | 200 | func verifyPasswordError() { 201 | GPassword.increaseErrorNum() 202 | let errorNum = globalOptions.maxErrorNum - GPassword.getErrorNum()! 203 | if errorNum == 0 { 204 | // do action about forgetting password 205 | warnLabel.showWarn(with: String.gp_localized("forget_password_title")) 206 | } else { 207 | var errorTitle = String.gp_localized("password_input_error") 208 | + "\(errorNum)" 209 | if PasswordViewController.getCurrentLanguage() == "en" { 210 | errorTitle += "times" 211 | } else { 212 | errorTitle += "次" 213 | } 214 | warnLabel.showWarn(with: errorTitle) 215 | } 216 | } 217 | 218 | static func getCurrentLanguage() -> String { 219 | let preferredLang = Bundle.main.preferredLocalizations.first! as NSString 220 | switch String(describing: preferredLang) { 221 | case "en-US", "en-CN": 222 | return "en" 223 | case "zh-Hans-US","zh-Hans-CN","zh-Hant-CN","zh-TW","zh-HK","zh-Hans": 224 | return "cn" 225 | default: 226 | return "en" 227 | } 228 | } 229 | } 230 | -------------------------------------------------------------------------------- /GPassword/GPassword/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // GPassword 4 | // 5 | // Created by Jie Li on 10/5/18. 6 | // 7 | // Copyright (c) 2018 Jie Li 8 | // 9 | // Permission is hereby granted, free of charge, to any person obtaining a copy 10 | // of this software and associated documentation files (the "Software"), to deal 11 | // in the Software without restriction, including without limitation the rights 12 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | // copies of the Software, and to permit persons to whom the Software is 14 | // furnished to do so, subject to the following conditions: 15 | // 16 | // The above copyright notice and this permission notice shall be included in 17 | // all copies or substantial portions of the Software. 18 | // 19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 | // THE SOFTWARE. 26 | 27 | let GWidth = UIScreen.main.bounds.width 28 | let GHeight = UIScreen.main.bounds.height 29 | 30 | import UIKit 31 | 32 | class ViewController: UIViewController { 33 | 34 | var tableView: UITableView = UITableView.init(frame: CGRect.init(x: 0, y: 0, width: GWidth, height: GHeight)) 35 | var titles: [String] = ["Set password", "Verify password", "Modify password", "Open password track", "Close password track"] 36 | 37 | override func viewDidLoad() { 38 | super.viewDidLoad() 39 | 40 | view.addSubview(tableView) 41 | tableView.delegate = self 42 | tableView.dataSource = self 43 | tableView.rowHeight = 44 44 | } 45 | 46 | override func didReceiveMemoryWarning() { 47 | super.didReceiveMemoryWarning() 48 | // Dispose of any resources that can be recreated. 49 | } 50 | } 51 | 52 | extension ViewController: UITableViewDelegate, UITableViewDataSource { 53 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 54 | return titles.count 55 | } 56 | 57 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 58 | var cell = tableView.dequeueReusableCell(withIdentifier: "GPassword") 59 | if cell == nil { 60 | cell = UITableViewCell.init(style: .default, reuseIdentifier: "GPassword") 61 | } 62 | cell?.textLabel?.text = titles[indexPath.row] 63 | return cell! 64 | } 65 | 66 | func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 67 | tableView.deselectRow(at: indexPath, animated: true) 68 | 69 | let vc = PasswordViewController() 70 | if indexPath.row == 0 { 71 | vc.type = .set 72 | navigationController?.pushViewController(vc, animated: true) 73 | } else if indexPath.row == 1 { 74 | vc.type = .verify 75 | navigationController?.pushViewController(vc, animated: true) 76 | } else if indexPath.row == 2 { 77 | vc.type = .modify 78 | navigationController?.pushViewController(vc, animated: true) 79 | } else if indexPath.row == 3 { 80 | GPassword.openTrack() 81 | } else if indexPath.row == 4 { 82 | GPassword.closeTrack() 83 | } 84 | } 85 | } 86 | 87 | -------------------------------------------------------------------------------- /GPassword/GPassword/en.lproj/GPassword.strings: -------------------------------------------------------------------------------- 1 | /* 2 | GPassword.strings 3 | GPassword 4 | 5 | Created by leoli on 2018/5/10. 6 | Copyright © 2018年 leoli. All rights reserved. 7 | */ 8 | 9 | -------------------------------------------------------------------------------- /GPassword/GPassword/en.lproj/LaunchScreen.strings: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /GPassword/GPassword/en.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | /* 2 | Localizable.strings 3 | GPassword 4 | 5 | Created by leoli on 2018/5/14. 6 | Copyright © 2018年 leoli. All rights reserved. 7 | */ 8 | "normal_title" = "Please input gesture password"; 9 | "gesture_password" = "Gesture Password"; 10 | "forget_password_title" = "forget password?"; 11 | "less_connect_point_num" = "At last connect 3 points, please redraw"; 12 | "setting_password_title" = "Please input gesture password"; 13 | "once_input_confirm" = "Please input again to confirm"; 14 | "two_input_different" = "Passwords different, Please re-set"; 15 | "password_input_error" = "Error password, can input"; 16 | "input_origin_password" = "Please input origin password"; 17 | -------------------------------------------------------------------------------- /GPassword/GPassword/en.lproj/Main.strings: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /GPassword/GPassword/zh-Hans.lproj/GPassword.strings: -------------------------------------------------------------------------------- 1 | /* 2 | GPassword.strings 3 | GPassword 4 | 5 | Created by leoli on 2018/5/10. 6 | Copyright © 2018年 leoli. All rights reserved. 7 | */ 8 | "normal_title" = "请输入手势密码"; 9 | -------------------------------------------------------------------------------- /GPassword/GPassword/zh-Hans.lproj/LaunchScreen.strings: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /GPassword/GPassword/zh-Hans.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | /* 2 | Localizable.strings 3 | GPassword 4 | 5 | Created by leoli on 2018/5/14. 6 | Copyright © 2018年 leoli. All rights reserved. 7 | */ 8 | "normal_title" = "请输入手势密码"; 9 | "gesture_password" = "手势密码"; 10 | "forget_password_title" = "忘记手势密码?"; 11 | "less_connect_point_num" = "至少连接 3 个点, 请重新绘制"; 12 | "setting_password_title" = "请设置手势密码"; 13 | "once_input_confirm" = "再次输入以确认"; 14 | "two_input_different" = "密码不一致, 请重新设置"; 15 | "password_input_error" = "密码错误, 你还可以输入"; 16 | "input_origin_password" = "请输入原手势密码"; 17 | -------------------------------------------------------------------------------- /GPassword/GPassword/zh-Hans.lproj/Main.strings: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Li Jie 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GPassword 2 | 3 | ![GPassword](https://github.com/hackjie/GPassword/blob/master/Resources/GPassword-logo.png) 4 | 5 | [![](https://travis-ci.org/hackjie/GPassword.svg?branch=master)](https://travis-ci.org/hackjie/GPassword) 6 | ![](https://img.shields.io/badge/language-swift-orange.svg) 7 | ![](https://img.shields.io/badge/platform-ios-lightgrey.svg) 8 | ![](https://img.shields.io/badge/license-MIT-000000.svg) 9 | 10 | Simple gesture password in swift 11 | 12 | 中文版本参考[这里](https://github.com/hackjie/GPassword/wiki/GPassword) 13 | 14 | ## Features 15 | 16 | * Use delegate for gesture view to pass password 17 | * Use CAShapeLayer、UIBezeierPath to draw for good performance 18 | * Support define `3*3`、`4*4`... Matrix 19 | * Support define many kinds of normal and selected style 20 | * Use KeyChain and UserDefaults to save informations 21 | 22 | ## Screenshots 23 | 24 |

25 | 26 | 27 | 28 |

29 | 30 | ## Requirements 31 | 32 | * iOS 8.0+ 33 | * Xcode 9.0+ 34 | * Swift 4.0+ 35 | 36 | ## Install 37 | 38 | CocoaPods 39 | 40 | ```swift 41 | pod "GPassword" 42 | ``` 43 | 44 | or you can drag Source folder into your project. 45 | 46 | ## Usage 47 | 48 | First custom UI style, here is what you need: 49 | 50 | ```swift 51 | GPassword.config { (options) in 52 | options.connectLineStart = .border 53 | options.normalstyle = .innerFill 54 | options.isDrawTriangle = true 55 | options.matrixNum = 3 56 | } 57 | ``` 58 | 59 | then you can use `Box.swift` or add it to a UIViewController 60 | 61 | ```swift 62 | fileprivate lazy var passwordBox: Box = { 63 | let box = Box(frame: CGRect(x: 50, y: 200, width: GWidth - 2 * 50, height: 400)) 64 | box.delegate = self 65 | return box 66 | }() 67 | ``` 68 | 69 | then you need to achieve two delegate methods, you should write business logics in them, `sendTouchPoint` can send out complete password and `touchesEnded` can deal business logics according to type(setting/verify/modify) 70 | 71 | ```swift 72 | extension PasswordViewController: EventDelegate { 73 | func sendTouchPoint(with tag: String) { 74 | password += tag 75 | // get complete password 76 | } 77 | 78 | func touchesEnded() { 79 | // write business logics according to type 80 | } 81 | } 82 | ``` 83 | 84 | more informations you can see `PasswordViewController.swift` in demo project, I write an example. 85 | 86 | ## License 87 | 88 | GPassword is released under the MIT license. [See LICENSE](https://github.com/hackjie/GPassword/blob/master/LICENSE) for details 89 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /Resources/GPassword-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackjie/GPassword/db3b825518c5aba523f1c795d8f071b289f09398/Resources/GPassword-logo.png -------------------------------------------------------------------------------- /Resources/first.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackjie/GPassword/db3b825518c5aba523f1c795d8f071b289f09398/Resources/first.gif -------------------------------------------------------------------------------- /Resources/second.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackjie/GPassword/db3b825518c5aba523f1c795d8f071b289f09398/Resources/second.gif -------------------------------------------------------------------------------- /Resources/third.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackjie/GPassword/db3b825518c5aba523f1c795d8f071b289f09398/Resources/third.gif -------------------------------------------------------------------------------- /Source/Core/Box.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Box.swift 3 | // GPassword 4 | // 5 | // Created by Jie Li on 27/4/18. 6 | // 7 | // Copyright (c) 2018 Jie Li 8 | // 9 | // Permission is hereby granted, free of charge, to any person obtaining a copy 10 | // of this software and associated documentation files (the "Software"), to deal 11 | // in the Software without restriction, including without limitation the rights 12 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | // copies of the Software, and to permit persons to whom the Software is 14 | // furnished to do so, subject to the following conditions: 15 | // 16 | // The above copyright notice and this permission notice shall be included in 17 | // all copies or substantial portions of the Software. 18 | // 19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 | // THE SOFTWARE. 26 | 27 | import UIKit 28 | 29 | /// Responsible for contain points and control touches 30 | open class Box: UIView { 31 | // MARK: - Properties 32 | 33 | /// Points selected in box 34 | fileprivate var points = [Point]() 35 | 36 | /// Current touching point 37 | var currentPoint: CGPoint? 38 | 39 | /// Connect line 40 | fileprivate var lineLayer = CAShapeLayer() 41 | 42 | public weak var delegate: GPasswordEventDelegate? 43 | 44 | // MARK: - Lifecycle 45 | override public init(frame: CGRect) { 46 | super.init(frame: frame) 47 | backgroundColor = globalOptions.boxBackgroundColor 48 | setupSubViews() 49 | } 50 | 51 | required public init?(coder aDecoder: NSCoder) { 52 | fatalError("init(coder:) has not been implemented") 53 | } 54 | 55 | private func setupSubViews() { 56 | (0..<(globalOptions.matrixNum * globalOptions.matrixNum)).forEach { (offset) in 57 | let space = globalOptions.pointSpace 58 | let pointWH = (frame.width - CGFloat(globalOptions.matrixNum - 1) * space) / CGFloat(globalOptions.matrixNum) 59 | // layout vertically 60 | let row = CGFloat(offset % globalOptions.matrixNum) 61 | let column = CGFloat(offset / globalOptions.matrixNum) 62 | let x = space * (row) + row * pointWH 63 | let y = space * (column) + column * pointWH 64 | let rect = CGRect(x: x, y: y, width: pointWH, height: pointWH) 65 | let point = Point(frame: rect) 66 | point.tag = offset 67 | layer.addSublayer(point) 68 | } 69 | layer.masksToBounds = true 70 | } 71 | 72 | /// Draw connect-lines 73 | private func drawLines() { 74 | if points.isEmpty { return } 75 | let linePath = UIBezierPath() 76 | linePath.lineCapStyle = .round 77 | linePath.lineJoinStyle = .round 78 | lineLayer.strokeColor = globalOptions.connectLineColor.cgColor 79 | lineLayer.fillColor = nil 80 | lineLayer.lineWidth = globalOptions.connectLineWidth 81 | 82 | points.enumerated().forEach { (offset, element) in 83 | let pointCenter = element.position 84 | if offset == 0 { 85 | linePath.move(to: pointCenter) 86 | } else { 87 | linePath.addLine(to: pointCenter) 88 | } 89 | } 90 | 91 | if let current = currentPoint { 92 | linePath.addLine(to: current) 93 | } 94 | lineLayer.path = linePath.cgPath 95 | if globalOptions.connectLineStart == .center { 96 | layer.addSublayer(lineLayer) 97 | } else { 98 | layer.insertSublayer(lineLayer, at: 0) 99 | } 100 | } 101 | 102 | // MARK: - Touches 103 | override open func touchesBegan(_ touches: Set, with event: UIEvent?) { 104 | handleTouches(touches) 105 | } 106 | 107 | override open func touchesMoved(_ touches: Set, with event: UIEvent?) { 108 | handleTouches(touches) 109 | } 110 | 111 | override open func touchesEnded(_ touches: Set, with event: UIEvent?) { 112 | noMoreTouches() 113 | } 114 | 115 | override open func touchesCancelled(_ touches: Set, with event: UIEvent?) { 116 | noMoreTouches() 117 | } 118 | 119 | // MARK: - Deal touches 120 | 121 | /// Handle touches, add to points, get draw angle 122 | /// 123 | /// - Parameter touches: Set 124 | private func handleTouches(_ touches: Set) { 125 | guard !touches.isEmpty else { return } 126 | currentPoint = touches.first!.location(in: self) 127 | let location = touches.first!.location(in: self) 128 | if let point = point(by: location), !points.contains(point) { 129 | points.append(point) 130 | calAngle() 131 | if hasOpenTrack() == nil || hasOpenTrack() == true { 132 | point.selected = true 133 | } 134 | // send touch point to delegate 135 | guard let delegate = self.delegate else { return } 136 | delegate.sendTouchPoint(with: "\(point.tag)") 137 | } 138 | if hasOpenTrack() == nil || hasOpenTrack() == true { 139 | drawLines() 140 | } 141 | } 142 | 143 | /// Calculate angle for circle draw triangle 144 | private func calAngle() { 145 | let count = points.count 146 | if count > 1 { 147 | let after = points[count - 1] 148 | let before = points[count - 2] 149 | 150 | let after_x = after.frame.minX 151 | let after_y = after.frame.minY 152 | let before_x = before.frame.minX 153 | let before_y = before.frame.minY 154 | 155 | let absX = fabsf(Float(before_x - after_x)) 156 | let absY = fabsf(Float(before_y - after_y)) 157 | let abxZ = sqrtf(pow(absX, 2) + pow(absY, 2)) 158 | 159 | if before_x == after_x, before_y > after_y { 160 | before.angle = 0 161 | } else if before_x < after_x, before_y > after_y { 162 | before.angle = -CGFloat(asin(absX/abxZ)) 163 | } else if before_x < after_x, before_y == after_y { 164 | before.angle = -CGFloat(Double.pi) / 2 165 | } else if before_x < after_x, before_y < after_y { 166 | before.angle = -(CGFloat(Double.pi) / 2) - CGFloat(asin(absY/abxZ)) 167 | } else if before_x == after_x, before_y < after_y { 168 | before.angle = -CGFloat(Double.pi) 169 | } else if before_x > after_x, before_y < after_y { 170 | before.angle = -CGFloat(Double.pi) - CGFloat(asin(absX/abxZ)) 171 | } else if before_x > after_x, before_y == after_y { 172 | before.angle = -CGFloat(Double.pi * 1.5) 173 | } else if before_x > after_x, before_y > after_y { 174 | before.angle = -CGFloat(Double.pi * 1.5) - CGFloat(asin(absY/abxZ)) 175 | } 176 | } 177 | } 178 | 179 | /// Find point with location 180 | /// 181 | /// - Parameter location: CGPoint 182 | /// - Returns: Point 183 | private func point(by location: CGPoint) -> Point? { 184 | guard let sublayers = layer.sublayers else { return nil } 185 | for point in sublayers where point is Point { 186 | if point.frame.contains(location) { 187 | return point as? Point 188 | } 189 | } 190 | return nil 191 | } 192 | 193 | /// Deal no touches 194 | private func noMoreTouches() { 195 | currentPoint = points.last?.position 196 | points.forEach { (point) in 197 | point.selected = false 198 | point.angle = 9999 199 | } 200 | points.removeAll() 201 | lineLayer.removeFromSuperlayer() 202 | // tell delegate touch end 203 | guard let delegate = self.delegate else { return } 204 | delegate.touchesEnded() 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /Source/Core/EventDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EventDelegate.swift 3 | // GPassword 4 | // 5 | // Created by Jie Li on 10/5/18. 6 | // 7 | // Copyright (c) 2018 Jie Li 8 | // 9 | // Permission is hereby granted, free of charge, to any person obtaining a copy 10 | // of this software and associated documentation files (the "Software"), to deal 11 | // in the Software without restriction, including without limitation the rights 12 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | // copies of the Software, and to permit persons to whom the Software is 14 | // furnished to do so, subject to the following conditions: 15 | // 16 | // The above copyright notice and this permission notice shall be included in 17 | // all copies or substantial portions of the Software. 18 | // 19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 | // THE SOFTWARE. 26 | 27 | /// Protocol for handle touched-Points and deal with points 28 | public protocol GPasswordEventDelegate: class { 29 | 30 | /// Use to notice the gesture point, and consist of password 31 | /// 32 | /// - Parameter tag: String. identifer for point 33 | func sendTouchPoint(with tag: String) 34 | 35 | /// Use to notice gesture end 36 | func touchesEnded() 37 | } 38 | -------------------------------------------------------------------------------- /Source/Core/LockManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LockManager.swift 3 | // GPassword 4 | // 5 | // Created by Jie Li on 11/5/18. 6 | // 7 | // Copyright (c) 2018 Jie Li 8 | // 9 | // Permission is hereby granted, free of charge, to any person obtaining a copy 10 | // of this software and associated documentation files (the "Software"), to deal 11 | // in the Software without restriction, including without limitation the rights 12 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | // copies of the Software, and to permit persons to whom the Software is 14 | // furnished to do so, subject to the following conditions: 15 | // 16 | // The above copyright notice and this permission notice shall be included in 17 | // all copies or substantial portions of the Software. 18 | // 19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 | // THE SOFTWARE. 26 | 27 | import UIKit 28 | 29 | /// Key for password 30 | let PASSWORD_KEY = "gesture_password_key_" 31 | /// Key for control gesture password open or close 32 | let CONTROL_KEY = "gesture_control_" 33 | /// Key for control gesture password track open or close 34 | let CONTROL_TRACK_KEY = "gesture_conroltrack_" 35 | /// Key for record error num 36 | let ERROR_NUM_KEY = "gesture_error_" 37 | 38 | /// Responsible for manage gesture password 39 | class LockManager { 40 | /// Singleton 41 | static let `default`: LockManager = { 42 | return LockManager() 43 | }() 44 | 45 | private init() {} 46 | 47 | /// Options for gesture password 48 | var options = LockOptions() 49 | 50 | /// Storage for manage gesture password 51 | var storage: Storage = KeychainSwift() 52 | 53 | /// Save String item with key 54 | /// 55 | /// - Parameters: 56 | /// - password: String 57 | /// - key: String 58 | func save(string item: String, with key: String) { 59 | storage.gp_set(item, with: key) 60 | } 61 | 62 | /// Get String item with key 63 | /// 64 | /// - Parameter key: String 65 | /// - Returns: String Optional 66 | func getStringItem(with key: String) -> String? { 67 | return storage.gp_get(with: key) 68 | } 69 | 70 | /// Save Bool item with key 71 | /// 72 | /// - Parameters: 73 | /// - item: Bool 74 | /// - key: String 75 | func save(bool item: Bool, with key: String) { 76 | storage.gp_setBool(item, with: key) 77 | } 78 | 79 | /// Get Bool item with key 80 | /// 81 | /// - Parameter key: String 82 | /// - Returns: Bool Optional 83 | func getBoolItem(with key: String) -> Bool? { 84 | return storage.gp_getBool(with: key) 85 | } 86 | 87 | /// Remove item with key 88 | /// 89 | /// - Parameter key: String 90 | func removeItem(with key: String) { 91 | storage.gp_remove(with: key) 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /Source/Core/LockOptions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LockOptions.swift 3 | // GPassword 4 | // 5 | // Created by Jie Li on 26/4/18. 6 | // 7 | // Copyright (c) 2018 Jie Li 8 | // 9 | // Permission is hereby granted, free of charge, to any person obtaining a copy 10 | // of this software and associated documentation files (the "Software"), to deal 11 | // in the Software without restriction, including without limitation the rights 12 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | // copies of the Software, and to permit persons to whom the Software is 14 | // furnished to do so, subject to the following conditions: 15 | // 16 | // The above copyright notice and this permission notice shall be included in 17 | // all copies or substantial portions of the Software. 18 | // 19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 | // THE SOFTWARE. 26 | 27 | import UIKit 28 | 29 | /// LockOptions is options for user to adjust something about UI. 30 | /// call GPassword.config to config global 31 | public class LockOptions { 32 | 33 | /// Num for Matrix. eg: 3 is 3*3; 4 is 4*4 34 | /// you need to adjust pointSpace to fit this matrix 35 | public var matrixNum: Int = 3 36 | 37 | /// Lock normal style 38 | public var normalstyle: NormalStyle = .outerStroke 39 | 40 | /// Box background color 41 | public var boxBackgroundColor: UIColor = .white 42 | 43 | /// Connect line color 44 | public var connectLineColor: UIColor = UIColor(gpRGB: 0x6EBF60) 45 | 46 | /// Connect line width 47 | public var connectLineWidth: CGFloat = 4.0 48 | 49 | /// Connect line start position 50 | public var connectLineStart: LineStart = .border 51 | 52 | /// Point context line width 53 | public var pointLineWidth: CGFloat = 1.0 54 | 55 | /// Point inner circle selected color 56 | public var innerSelectedColor: UIColor = UIColor(gpRGB: 0x8ABF82) 57 | 58 | /// Point inner circle normal color 59 | public var innerNormalColor: UIColor = UIColor(gpRGB: 0xE2E3F2) 60 | 61 | /// Point outer circle normal fill color 62 | public var outerNormalColor: UIColor = .white 63 | 64 | /// Point outer circle selected fill color 65 | public var outerSelectedColor: UIColor = .white 66 | 67 | /// Point inner circle scale of Point 68 | public var scale: CGFloat = 0.3 69 | 70 | /// Point inner circle whether draw stroke, width according to pointLineWidth 71 | public var isInnerStroke: Bool = false 72 | 73 | /// Point outer circle whether draw stroke, width according to pointLineWidth 74 | public var isOuterStroke: Bool = true 75 | 76 | /// Point inner circle draw stroke color 77 | public var innerStrokeColor: UIColor = UIColor(gpRGB: 0x8ABF82) 78 | 79 | /// Point outer circle draw stroke color 80 | public var outerStrokeColor: UIColor = UIColor(gpRGB: 0x8ABF82) 81 | 82 | /// Space between points 83 | public var pointSpace: CGFloat = 30.0 84 | 85 | /// Point inner circle whether draw triangle 86 | public var isDrawTriangle: Bool = false 87 | 88 | /// Triangle background color 89 | public var triangleColor: UIColor = UIColor(gpRGB: 0x8ABF82) 90 | 91 | /// Triangle width 92 | public var triangleWidth: CGFloat = 10 93 | 94 | /// Triangle height 95 | public var triangleHeight: CGFloat = 7 96 | 97 | /// Offset between inner circle and triangle 98 | public var offsetInnerCircleAndTriangle: CGFloat = 4 99 | 100 | /// Suffix of key, for supporting muti accounts 101 | /// if you want to support muti accounts, you should first set it at config method 102 | public var keySuffix = "" 103 | 104 | /// Used to verify and modify 105 | public var maxErrorNum: Int = 5 106 | 107 | /// Store type 108 | public var storeType: StoreType = .userDefault { 109 | willSet { 110 | if storeType == .userDefault { 111 | LockManager.default.storage = UserDefaults() 112 | } else { 113 | LockManager.default.storage = KeychainSwift() 114 | } 115 | } 116 | } 117 | } 118 | 119 | /// Enum about draw connect line start from center or border 120 | public enum LineStart { 121 | case center, border 122 | } 123 | 124 | /// Enum about Point normal style 125 | public enum NormalStyle { 126 | case innerFill, outerStroke 127 | } 128 | 129 | /// Enum about store tool 130 | public enum StoreType { 131 | case keyChain, userDefault 132 | } 133 | -------------------------------------------------------------------------------- /Source/Core/Point.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Point.swift 3 | // GPassword 4 | // 5 | // Created by Jie Li on 8/5/18. 6 | // 7 | // Copyright (c) 2018 Jie Li 8 | // 9 | // Permission is hereby granted, free of charge, to any person obtaining a copy 10 | // of this software and associated documentation files (the "Software"), to deal 11 | // in the Software without restriction, including without limitation the rights 12 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | // copies of the Software, and to permit persons to whom the Software is 14 | // furnished to do so, subject to the following conditions: 15 | // 16 | // The above copyright notice and this permission notice shall be included in 17 | // all copies or substantial portions of the Software. 18 | // 19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 | // THE SOFTWARE. 26 | 27 | import UIKit 28 | 29 | class Point: CAShapeLayer { 30 | 31 | /// Contain all infos to draw 32 | fileprivate struct Shape { 33 | let fillColor: UIColor 34 | let rect: CGRect 35 | let stroke: Bool 36 | let strokeColor: UIColor 37 | } 38 | 39 | // MARK: - Properties 40 | 41 | /// Point selected 42 | var selected: Bool = false { 43 | didSet { 44 | drawAll() 45 | } 46 | } 47 | 48 | /// Angle used to draw triangle when isDrawTriangle is true 49 | var angle: CGFloat = 9999 { 50 | didSet { 51 | drawAll() 52 | } 53 | } 54 | 55 | /// The identifier for point 56 | var tag: Int = 0 57 | 58 | /// Contain draw infos of inner circle normal 59 | fileprivate lazy var innerNormal: Shape = { 60 | let rectWH = bounds.width * globalOptions.scale 61 | let rectXY = bounds.width * (1 - globalOptions.scale) * 0.5 62 | let rect = CGRect(x: rectXY, y: rectXY, width: rectWH, height: rectWH) 63 | let inner = Shape(fillColor: globalOptions.innerNormalColor, 64 | rect: rect, 65 | stroke: globalOptions.isInnerStroke, 66 | strokeColor: globalOptions.innerStrokeColor) 67 | return inner 68 | }() 69 | 70 | /// Contain draw infos of inner circle selected 71 | fileprivate lazy var innerSelected: Shape = { 72 | let rectWH = bounds.width * globalOptions.scale 73 | let rectXY = bounds.width * (1 - globalOptions.scale) * 0.5 74 | let rect = CGRect(x: rectXY, y: rectXY, width: rectWH, height: rectWH) 75 | let inner = Shape(fillColor: globalOptions.innerSelectedColor, 76 | rect: rect, 77 | stroke: globalOptions.isInnerStroke, 78 | strokeColor: globalOptions.innerStrokeColor) 79 | return inner 80 | }() 81 | 82 | /// Contain draw infos of inner circle normal 83 | fileprivate lazy var innerTriangle: Shape = { 84 | let rectWH = bounds.width * globalOptions.scale 85 | let rectXY = bounds.width * (1 - globalOptions.scale) * 0.5 86 | let rect = CGRect(x: rectXY, y: rectXY, width: rectWH, height: rectWH) 87 | let inner = Shape(fillColor: globalOptions.triangleColor, 88 | rect: rect, 89 | stroke: globalOptions.isInnerStroke, 90 | strokeColor: globalOptions.innerStrokeColor) 91 | return inner 92 | }() 93 | 94 | /// Contain draw infos of outer circle stroke 95 | fileprivate lazy var outerStroke: Shape = { 96 | let sizeWH = bounds.width - 2 * globalOptions.pointLineWidth 97 | let originXY = globalOptions.pointLineWidth 98 | let rect = CGRect(x: originXY, y: originXY, width: sizeWH, height: sizeWH) 99 | let outer = Shape(fillColor: globalOptions.outerNormalColor, 100 | rect: rect, 101 | stroke: globalOptions.isOuterStroke, 102 | strokeColor: globalOptions.outerStrokeColor) 103 | return outer 104 | }() 105 | 106 | /// Contain draw infos of outer circle selected 107 | fileprivate lazy var outerSelected: Shape = { 108 | let sizeWH = bounds.width - 2 * globalOptions.pointLineWidth 109 | let originXY = globalOptions.pointLineWidth 110 | let rect = CGRect(x: originXY, y: originXY, width: sizeWH, height: sizeWH) 111 | let outer = Shape(fillColor: globalOptions.outerSelectedColor, 112 | rect: rect, 113 | stroke: globalOptions.isOuterStroke, 114 | strokeColor: globalOptions.outerStrokeColor) 115 | return outer 116 | }() 117 | 118 | // MARK: - Lifecycle 119 | init(frame: CGRect) { 120 | super.init() 121 | self.frame = frame 122 | drawAll() 123 | } 124 | 125 | override init(layer: Any) { 126 | super.init(layer: layer) 127 | } 128 | 129 | required init?(coder aDecoder: NSCoder) { 130 | fatalError("init(coder:) has not been implemented") 131 | } 132 | 133 | // MARK: - Draw 134 | 135 | /// Draw all layers in point 136 | func drawAll() { 137 | sublayers?.removeAll() 138 | if selected { 139 | drawShape(outerSelected) 140 | drawShape(innerSelected) 141 | if globalOptions.isDrawTriangle { 142 | drawTriangle(innerTriangle) 143 | } 144 | } else { 145 | if globalOptions.normalstyle == .innerFill { 146 | drawShape(innerNormal) 147 | } else { 148 | drawShape(outerStroke) 149 | } 150 | } 151 | } 152 | 153 | /// Draw single layer in point 154 | /// 155 | /// - Parameter shape: Shape 156 | private func drawShape(_ shape: Shape) { 157 | let path = UIBezierPath(ovalIn: shape.rect) 158 | let shapeLayer = CAShapeLayer() 159 | shapeLayer.fillColor = shape.fillColor.cgColor 160 | if shape.stroke { 161 | shapeLayer.strokeColor = shape.strokeColor.cgColor 162 | } 163 | shapeLayer.path = path.cgPath 164 | addSublayer(shapeLayer) 165 | } 166 | 167 | /// Draw triangle according angle property 168 | /// 169 | /// - Parameter shape: Shape 170 | private func drawTriangle(_ shape: Shape) { 171 | if angle == 9999 { return } 172 | let triangleLayer = CAShapeLayer() 173 | let path = UIBezierPath() 174 | triangleLayer.fillColor = globalOptions.triangleColor.cgColor 175 | 176 | let width = globalOptions.triangleWidth 177 | let height = globalOptions.triangleHeight 178 | let topX = shape.rect.minX + shape.rect.width * 0.5 179 | let topY = shape.rect.minY + (shape.rect.width * 0.5 - height - globalOptions.offsetInnerCircleAndTriangle - shape.rect.height * 0.5) 180 | path.move(to: CGPoint(x: topX, y: topY)) 181 | let leftPointX = topX - width * 0.5 182 | let leftPointY = topY + height 183 | path.addLine(to: CGPoint(x: leftPointX, y: leftPointY)) 184 | let rightPointX = topX + width * 0.5 185 | path.addLine(to: CGPoint(x: rightPointX, y: leftPointY)) 186 | triangleLayer.path = path.cgPath 187 | 188 | // rotate 189 | var transform = CATransform3DIdentity 190 | transform = CATransform3DTranslate(transform, frame.width/2, frame.height/2, 0) 191 | transform = CATransform3DRotate(transform, angle, 0.0, 0.0, -1.0); 192 | transform = CATransform3DTranslate(transform, -frame.width/2, -frame.height/2, 0) 193 | triangleLayer.transform = transform 194 | addSublayer(triangleLayer) 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /Source/Extension/Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Extensions.swift 3 | // GPassword 4 | // 5 | // Created by Jie Li on 26/4/18. 6 | // 7 | // Copyright (c) 2018 Jie Li 8 | // 9 | // Permission is hereby granted, free of charge, to any person obtaining a copy 10 | // of this software and associated documentation files (the "Software"), to deal 11 | // in the Software without restriction, including without limitation the rights 12 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | // copies of the Software, and to permit persons to whom the Software is 14 | // furnished to do so, subject to the following conditions: 15 | // 16 | // The above copyright notice and this permission notice shall be included in 17 | // all copies or substantial portions of the Software. 18 | // 19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 | // THE SOFTWARE. 26 | 27 | import UIKit 28 | 29 | // MARK: - UIColor 30 | extension UIColor { 31 | convenience public init(gpRGB: UInt, alpha: CGFloat = 1.0) { 32 | self.init( 33 | red: CGFloat((gpRGB & 0xFF0000) >> 16) / 255.0, 34 | green: CGFloat((gpRGB & 0x00FF00) >> 8) / 255.0, 35 | blue: CGFloat(gpRGB & 0x0000FF) / 255.0, 36 | alpha: alpha 37 | ) 38 | } 39 | } 40 | 41 | // MARK: - CALayer 42 | extension CALayer { 43 | public func gp_shake() { 44 | let keyFrameAnimation = CAKeyframeAnimation(keyPath: "transform.translation.x") 45 | let s = 16 46 | keyFrameAnimation.values = [0, s, -s, 0] 47 | keyFrameAnimation.duration = 0.3 48 | keyFrameAnimation.repeatCount = 1 49 | add(keyFrameAnimation, forKey: "shake") 50 | } 51 | } 52 | 53 | // MARK: - String 54 | extension String { 55 | static public func gp_localized(_ key: String) -> String { 56 | return NSLocalizedString(key, comment: "") 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Source/GPassword.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GPassword.swift 3 | // GPassword 4 | // 5 | // Created by Jie Li on 26/4/18. 6 | // 7 | // Copyright (c) 2018 Jie Li 8 | // 9 | // Permission is hereby granted, free of charge, to any person obtaining a copy 10 | // of this software and associated documentation files (the "Software"), to deal 11 | // in the Software without restriction, including without limitation the rights 12 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | // copies of the Software, and to permit persons to whom the Software is 14 | // furnished to do so, subject to the following conditions: 15 | // 16 | // The above copyright notice and this permission notice shall be included in 17 | // all copies or substantial portions of the Software. 18 | // 19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 | // THE SOFTWARE. 26 | 27 | import Foundation 28 | 29 | public let globalOptions = LockManager.default.options 30 | 31 | public typealias ConfigOptionsCompletion = (_ options: LockOptions) -> Void 32 | 33 | /// Config lock view properties. eg: color title... 34 | /// 35 | /// - Parameter config: ConfigOptionsCompletion 36 | public func config(_ config: ConfigOptionsCompletion) { 37 | config(globalOptions) 38 | } 39 | 40 | // MARK: - Save get remove password 41 | 42 | /// Save Password 43 | /// 44 | /// - Parameters: 45 | /// - password: String 46 | public func save(password: String) { 47 | LockManager.default.save(string: password, with: PASSWORD_KEY + globalOptions.keySuffix) 48 | } 49 | 50 | /// Remove Password 51 | public func removePassword() { 52 | LockManager.default.removeItem(with: PASSWORD_KEY + globalOptions.keySuffix) 53 | } 54 | 55 | /// Get Password 56 | /// 57 | /// - Returns: String Optional 58 | public func getPassword() -> String? { 59 | return LockManager.default.getStringItem(with: PASSWORD_KEY + globalOptions.keySuffix) 60 | } 61 | 62 | // MARK: - Control gesture open close 63 | 64 | /// Whether open gesture password 65 | /// 66 | /// - Returns: Bool Optional 67 | public func hasOpenGesture() -> Bool? { 68 | return LockManager.default.getBoolItem(with: CONTROL_KEY + globalOptions.keySuffix) 69 | } 70 | 71 | /// Open gesture password 72 | public func openGesture() { 73 | LockManager.default.save(bool: true, with: CONTROL_KEY + globalOptions.keySuffix) 74 | } 75 | 76 | /// Close gesture password 77 | public func closeGesture() { 78 | LockManager.default.save(bool: false, with: CONTROL_KEY + globalOptions.keySuffix) 79 | } 80 | 81 | // MARK: - Control show selected-track or not 82 | 83 | /// Whether show points those had been selected 84 | /// 85 | /// - Returns: Bool Optional 86 | public func hasOpenTrack() -> Bool? { 87 | return LockManager.default.getBoolItem(with: CONTROL_TRACK_KEY + globalOptions.keySuffix) 88 | } 89 | 90 | /// Show points selected 91 | public func openTrack() { 92 | LockManager.default.save(bool: true, with: CONTROL_TRACK_KEY + globalOptions.keySuffix) 93 | } 94 | 95 | /// Hide points selected 96 | public func closeTrack() { 97 | LockManager.default.save(bool: false, with: CONTROL_TRACK_KEY + globalOptions.keySuffix) 98 | } 99 | 100 | // MARK: - Max error num 101 | 102 | /// Increase error num 103 | public func increaseErrorNum() { 104 | var num = 0 105 | let key = ERROR_NUM_KEY + globalOptions.keySuffix 106 | if let hasNum = getErrorNum(), hasNum < globalOptions.maxErrorNum { 107 | num = hasNum + 1 108 | } else { 109 | num = globalOptions.maxErrorNum 110 | } 111 | LockManager.default.save(string: String(num), with: key) 112 | } 113 | 114 | /// Get error num 115 | /// 116 | /// - Returns: Int Optional 117 | public func getErrorNum() -> Int? { 118 | let key = ERROR_NUM_KEY + globalOptions.keySuffix 119 | return Int(LockManager.default.getStringItem(with: key) ?? "0") 120 | } 121 | 122 | /// Remove error record 123 | public func removeErrorNum() { 124 | let key = ERROR_NUM_KEY + globalOptions.keySuffix 125 | LockManager.default.removeItem(with: key) 126 | } 127 | -------------------------------------------------------------------------------- /Source/Store/KeychainSwift.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KeychainSwift.swift 3 | // GPassword 4 | // 5 | // Created by Jie Li on 10/5/18. 6 | // 7 | // Copyright (c) 2018 Jie Li 8 | // 9 | // Permission is hereby granted, free of charge, to any person obtaining a copy 10 | // of this software and associated documentation files (the "Software"), to deal 11 | // in the Software without restriction, including without limitation the rights 12 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | // copies of the Software, and to permit persons to whom the Software is 14 | // furnished to do so, subject to the following conditions: 15 | // 16 | // The above copyright notice and this permission notice shall be included in 17 | // all copies or substantial portions of the Software. 18 | // 19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 | // THE SOFTWARE. 26 | // Thanks to https://github.com/evgenyneu/keychain-swift 27 | 28 | import Security 29 | import Foundation 30 | 31 | /** 32 | A collection of helper functions for saving text and data in the keychain. 33 | */ 34 | open class KeychainSwift { 35 | 36 | var lastQueryParameters: [String: Any]? // Used by the unit tests 37 | 38 | /// Contains result code from the last operation. Value is noErr (0) for a successful result. 39 | open var lastResultCode: OSStatus = noErr 40 | 41 | var keyPrefix = "" // Can be useful in test. 42 | 43 | /** 44 | Specify an access group that will be used to access keychain items. Access groups can be used to share keychain items between applications. When access group value is nil all application access groups are being accessed. Access group name is used by all functions: set, get, delete and clear. 45 | */ 46 | open var accessGroup: String? 47 | 48 | 49 | /** 50 | 51 | Specifies whether the items can be synchronized with other devices through iCloud. Setting this property to true will 52 | add the item to other devices with the `set` method and obtain synchronizable items with the `get` command. Deleting synchronizable items will remove them from all devices. In order for keychain synchronization to work the user must enable "Keychain" in iCloud settings. 53 | 54 | Does not work on macOS. 55 | 56 | */ 57 | open var synchronizable: Bool = false 58 | 59 | private let readLock = NSLock() 60 | 61 | /// Instantiate a KeychainSwift object 62 | public init() { } 63 | 64 | /** 65 | 66 | - parameter keyPrefix: a prefix that is added before the key in get/set methods. Note that `clear` method still clears everything from the Keychain. 67 | */ 68 | public init(keyPrefix: String) { 69 | self.keyPrefix = keyPrefix 70 | } 71 | 72 | /** 73 | 74 | Stores the text value in the keychain item under the given key. 75 | 76 | - parameter key: Key under which the text value is stored in the keychain. 77 | - parameter value: Text string to be written to the keychain. 78 | - parameter withAccess: Value that indicates when your app needs access to the text in the keychain item. By default the .AccessibleWhenUnlocked option is used that permits the data to be accessed only while the device is unlocked by the user. 79 | 80 | - returns: True if the text was successfully written to the keychain. 81 | */ 82 | @discardableResult 83 | open func set(_ value: String, forKey key: String, 84 | withAccess access: KeychainSwiftAccessOptions? = nil) -> Bool { 85 | 86 | if let value = value.data(using: String.Encoding.utf8) { 87 | return set(value, forKey: key, withAccess: access) 88 | } 89 | 90 | return false 91 | } 92 | 93 | /** 94 | 95 | Stores the data in the keychain item under the given key. 96 | 97 | - parameter key: Key under which the data is stored in the keychain. 98 | - parameter value: Data to be written to the keychain. 99 | - parameter withAccess: Value that indicates when your app needs access to the text in the keychain item. By default the .AccessibleWhenUnlocked option is used that permits the data to be accessed only while the device is unlocked by the user. 100 | 101 | - returns: True if the text was successfully written to the keychain. 102 | 103 | */ 104 | @discardableResult 105 | open func set(_ value: Data, forKey key: String, 106 | withAccess access: KeychainSwiftAccessOptions? = nil) -> Bool { 107 | 108 | delete(key) // Delete any existing key before saving it 109 | let accessible = access?.value ?? KeychainSwiftAccessOptions.defaultOption.value 110 | 111 | let prefixedKey = keyWithPrefix(key) 112 | 113 | var query: [String : Any] = [ 114 | KeychainSwiftConstants.klass : kSecClassGenericPassword, 115 | KeychainSwiftConstants.attrAccount : prefixedKey, 116 | KeychainSwiftConstants.valueData : value, 117 | KeychainSwiftConstants.accessible : accessible 118 | ] 119 | 120 | query = addAccessGroupWhenPresent(query) 121 | query = addSynchronizableIfRequired(query, addingItems: true) 122 | lastQueryParameters = query 123 | 124 | lastResultCode = SecItemAdd(query as CFDictionary, nil) 125 | 126 | return lastResultCode == noErr 127 | } 128 | 129 | /** 130 | Stores the boolean value in the keychain item under the given key. 131 | - parameter key: Key under which the value is stored in the keychain. 132 | - parameter value: Boolean to be written to the keychain. 133 | - parameter withAccess: Value that indicates when your app needs access to the value in the keychain item. By default the .AccessibleWhenUnlocked option is used that permits the data to be accessed only while the device is unlocked by the user. 134 | - returns: True if the value was successfully written to the keychain. 135 | */ 136 | @discardableResult 137 | open func set(_ value: Bool, forKey key: String, 138 | withAccess access: KeychainSwiftAccessOptions? = nil) -> Bool { 139 | 140 | let bytes: [UInt8] = value ? [1] : [0] 141 | let data = Data(bytes: bytes) 142 | 143 | return set(data, forKey: key, withAccess: access) 144 | } 145 | 146 | /** 147 | 148 | Retrieves the text value from the keychain that corresponds to the given key. 149 | 150 | - parameter key: The key that is used to read the keychain item. 151 | - returns: The text value from the keychain. Returns nil if unable to read the item. 152 | 153 | */ 154 | open func get(_ key: String) -> String? { 155 | if let data = getData(key) { 156 | 157 | if let currentString = String(data: data, encoding: .utf8) { 158 | return currentString 159 | } 160 | 161 | lastResultCode = -67853 // errSecInvalidEncoding 162 | } 163 | 164 | return nil 165 | } 166 | 167 | /** 168 | 169 | Retrieves the data from the keychain that corresponds to the given key. 170 | 171 | - parameter key: The key that is used to read the keychain item. 172 | - returns: The text value from the keychain. Returns nil if unable to read the item. 173 | 174 | */ 175 | open func getData(_ key: String) -> Data? { 176 | // The lock prevents the code to be run simlultaneously 177 | // from multiple threads which may result in crashing 178 | readLock.lock() 179 | defer { readLock.unlock() } 180 | 181 | let prefixedKey = keyWithPrefix(key) 182 | 183 | var query: [String: Any] = [ 184 | KeychainSwiftConstants.klass : kSecClassGenericPassword, 185 | KeychainSwiftConstants.attrAccount : prefixedKey, 186 | KeychainSwiftConstants.returnData : kCFBooleanTrue, 187 | KeychainSwiftConstants.matchLimit : kSecMatchLimitOne 188 | ] 189 | 190 | query = addAccessGroupWhenPresent(query) 191 | query = addSynchronizableIfRequired(query, addingItems: false) 192 | lastQueryParameters = query 193 | 194 | var result: AnyObject? 195 | 196 | lastResultCode = withUnsafeMutablePointer(to: &result) { 197 | SecItemCopyMatching(query as CFDictionary, UnsafeMutablePointer($0)) 198 | } 199 | 200 | if lastResultCode == noErr { return result as? Data } 201 | 202 | return nil 203 | } 204 | 205 | /** 206 | Retrieves the boolean value from the keychain that corresponds to the given key. 207 | - parameter key: The key that is used to read the keychain item. 208 | - returns: The boolean value from the keychain. Returns nil if unable to read the item. 209 | */ 210 | open func getBool(_ key: String) -> Bool? { 211 | guard let data = getData(key) else { return nil } 212 | guard let firstBit = data.first else { return nil } 213 | return firstBit == 1 214 | } 215 | 216 | /** 217 | Deletes the single keychain item specified by the key. 218 | 219 | - parameter key: The key that is used to delete the keychain item. 220 | - returns: True if the item was successfully deleted. 221 | 222 | */ 223 | @discardableResult 224 | open func delete(_ key: String) -> Bool { 225 | let prefixedKey = keyWithPrefix(key) 226 | 227 | var query: [String: Any] = [ 228 | KeychainSwiftConstants.klass : kSecClassGenericPassword, 229 | KeychainSwiftConstants.attrAccount : prefixedKey 230 | ] 231 | 232 | query = addAccessGroupWhenPresent(query) 233 | query = addSynchronizableIfRequired(query, addingItems: false) 234 | lastQueryParameters = query 235 | 236 | lastResultCode = SecItemDelete(query as CFDictionary) 237 | 238 | return lastResultCode == noErr 239 | } 240 | 241 | /** 242 | 243 | Deletes all Keychain items used by the app. Note that this method deletes all items regardless of the prefix settings used for initializing the class. 244 | 245 | - returns: True if the keychain items were successfully deleted. 246 | 247 | */ 248 | @discardableResult 249 | open func clear() -> Bool { 250 | var query: [String: Any] = [ kSecClass as String : kSecClassGenericPassword ] 251 | query = addAccessGroupWhenPresent(query) 252 | query = addSynchronizableIfRequired(query, addingItems: false) 253 | lastQueryParameters = query 254 | 255 | lastResultCode = SecItemDelete(query as CFDictionary) 256 | 257 | return lastResultCode == noErr 258 | } 259 | 260 | /// Returns the key with currently set prefix. 261 | func keyWithPrefix(_ key: String) -> String { 262 | return "\(keyPrefix)\(key)" 263 | } 264 | 265 | func addAccessGroupWhenPresent(_ items: [String: Any]) -> [String: Any] { 266 | guard let accessGroup = accessGroup else { return items } 267 | 268 | var result: [String: Any] = items 269 | result[KeychainSwiftConstants.accessGroup] = accessGroup 270 | return result 271 | } 272 | 273 | /** 274 | 275 | Adds kSecAttrSynchronizable: kSecAttrSynchronizableAny` item to the dictionary when the `synchronizable` property is true. 276 | 277 | - parameter items: The dictionary where the kSecAttrSynchronizable items will be added when requested. 278 | - parameter addingItems: Use `true` when the dictionary will be used with `SecItemAdd` method (adding a keychain item). For getting and deleting items, use `false`. 279 | 280 | - returns: the dictionary with kSecAttrSynchronizable item added if it was requested. Otherwise, it returns the original dictionary. 281 | 282 | */ 283 | func addSynchronizableIfRequired(_ items: [String: Any], addingItems: Bool) -> [String: Any] { 284 | if !synchronizable { return items } 285 | var result: [String: Any] = items 286 | result[KeychainSwiftConstants.attrSynchronizable] = addingItems == true ? true : kSecAttrSynchronizableAny 287 | return result 288 | } 289 | } 290 | 291 | 292 | // ---------------------------- 293 | // 294 | // TegKeychainConstants.swift 295 | // 296 | // ---------------------------- 297 | import Foundation 298 | import Security 299 | 300 | /// Constants used by the library 301 | public struct KeychainSwiftConstants { 302 | /// Specifies a Keychain access group. Used for sharing Keychain items between apps. 303 | public static var accessGroup: String { return toString(kSecAttrAccessGroup) } 304 | 305 | /** 306 | 307 | A value that indicates when your app needs access to the data in a keychain item. The default value is AccessibleWhenUnlocked. For a list of possible values, see KeychainSwiftAccessOptions. 308 | 309 | */ 310 | public static var accessible: String { return toString(kSecAttrAccessible) } 311 | 312 | /// Used for specifying a String key when setting/getting a Keychain value. 313 | public static var attrAccount: String { return toString(kSecAttrAccount) } 314 | 315 | /// Used for specifying synchronization of keychain items between devices. 316 | public static var attrSynchronizable: String { return toString(kSecAttrSynchronizable) } 317 | 318 | /// An item class key used to construct a Keychain search dictionary. 319 | public static var klass: String { return toString(kSecClass) } 320 | 321 | /// Specifies the number of values returned from the keychain. The library only supports single values. 322 | public static var matchLimit: String { return toString(kSecMatchLimit) } 323 | 324 | /// A return data type used to get the data from the Keychain. 325 | public static var returnData: String { return toString(kSecReturnData) } 326 | 327 | /// Used for specifying a value when setting a Keychain value. 328 | public static var valueData: String { return toString(kSecValueData) } 329 | 330 | static func toString(_ value: CFString) -> String { 331 | return value as String 332 | } 333 | } 334 | 335 | 336 | // ---------------------------- 337 | // 338 | // KeychainSwiftAccessOptions.swift 339 | // 340 | // ---------------------------- 341 | import Security 342 | 343 | /** 344 | These options are used to determine when a keychain item should be readable. The default value is AccessibleWhenUnlocked. 345 | */ 346 | public enum KeychainSwiftAccessOptions { 347 | 348 | /** 349 | 350 | The data in the keychain item can be accessed only while the device is unlocked by the user. 351 | 352 | This is recommended for items that need to be accessible only while the application is in the foreground. Items with this attribute migrate to a new device when using encrypted backups. 353 | 354 | This is the default value for keychain items added without explicitly setting an accessibility constant. 355 | 356 | */ 357 | case accessibleWhenUnlocked 358 | 359 | /** 360 | 361 | The data in the keychain item can be accessed only while the device is unlocked by the user. 362 | 363 | This is recommended for items that need to be accessible only while the application is in the foreground. Items with this attribute do not migrate to a new device. Thus, after restoring from a backup of a different device, these items will not be present. 364 | 365 | */ 366 | case accessibleWhenUnlockedThisDeviceOnly 367 | 368 | /** 369 | 370 | The data in the keychain item cannot be accessed after a restart until the device has been unlocked once by the user. 371 | 372 | After the first unlock, the data remains accessible until the next restart. This is recommended for items that need to be accessed by background applications. Items with this attribute migrate to a new device when using encrypted backups. 373 | 374 | */ 375 | case accessibleAfterFirstUnlock 376 | 377 | /** 378 | 379 | The data in the keychain item cannot be accessed after a restart until the device has been unlocked once by the user. 380 | 381 | After the first unlock, the data remains accessible until the next restart. This is recommended for items that need to be accessed by background applications. Items with this attribute do not migrate to a new device. Thus, after restoring from a backup of a different device, these items will not be present. 382 | 383 | */ 384 | case accessibleAfterFirstUnlockThisDeviceOnly 385 | 386 | /** 387 | 388 | The data in the keychain item can always be accessed regardless of whether the device is locked. 389 | 390 | This is not recommended for application use. Items with this attribute migrate to a new device when using encrypted backups. 391 | 392 | */ 393 | case accessibleAlways 394 | 395 | /** 396 | 397 | The data in the keychain can only be accessed when the device is unlocked. Only available if a passcode is set on the device. 398 | 399 | This is recommended for items that only need to be accessible while the application is in the foreground. Items with this attribute never migrate to a new device. After a backup is restored to a new device, these items are missing. No items can be stored in this class on devices without a passcode. Disabling the device passcode causes all items in this class to be deleted. 400 | 401 | */ 402 | case accessibleWhenPasscodeSetThisDeviceOnly 403 | 404 | /** 405 | 406 | The data in the keychain item can always be accessed regardless of whether the device is locked. 407 | 408 | This is not recommended for application use. Items with this attribute do not migrate to a new device. Thus, after restoring from a backup of a different device, these items will not be present. 409 | 410 | */ 411 | case accessibleAlwaysThisDeviceOnly 412 | 413 | static var defaultOption: KeychainSwiftAccessOptions { 414 | return .accessibleWhenUnlocked 415 | } 416 | 417 | var value: String { 418 | switch self { 419 | case .accessibleWhenUnlocked: 420 | return toString(kSecAttrAccessibleWhenUnlocked) 421 | 422 | case .accessibleWhenUnlockedThisDeviceOnly: 423 | return toString(kSecAttrAccessibleWhenUnlockedThisDeviceOnly) 424 | 425 | case .accessibleAfterFirstUnlock: 426 | return toString(kSecAttrAccessibleAfterFirstUnlock) 427 | 428 | case .accessibleAfterFirstUnlockThisDeviceOnly: 429 | return toString(kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly) 430 | 431 | case .accessibleAlways: 432 | return toString(kSecAttrAccessibleAlways) 433 | 434 | case .accessibleWhenPasscodeSetThisDeviceOnly: 435 | return toString(kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly) 436 | 437 | case .accessibleAlwaysThisDeviceOnly: 438 | return toString(kSecAttrAccessibleAlwaysThisDeviceOnly) 439 | } 440 | } 441 | 442 | func toString(_ value: CFString) -> String { 443 | return KeychainSwiftConstants.toString(value) 444 | } 445 | } 446 | -------------------------------------------------------------------------------- /Source/Store/Storage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Storagable.swift 3 | // GPassword 4 | // 5 | // Created by Jie Li on 8/5/18. 6 | // 7 | // Copyright (c) 2018 Jie Li 8 | // 9 | // Permission is hereby granted, free of charge, to any person obtaining a copy 10 | // of this software and associated documentation files (the "Software"), to deal 11 | // in the Software without restriction, including without limitation the rights 12 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | // copies of the Software, and to permit persons to whom the Software is 14 | // furnished to do so, subject to the following conditions: 15 | // 16 | // The above copyright notice and this permission notice shall be included in 17 | // all copies or substantial portions of the Software. 18 | // 19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 | // THE SOFTWARE. 26 | 27 | import Foundation 28 | 29 | // MARK: - Protocol for set/get/remove value 30 | protocol Storage { 31 | func gp_set(_ value: String, with key: String) 32 | func gp_get(with key: String) -> String? 33 | func gp_setBool(_ value: Bool, with key: String) 34 | func gp_getBool(with key: String) -> Bool? 35 | func gp_remove(with key: String) 36 | } 37 | 38 | 39 | // MARK: - UserDefaults 40 | extension UserDefaults: Storage { 41 | func gp_set(_ value: String, with key: String) { 42 | self.setValue(value, forKey: key) 43 | self.synchronize() 44 | } 45 | 46 | func gp_get(with key: String) -> String? { 47 | return self.string(forKey: key) 48 | } 49 | 50 | func gp_setBool(_ value: Bool, with key: String) { 51 | self.setValue(value, forKey: key) 52 | self.synchronize() 53 | } 54 | 55 | func gp_getBool(with key: String) -> Bool? { 56 | // do not call self.bool(forKey: key), if the key not set, 57 | // this method will return false, not nil 58 | return self.object(forKey: key) as? Bool 59 | } 60 | 61 | func gp_remove(with key: String) { 62 | self.removeObject(forKey: key) 63 | } 64 | } 65 | 66 | // MARK: - KeyChain 67 | extension KeychainSwift: Storage { 68 | func gp_set(_ value: String, with key: String) { 69 | self.set(value, forKey: key) 70 | } 71 | 72 | func gp_get(with key: String) -> String? { 73 | return self.get(key) 74 | } 75 | 76 | func gp_setBool(_ value: Bool, with key: String) { 77 | self.set(value, forKey: key) 78 | } 79 | 80 | func gp_getBool(with key: String) -> Bool? { 81 | return self.getBool(key) 82 | } 83 | 84 | func gp_remove(with key: String) { 85 | self.delete(key) 86 | } 87 | } 88 | --------------------------------------------------------------------------------