├── Fingered.xcodeproj ├── project.xcworkspace │ └── contents.xcworkspacedata ├── xcuserdata │ └── chris.xcuserdatad │ │ └── xcschemes │ │ └── xcschememanagement.plist └── project.pbxproj ├── Fingered ├── KeychainWrapper.h ├── Fingered.h ├── Fingerable.swift ├── Info.plist ├── Ring.swift ├── Hand.swift └── KeychainWrapper.m ├── FingeredTests ├── Info.plist └── FingeredTests.swift ├── License.txt ├── .gitignore └── README.md /Fingered.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Fingered/KeychainWrapper.h: -------------------------------------------------------------------------------- 1 | // 2 | // KeychainWrapper.h 3 | // Apple's Keychain Services Programming Guide 4 | // 5 | // Created by Tim Mitra on 11/17/14. 6 | // Copyright (c) 2014 Apple. All rights reserved. 7 | // 8 | 9 | #import 10 | #import 11 | 12 | @interface KeychainWrapper : NSObject 13 | 14 | - (void)setObject:(id)inObject forKey:(id)key; 15 | - (id)objectForKey:(id)key; 16 | - (void)writeToKeychain; 17 | 18 | @end 19 | -------------------------------------------------------------------------------- /Fingered/Fingered.h: -------------------------------------------------------------------------------- 1 | // 2 | // Fingered.h 3 | // Fingered 4 | // 5 | // Created by Chris on 12/12/2016. 6 | // Copyright © 2016 Nodes. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | //! Project version number for Fingered. 12 | FOUNDATION_EXPORT double FingeredVersionNumber; 13 | 14 | //! Project version string for Fingered. 15 | FOUNDATION_EXPORT const unsigned char FingeredVersionString[]; 16 | 17 | // In this header, you should import all the public headers of your framework using statements like #import 18 | 19 | #import "KeychainWrapper.h" 20 | -------------------------------------------------------------------------------- /Fingered/Fingerable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Fingerable.swift 3 | // Fingered 4 | // 5 | // Created by Chris on 12/12/2016. 6 | // Copyright © 2016 Nodes. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// Your user model should conform to this protocol so that it can be saved in the keychain. 12 | public protocol Fingerable { 13 | /// The identifier to use for your user (name, id, email address, however your models are uniquely identified). 14 | var fingerIdentifier: String { get } 15 | 16 | /// Any extra data you may need to store and retreive when using TouchID in your application. 17 | var fingerUserInfo: [String: AnyObject]? { get } 18 | } 19 | -------------------------------------------------------------------------------- /Fingered.xcodeproj/xcuserdata/chris.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | Fingered.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | SuppressBuildableAutocreation 14 | 15 | 83B5DC921DFEB48500AD38E7 16 | 17 | primary 18 | 19 | 20 | 83B5DC9B1DFEB48500AD38E7 21 | 22 | primary 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /FingeredTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /Fingered/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | NSPrincipalClass 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /FingeredTests/FingeredTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FingeredTests.swift 3 | // FingeredTests 4 | // 5 | // Created by Chris on 12/12/2016. 6 | // Copyright © 2016 Nodes. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import Fingered 11 | 12 | class FingeredTests: XCTestCase { 13 | 14 | override func setUp() { 15 | super.setUp() 16 | // Put setup code here. This method is called before the invocation of each test method in the class. 17 | } 18 | 19 | override func tearDown() { 20 | // Put teardown code here. This method is called after the invocation of each test method in the class. 21 | super.tearDown() 22 | } 23 | 24 | func testExample() { 25 | // This is an example of a functional test case. 26 | // Use XCTAssert and related functions to verify your tests produce the correct results. 27 | } 28 | 29 | func testPerformanceExample() { 30 | // This is an example of a performance test case. 31 | self.measure { 32 | // Put the code you want to measure the time of here. 33 | } 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /License.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Nodes Agency - iOS 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 | -------------------------------------------------------------------------------- /.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 | *.xccheckout 22 | *.moved-aside 23 | *.xcuserstate 24 | *.xcscmblueprint 25 | 26 | ## Obj-C/Swift specific 27 | *.hmap 28 | *.ipa 29 | 30 | ## Playgrounds 31 | timeline.xctimeline 32 | playground.xcworkspace 33 | 34 | # Swift Package Manager 35 | # 36 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 37 | # Packages/ 38 | .build/ 39 | 40 | # CocoaPods 41 | # 42 | # We recommend against adding the Pods directory to your .gitignore. However 43 | # you should judge for yourself, the pros and cons are mentioned at: 44 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 45 | # 46 | # Pods/ 47 | 48 | # Carthage 49 | # 50 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 51 | # Carthage/Checkouts 52 | Carthage/ 53 | #Carthage/Build 54 | 55 | # fastlane 56 | # 57 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 58 | # screenshots whenever they are needed. 59 | # For more information about the recommended setup visit: 60 | # https://github.com/fastlane/fastlane/blob/master/docs/Gitignore.md 61 | 62 | fastlane/report.xml 63 | fastlane/screenshots 64 | -------------------------------------------------------------------------------- /Fingered/Ring.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Ring.swift 3 | // Fingered 4 | // 5 | // Created by Chris on 12/12/2016. 6 | // Copyright © 2016 Nodes. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | 12 | /// The ring is the metaphor that represents the stored user info. The ring 13 | /// can only be placed on one finger, and that finger will be the one that 14 | /// is stored. 15 | 16 | public struct Ring { 17 | 18 | private static let keychainWrapper = KeychainWrapper() 19 | 20 | /// The stored finger information contained in a tuple 21 | public typealias RingFinger = (identifier: String, password: String, userInfo: [String: AnyObject]?) 22 | 23 | /// Identifier of the currently stored Finger. Nil if none exist. 24 | internal static var currentFingerIdentifier: String? { 25 | return keychainWrapper.object(forKey: IdentifierKey) as? String 26 | } 27 | 28 | 29 | /// Full details of the currently stored Finger. Does not return optional data, so `currentFingerIdentifier` 30 | /// should be checked first to ensure a finger exists. 31 | 32 | internal static var currentFullFinger: RingFinger { 33 | let id = keychainWrapper.object(forKey: IdentifierKey) as? String ?? "" 34 | let pass = keychainWrapper.object(forKey: PasswordKey) as? String ?? "" 35 | let userInfo = keychainWrapper.object(forKey: UserInfoKey) as? [String: AnyObject] 36 | return (id, pass, userInfo) 37 | } 38 | 39 | /// True if the ring is already claimed by a finger. 40 | internal static var isClaimed: Bool { 41 | return UserDefaults.standard.bool(forKey: Ring.IsClaimed) //TODO: User something better than UserDefaults? 42 | } 43 | 44 | 45 | /// Places the ring on a new finger. Old details will be overwritten, if they exist. 46 | /// 47 | /// - parameter finger: The finger to place the ring on. 48 | 49 | internal static func placeOn(finger: Fingerable, withVow password: String) { 50 | keychainWrapper.setObject(finger.fingerIdentifier, forKey: Ring.IdentifierKey) 51 | keychainWrapper.setObject(password, forKey: Ring.PasswordKey) 52 | keychainWrapper.setObject(finger.fingerUserInfo, forKey: Ring.UserInfoKey) 53 | 54 | UserDefaults.standard.set(true, forKey: Ring.IsClaimed) 55 | keychainWrapper.writeToKeychain() 56 | } 57 | } 58 | 59 | /// Constants used as Keychain/UserDefaults keys 60 | extension Ring { 61 | internal static let IdentifierKey = "FingeredIdentifierKey" 62 | internal static let PasswordKey = "FingeredPasswordKey" 63 | internal static let UserInfoKey = "FingeredUserInfoKey" 64 | 65 | internal static let IsClaimed = "FingeredIsClaimedKey" 66 | } 67 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Fingered 👈 2 | A Protocol-based Touch ID wrapper written in Swift. Touch ID implementation is simple, but all of the extra logic around the actual use case requires a lot of work. Fingered simplifies this down to 3 calls and one protocol conformance. 3 | 4 | ## 📝 Requirements 5 | * iOS 8.0+ 6 | * Swift 3.0+ 7 | 8 | ## 📦 Installation 9 | 10 | ### Carthage 11 | ~~~bash 12 | github "nodes-ios/Fingered" 13 | ~~~ 14 | 15 | ### Cocoapods and SPM 16 | Coming soon 17 | 18 | ## 💻 Usage 19 | 20 | There are three main entities in Fingered: A *Finger* 👈, the *Hand* ✋, and the *Ring* 💍. A Finger is something that conforms to `Fingerable`. The Hand is what that **hand**les all of the interaction between a Finger, the Ring, and the app. The Ring is a metaphor for the stored credentials- Only one Finger can have the Ring, and it is this Finger that will be stored in the keychain for use with TouchID. 21 | 22 | First, you will need your user object to conform to the `Fingerable` protocol. This only requires one computed property, a unique identifier for your model. There is also an optional dictionary if you need to store additional information in keychain beyond the username and password. 23 | 24 | Next, there are three methods to be aware of that are provided by the Hand ✋. 25 | 26 | When a user logs in to your app successfully for the first time, you can check to see if they can be saved in the keychain for use with TouchID. Call the `proposeWithRingFor(finger:) throws` method to ensure that this user qualifies. If this returns `true`, you should prompt your user if they want to enable TouchID use. 27 | **NOTE**: This will only return true *once* per user, so that they are not asked repeatedly every time they log in. 28 | 29 | If the previous method returns true and your user accepts whatever UI message you present them, you can store their credentials by calling `placeRingOnFinger(finger: withVow: withReason: completion:)`, with the "vow" as the password the user has just used to log in with. This will prompt them to verify their TouchID fingerprint, and afterwards store the credentials in the keychain. 30 | 31 | The last piece to implement is the actual TouchID login. In your login view's viewWillAppear(animated:) (or whenever you want to attempt a TouchID login), call `verifyRingFinger(finger: withReason: completion:)` which will return a `RingFinger` tuple with the user's stored credentials if it was successful. Note that you will need to provide your own way of storing and retrieving previously logged in user identifiers. 32 | 33 | ## 👥 Credits 34 | Made with ❤️ at [Nodes](http://nodesagency.com). 35 | 36 | ## 📄 License 37 | **Fingered** is available under the MIT license. See the [LICENSE](https://github.com/nodes-ios/Fingered/blob/master/License.txt) file for more info. -------------------------------------------------------------------------------- /Fingered/Hand.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Hand.swift 3 | // Fingered 4 | // 5 | // Created by Chris on 12/12/2016. 6 | // Copyright © 2016 Nodes. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import LocalAuthentication 11 | 12 | /// The Hand is the main HANDler of the Finger and Ring. 13 | public struct Hand { 14 | 15 | private static let context = LAContext() 16 | 17 | 18 | /// Checks to see if the ring is currentlyl on the provided finger. If the ring is 19 | /// not on any fingers, it will return false. 20 | /// 21 | /// - parameter finger: Check to see if the ring is on this finger. 22 | 23 | private static func ringIsOn(finger: Fingerable) -> Bool { 24 | return Ring.currentFingerIdentifier == finger.fingerIdentifier 25 | } 26 | 27 | 28 | /// Verifies the TouchID status if the finger has the ring. Call this method whenever you want to retrieve 29 | /// the credentials to log your user in. 30 | /// 31 | /// - parameter finger: The user to attempt to log in with. 32 | /// - parameter reason: The message to display in the TouchID alert view. 33 | /// - parameter completion: Called when the fingerprint scan was a success, or if an error occurs. 34 | /// - parameter ringFinger: The full details of the stored user, nil if there was an error. 35 | /// - parameter error: Error details if the verification was unsuccessful. 36 | 37 | public static func verifyRingFinger(finger: Fingerable, withReason reason: String = "", completion: @escaping (_ ringFinger: Ring.RingFinger?, _ error: FingerError?) -> ()) { 38 | if !ringIsOn(finger: finger) { 39 | completion(nil, .noRingOnFinger) 40 | } 41 | 42 | if Hand.context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: nil) { 43 | Hand.context.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, localizedReason: reason, reply: { (success, error) in 44 | if success { 45 | completion(Ring.currentFullFinger, nil) 46 | } 47 | else if let error = error { 48 | completion(nil, .evaluationError(error: error)) 49 | } 50 | else { 51 | completion(nil, .unknown) 52 | } 53 | }) 54 | } 55 | else { 56 | completion(nil, .cannotEvaluateBiometrics) 57 | } 58 | } 59 | 60 | /// Checks various conditions to ensure that the ring can be placed on a given finger. 61 | /// If this returns true, you should display something to your user letting them know that 62 | /// TouchID is available, and/or ask them if they would like to use it. 63 | /// NOTE: This method will only return true ONCE per user. 64 | /// 65 | /// - parameter finger: The user to check validity of. 66 | /// - throws: A `FingerError` based on the issue that prevents the ring from being placed on the given finger. 67 | /// - returns: `true` if the ring can be placed 68 | 69 | public static func proposeWithRingFor(finger: Fingerable) throws -> Bool { 70 | if hasAsked(finger: finger) { 71 | throw FingerError.alreadyAsked 72 | } 73 | else if Ring.isClaimed { 74 | throw FingerError.ringIsTaken 75 | } 76 | else if Hand.context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: nil) { 77 | UserDefaults.standard.set(true, forKey: Hand.HasAskedKey + "_\(finger.fingerIdentifier)") 78 | return true 79 | } 80 | else { 81 | throw FingerError.cannotEvaluateBiometrics 82 | } 83 | } 84 | 85 | /// Finally stores the user data in the keychain by placing the ring on the given finger. Call this 86 | /// method after the user has agreed to store their info in keychain for use with TouchID. 87 | /// 88 | /// - parameter finger: The user to store in keychain. 89 | /// - parameter password: The password to store along with the user. 90 | /// - parameter reason: The message to display in the TouchID alert view. 91 | /// - parameter completion: Called with the method has completed. 92 | /// - parameter success: True if the user was stored in the keychain. 93 | /// - parameter error: Error details if any part of the storage failed. 94 | public static func placeRingOnFinger(finger: Fingerable, withVow password: String, withReason reason: String = "", completion: @escaping (_ success: Bool, _ error: FingerError?) -> ()) { 95 | if Hand.context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: nil) { 96 | Hand.context.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, localizedReason: reason, reply: { (success, error) in 97 | if success { 98 | Ring.placeOn(finger: finger, withVow: password) 99 | completion(true, nil) 100 | } 101 | else if let error = error { 102 | completion(false, .evaluationError(error: error)) 103 | } 104 | else { 105 | completion(false, .unknown) 106 | } 107 | }) 108 | } 109 | else { 110 | completion(false, .cannotEvaluateBiometrics) 111 | } 112 | } 113 | /// Checks to see if this user has already been asked about storing 114 | private static func hasAsked(finger: Fingerable) -> Bool { 115 | return UserDefaults.standard.bool(forKey: Hand.HasAskedKey + "_\(finger.fingerIdentifier)") 116 | } 117 | } 118 | 119 | extension Hand { 120 | public enum FingerError: Error { 121 | /// There is no ring on this finger 122 | case noRingOnFinger 123 | /// Most likely this is a device without TouchID support 124 | case cannotEvaluateBiometrics 125 | /// There was an error with the `evaluatePolicy` call, check the nested error for more info 126 | case evaluationError(error: Error) 127 | /// Something unknown occurred 128 | case unknown 129 | /// This finger has already been asked 130 | case alreadyAsked 131 | /// The ring is already on another finger 132 | case ringIsTaken 133 | } 134 | } 135 | 136 | extension Hand { 137 | static let HasAskedKey = "FingeredHasAskedKey" 138 | } 139 | -------------------------------------------------------------------------------- /Fingered/KeychainWrapper.m: -------------------------------------------------------------------------------- 1 | // 2 | // KeychainWrapper.h 3 | // Apple's Keychain Services Programming Guide 4 | // 5 | // Created by Tim Mitra on 11/17/14. 6 | // Copyright (c) 2014 Apple. All rights reserved. 7 | // 8 | 9 | #import "KeychainWrapper.h" 10 | 11 | //Unique string used to identify the keychain item: 12 | static const UInt8 kKeychainItemIdentifier[] = "com.apple.dts.KeychainUI\0"; 13 | 14 | @interface KeychainWrapper () 15 | 16 | @property (nonatomic, strong) NSMutableDictionary *keychainData; 17 | @property (nonatomic, strong) NSMutableDictionary *genericPasswordQuery; 18 | 19 | @end 20 | 21 | @interface KeychainWrapper (PrivateMethods) 22 | 23 | //The following two methods translate dictionaries between the format used by 24 | // the view controller (NSString *) and the Keychain Services API: 25 | - (NSMutableDictionary *)secItemFormatToDictionary:(NSDictionary *)dictionaryToConvert; 26 | - (NSMutableDictionary *)dictionaryToSecItemFormat:(NSDictionary *)dictionaryToConvert; 27 | // Method used to write data to the keychain: 28 | - (void)writeToKeychain; 29 | 30 | @end 31 | 32 | 33 | @implementation KeychainWrapper 34 | 35 | - (instancetype)init 36 | { 37 | self = [super init]; 38 | 39 | if (self) { 40 | 41 | 42 | OSStatus keychainErr = noErr; 43 | // Set up the keychain search dictionary: 44 | _genericPasswordQuery = [[NSMutableDictionary alloc] init]; 45 | 46 | // This keychain item is a generic password. 47 | [_genericPasswordQuery setObject:(__bridge id)kSecClassGenericPassword 48 | forKey:(__bridge id)kSecClass]; 49 | 50 | // The kSecAttrGeneric attribute is used to store a unique string that is used 51 | // to easily identify and find this keychain item. The string is first 52 | // converted to an NSData object: 53 | NSData *keychainItemID = [NSData dataWithBytes:kKeychainItemIdentifier 54 | length:strlen((const char *)kKeychainItemIdentifier)]; 55 | [_genericPasswordQuery setObject:keychainItemID forKey:(__bridge id)kSecAttrGeneric]; 56 | 57 | // Return the attributes of the first match only: 58 | [_genericPasswordQuery setObject:(__bridge id)kSecMatchLimitOne forKey:(__bridge id)kSecMatchLimit]; 59 | 60 | // Return the attributes of the keychain item (the password is 61 | // acquired in the secItemFormatToDictionary: method): 62 | [_genericPasswordQuery setObject:(__bridge id)kCFBooleanTrue 63 | forKey:(__bridge id)kSecReturnAttributes]; 64 | 65 | //Initialize the dictionary used to hold return data from the keychain: 66 | CFMutableDictionaryRef outDictionary = nil; 67 | // If the keychain item exists, return the attributes of the item: 68 | keychainErr = SecItemCopyMatching((__bridge CFDictionaryRef)_genericPasswordQuery, 69 | (CFTypeRef *)&outDictionary); 70 | 71 | if (keychainErr == noErr) { 72 | // Convert the data dictionary into the format used by the view controller: 73 | self.keychainData = [self secItemFormatToDictionary:(__bridge_transfer NSMutableDictionary *)outDictionary]; 74 | } else if (keychainErr == errSecItemNotFound) { 75 | // Put default values into the keychain if no matching 76 | // keychain item is found: 77 | [self resetKeychainItem]; 78 | if (outDictionary) CFRelease(outDictionary); 79 | } else { 80 | // Any other error is unexpected. 81 | NSAssert(NO, @"Serious error.\n"); 82 | if (outDictionary) CFRelease(outDictionary); 83 | } 84 | } 85 | 86 | return self; 87 | } 88 | 89 | // Implement the mySetObject:forKey method, which writes attributes to the keychain: 90 | - (void)setObject:(id)inObject forKey:(id)key 91 | { 92 | if (inObject == nil) return; 93 | id currentObject = [_keychainData objectForKey:key]; 94 | if (![currentObject isEqual:inObject]) 95 | { 96 | [_keychainData setObject:inObject forKey:key]; 97 | [self writeToKeychain]; 98 | } 99 | } 100 | 101 | // Implement the myObjectForKey: method, which reads an attribute value from a dictionary: 102 | - (id)objectForKey:(id)key 103 | { 104 | return [_keychainData objectForKey:key]; 105 | } 106 | 107 | // Reset the values in the keychain item, or create a new item if it 108 | // doesn't already exist: 109 | 110 | - (void)resetKeychainItem 111 | { 112 | if (!_keychainData) //Allocate the keychainData dictionary if it doesn't exist yet. 113 | { 114 | self.keychainData = [[NSMutableDictionary alloc] init]; 115 | } 116 | else if (_keychainData) 117 | { 118 | // Format the data in the keychainData dictionary into the format needed for a query 119 | // and put it into tmpDictionary: 120 | NSMutableDictionary *tmpDictionary = 121 | [self dictionaryToSecItemFormat:_keychainData]; 122 | // Delete the keychain item in preparation for resetting the values: 123 | OSStatus errorcode = SecItemDelete((__bridge CFDictionaryRef)tmpDictionary); 124 | NSAssert(errorcode == noErr, @"Problem deleting current keychain item." ); 125 | } 126 | 127 | // Default generic data for Keychain Item: 128 | [_keychainData setObject:@"Item label" forKey:(__bridge id)kSecAttrLabel]; 129 | [_keychainData setObject:@"Item description" forKey:(__bridge id)kSecAttrDescription]; 130 | [_keychainData setObject:@"Account" forKey:(__bridge id)kSecAttrAccount]; 131 | [_keychainData setObject:@"Service" forKey:(__bridge id)kSecAttrService]; 132 | [_keychainData setObject:@"Your comment here." forKey:(__bridge id)kSecAttrComment]; 133 | [_keychainData setObject:@"password" forKey:(__bridge id)kSecValueData]; 134 | } 135 | 136 | 137 | // Implement the dictionaryToSecItemFormat: method, which takes the attributes that 138 | // you want to add to the keychain item and sets up a dictionary in the format 139 | // needed by Keychain Services: 140 | - (NSMutableDictionary *)dictionaryToSecItemFormat:(NSDictionary *)dictionaryToConvert 141 | { 142 | // This method must be called with a properly populated dictionary 143 | // containing all the right key/value pairs for a keychain item search. 144 | 145 | // Create the return dictionary: 146 | NSMutableDictionary *returnDictionary = 147 | [NSMutableDictionary dictionaryWithDictionary:dictionaryToConvert]; 148 | 149 | // Add the keychain item class and the generic attribute: 150 | NSData *keychainItemID = [NSData dataWithBytes:kKeychainItemIdentifier 151 | length:strlen((const char *)kKeychainItemIdentifier)]; 152 | [returnDictionary setObject:keychainItemID forKey:(__bridge id)kSecAttrGeneric]; 153 | [returnDictionary setObject:(__bridge id)kSecClassGenericPassword forKey:(__bridge id)kSecClass]; 154 | 155 | // Convert the password NSString to NSData to fit the API paradigm: 156 | NSString *passwordString = [dictionaryToConvert objectForKey:(__bridge id)kSecValueData]; 157 | [returnDictionary setObject:[passwordString dataUsingEncoding:NSUTF8StringEncoding] 158 | forKey:(__bridge id)kSecValueData]; 159 | return returnDictionary; 160 | } 161 | 162 | // Implement the secItemFormatToDictionary: method, which takes the attribute dictionary 163 | // obtained from the keychain item, acquires the password from the keychain, and 164 | // adds it to the attribute dictionary: 165 | - (NSMutableDictionary *)secItemFormatToDictionary:(NSDictionary *)dictionaryToConvert 166 | { 167 | // This method must be called with a properly populated dictionary 168 | // containing all the right key/value pairs for the keychain item. 169 | 170 | // Create a return dictionary populated with the attributes: 171 | NSMutableDictionary *returnDictionary = [NSMutableDictionary 172 | dictionaryWithDictionary:dictionaryToConvert]; 173 | 174 | // To acquire the password data from the keychain item, 175 | // first add the search key and class attribute required to obtain the password: 176 | [returnDictionary setObject:(__bridge id)kCFBooleanTrue forKey:(__bridge id)kSecReturnData]; 177 | [returnDictionary setObject:(__bridge id)kSecClassGenericPassword forKey:(__bridge id)kSecClass]; 178 | // Then call Keychain Services to get the password: 179 | CFDataRef passwordData = NULL; 180 | OSStatus keychainError = noErr; // 181 | keychainError = SecItemCopyMatching((__bridge CFDictionaryRef)returnDictionary, 182 | (CFTypeRef *)&passwordData); 183 | if (keychainError == noErr) 184 | { 185 | // Remove the kSecReturnData key; we don't need it anymore: 186 | [returnDictionary removeObjectForKey:(__bridge id)kSecReturnData]; 187 | 188 | // Convert the password to an NSString and add it to the return dictionary: 189 | NSString *password = [[NSString alloc] initWithBytes:[(__bridge_transfer NSData *)passwordData bytes] 190 | length:[(__bridge NSData *)passwordData length] encoding:NSUTF8StringEncoding]; 191 | [returnDictionary setObject:password forKey:(__bridge id)kSecValueData]; 192 | } 193 | // Don't do anything if nothing is found. 194 | else if (keychainError == errSecItemNotFound) { 195 | NSAssert(NO, @"Nothing was found in the keychain.\n"); 196 | if (passwordData) CFRelease(passwordData); 197 | } 198 | // Any other error is unexpected. 199 | else 200 | { 201 | NSAssert(NO, @"Serious error.\n"); 202 | if (passwordData) CFRelease(passwordData); 203 | } 204 | 205 | return returnDictionary; 206 | } 207 | 208 | // could be in a class 209 | - (void)writeToKeychain { 210 | 211 | CFDictionaryRef attributes = nil; 212 | NSMutableDictionary *updateItem = nil; 213 | 214 | // If the keychain item already exists, modify it: 215 | if (SecItemCopyMatching((__bridge CFDictionaryRef)_genericPasswordQuery, 216 | (CFTypeRef *)&attributes) == noErr) 217 | { 218 | // First, get the attributes returned from the keychain and add them to the 219 | // dictionary that controls the update: 220 | updateItem = [NSMutableDictionary dictionaryWithDictionary:(__bridge_transfer NSDictionary *)attributes]; 221 | 222 | // Second, get the class value from the generic password query dictionary and 223 | // add it to the updateItem dictionary: 224 | [updateItem setObject:[_genericPasswordQuery objectForKey:(__bridge id)kSecClass] 225 | forKey:(__bridge id)kSecClass]; 226 | 227 | // Finally, set up the dictionary that contains new values for the attributes: 228 | NSMutableDictionary *tempCheck = [self dictionaryToSecItemFormat:_keychainData]; 229 | //Remove the class--it's not a keychain attribute: 230 | [tempCheck removeObjectForKey:(__bridge id)kSecClass]; 231 | 232 | // You can update only a single keychain item at a time. 233 | OSStatus errorcode = SecItemUpdate( 234 | (__bridge CFDictionaryRef)updateItem, 235 | (__bridge CFDictionaryRef)tempCheck); 236 | NSAssert(errorcode == noErr, @"Couldn't update the Keychain Item." ); 237 | } 238 | else 239 | { 240 | // No previous item found; add the new item. 241 | // The new value was added to the keychainData dictionary in the mySetObject routine, 242 | // and the other values were added to the keychainData dictionary previously. 243 | // No pointer to the newly-added items is needed, so pass NULL for the second parameter: 244 | OSStatus errorcode = SecItemAdd( 245 | (__bridge CFDictionaryRef)[self dictionaryToSecItemFormat:_keychainData], 246 | NULL); 247 | NSAssert(errorcode == noErr, @"Couldn't add the Keychain Item." ); 248 | if (attributes) CFRelease(attributes); 249 | } 250 | 251 | } 252 | 253 | 254 | @end 255 | -------------------------------------------------------------------------------- /Fingered.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 83B5DC9D1DFEB48500AD38E7 /* Fingered.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 83B5DC931DFEB48500AD38E7 /* Fingered.framework */; }; 11 | 83B5DCA21DFEB48500AD38E7 /* FingeredTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83B5DCA11DFEB48500AD38E7 /* FingeredTests.swift */; }; 12 | 83B5DCA41DFEB48500AD38E7 /* Fingered.h in Headers */ = {isa = PBXBuildFile; fileRef = 83B5DC961DFEB48500AD38E7 /* Fingered.h */; settings = {ATTRIBUTES = (Public, ); }; }; 13 | 83B5DCAF1DFEB49800AD38E7 /* Fingerable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83B5DCAE1DFEB49800AD38E7 /* Fingerable.swift */; }; 14 | 83B5DCB11DFEB5DA00AD38E7 /* Hand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83B5DCB01DFEB5DA00AD38E7 /* Hand.swift */; }; 15 | 83B5DCB31DFEB77400AD38E7 /* Ring.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83B5DCB21DFEB77400AD38E7 /* Ring.swift */; }; 16 | 83B5DCB71DFEB85B00AD38E7 /* KeychainWrapper.h in Headers */ = {isa = PBXBuildFile; fileRef = 83B5DCB51DFEB85B00AD38E7 /* KeychainWrapper.h */; settings = {ATTRIBUTES = (Public, ); }; }; 17 | 83B5DCB81DFEB85B00AD38E7 /* KeychainWrapper.m in Sources */ = {isa = PBXBuildFile; fileRef = 83B5DCB61DFEB85B00AD38E7 /* KeychainWrapper.m */; }; 18 | /* End PBXBuildFile section */ 19 | 20 | /* Begin PBXContainerItemProxy section */ 21 | 83B5DC9E1DFEB48500AD38E7 /* PBXContainerItemProxy */ = { 22 | isa = PBXContainerItemProxy; 23 | containerPortal = 83B5DC8A1DFEB48500AD38E7 /* Project object */; 24 | proxyType = 1; 25 | remoteGlobalIDString = 83B5DC921DFEB48500AD38E7; 26 | remoteInfo = Fingered; 27 | }; 28 | /* End PBXContainerItemProxy section */ 29 | 30 | /* Begin PBXFileReference section */ 31 | 83B5DC931DFEB48500AD38E7 /* Fingered.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Fingered.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 32 | 83B5DC961DFEB48500AD38E7 /* Fingered.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Fingered.h; sourceTree = ""; }; 33 | 83B5DC971DFEB48500AD38E7 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 34 | 83B5DC9C1DFEB48500AD38E7 /* FingeredTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = FingeredTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 35 | 83B5DCA11DFEB48500AD38E7 /* FingeredTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FingeredTests.swift; sourceTree = ""; }; 36 | 83B5DCA31DFEB48500AD38E7 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 37 | 83B5DCAE1DFEB49800AD38E7 /* Fingerable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Fingerable.swift; sourceTree = ""; }; 38 | 83B5DCB01DFEB5DA00AD38E7 /* Hand.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Hand.swift; sourceTree = ""; }; 39 | 83B5DCB21DFEB77400AD38E7 /* Ring.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Ring.swift; sourceTree = ""; }; 40 | 83B5DCB51DFEB85B00AD38E7 /* KeychainWrapper.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = KeychainWrapper.h; sourceTree = ""; }; 41 | 83B5DCB61DFEB85B00AD38E7 /* KeychainWrapper.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = KeychainWrapper.m; sourceTree = ""; }; 42 | /* End PBXFileReference section */ 43 | 44 | /* Begin PBXFrameworksBuildPhase section */ 45 | 83B5DC8F1DFEB48500AD38E7 /* Frameworks */ = { 46 | isa = PBXFrameworksBuildPhase; 47 | buildActionMask = 2147483647; 48 | files = ( 49 | ); 50 | runOnlyForDeploymentPostprocessing = 0; 51 | }; 52 | 83B5DC991DFEB48500AD38E7 /* Frameworks */ = { 53 | isa = PBXFrameworksBuildPhase; 54 | buildActionMask = 2147483647; 55 | files = ( 56 | 83B5DC9D1DFEB48500AD38E7 /* Fingered.framework in Frameworks */, 57 | ); 58 | runOnlyForDeploymentPostprocessing = 0; 59 | }; 60 | /* End PBXFrameworksBuildPhase section */ 61 | 62 | /* Begin PBXGroup section */ 63 | 83B5DC891DFEB48500AD38E7 = { 64 | isa = PBXGroup; 65 | children = ( 66 | 83B5DC951DFEB48500AD38E7 /* Fingered */, 67 | 83B5DCA01DFEB48500AD38E7 /* FingeredTests */, 68 | 83B5DC941DFEB48500AD38E7 /* Products */, 69 | ); 70 | sourceTree = ""; 71 | }; 72 | 83B5DC941DFEB48500AD38E7 /* Products */ = { 73 | isa = PBXGroup; 74 | children = ( 75 | 83B5DC931DFEB48500AD38E7 /* Fingered.framework */, 76 | 83B5DC9C1DFEB48500AD38E7 /* FingeredTests.xctest */, 77 | ); 78 | name = Products; 79 | sourceTree = ""; 80 | }; 81 | 83B5DC951DFEB48500AD38E7 /* Fingered */ = { 82 | isa = PBXGroup; 83 | children = ( 84 | 83B5DCAD1DFEB49000AD38E7 /* Source */, 85 | 83B5DC961DFEB48500AD38E7 /* Fingered.h */, 86 | 83B5DC971DFEB48500AD38E7 /* Info.plist */, 87 | ); 88 | path = Fingered; 89 | sourceTree = ""; 90 | }; 91 | 83B5DCA01DFEB48500AD38E7 /* FingeredTests */ = { 92 | isa = PBXGroup; 93 | children = ( 94 | 83B5DCA11DFEB48500AD38E7 /* FingeredTests.swift */, 95 | 83B5DCA31DFEB48500AD38E7 /* Info.plist */, 96 | ); 97 | path = FingeredTests; 98 | sourceTree = ""; 99 | }; 100 | 83B5DCAD1DFEB49000AD38E7 /* Source */ = { 101 | isa = PBXGroup; 102 | children = ( 103 | 83B5DCB41DFEB85200AD38E7 /* External */, 104 | 83B5DCAE1DFEB49800AD38E7 /* Fingerable.swift */, 105 | 83B5DCB01DFEB5DA00AD38E7 /* Hand.swift */, 106 | 83B5DCB21DFEB77400AD38E7 /* Ring.swift */, 107 | ); 108 | name = Source; 109 | sourceTree = ""; 110 | }; 111 | 83B5DCB41DFEB85200AD38E7 /* External */ = { 112 | isa = PBXGroup; 113 | children = ( 114 | 83B5DCB51DFEB85B00AD38E7 /* KeychainWrapper.h */, 115 | 83B5DCB61DFEB85B00AD38E7 /* KeychainWrapper.m */, 116 | ); 117 | name = External; 118 | sourceTree = ""; 119 | }; 120 | /* End PBXGroup section */ 121 | 122 | /* Begin PBXHeadersBuildPhase section */ 123 | 83B5DC901DFEB48500AD38E7 /* Headers */ = { 124 | isa = PBXHeadersBuildPhase; 125 | buildActionMask = 2147483647; 126 | files = ( 127 | 83B5DCB71DFEB85B00AD38E7 /* KeychainWrapper.h in Headers */, 128 | 83B5DCA41DFEB48500AD38E7 /* Fingered.h in Headers */, 129 | ); 130 | runOnlyForDeploymentPostprocessing = 0; 131 | }; 132 | /* End PBXHeadersBuildPhase section */ 133 | 134 | /* Begin PBXNativeTarget section */ 135 | 83B5DC921DFEB48500AD38E7 /* Fingered */ = { 136 | isa = PBXNativeTarget; 137 | buildConfigurationList = 83B5DCA71DFEB48500AD38E7 /* Build configuration list for PBXNativeTarget "Fingered" */; 138 | buildPhases = ( 139 | 83B5DC8E1DFEB48500AD38E7 /* Sources */, 140 | 83B5DC8F1DFEB48500AD38E7 /* Frameworks */, 141 | 83B5DC901DFEB48500AD38E7 /* Headers */, 142 | 83B5DC911DFEB48500AD38E7 /* Resources */, 143 | ); 144 | buildRules = ( 145 | ); 146 | dependencies = ( 147 | ); 148 | name = Fingered; 149 | productName = Fingered; 150 | productReference = 83B5DC931DFEB48500AD38E7 /* Fingered.framework */; 151 | productType = "com.apple.product-type.framework"; 152 | }; 153 | 83B5DC9B1DFEB48500AD38E7 /* FingeredTests */ = { 154 | isa = PBXNativeTarget; 155 | buildConfigurationList = 83B5DCAA1DFEB48500AD38E7 /* Build configuration list for PBXNativeTarget "FingeredTests" */; 156 | buildPhases = ( 157 | 83B5DC981DFEB48500AD38E7 /* Sources */, 158 | 83B5DC991DFEB48500AD38E7 /* Frameworks */, 159 | 83B5DC9A1DFEB48500AD38E7 /* Resources */, 160 | ); 161 | buildRules = ( 162 | ); 163 | dependencies = ( 164 | 83B5DC9F1DFEB48500AD38E7 /* PBXTargetDependency */, 165 | ); 166 | name = FingeredTests; 167 | productName = FingeredTests; 168 | productReference = 83B5DC9C1DFEB48500AD38E7 /* FingeredTests.xctest */; 169 | productType = "com.apple.product-type.bundle.unit-test"; 170 | }; 171 | /* End PBXNativeTarget section */ 172 | 173 | /* Begin PBXProject section */ 174 | 83B5DC8A1DFEB48500AD38E7 /* Project object */ = { 175 | isa = PBXProject; 176 | attributes = { 177 | LastSwiftUpdateCheck = 0810; 178 | LastUpgradeCheck = 0810; 179 | ORGANIZATIONNAME = Nodes; 180 | TargetAttributes = { 181 | 83B5DC921DFEB48500AD38E7 = { 182 | CreatedOnToolsVersion = 8.1; 183 | DevelopmentTeam = HW27H6H98R; 184 | LastSwiftMigration = 0810; 185 | ProvisioningStyle = Automatic; 186 | }; 187 | 83B5DC9B1DFEB48500AD38E7 = { 188 | CreatedOnToolsVersion = 8.1; 189 | DevelopmentTeam = HW27H6H98R; 190 | ProvisioningStyle = Automatic; 191 | }; 192 | }; 193 | }; 194 | buildConfigurationList = 83B5DC8D1DFEB48500AD38E7 /* Build configuration list for PBXProject "Fingered" */; 195 | compatibilityVersion = "Xcode 3.2"; 196 | developmentRegion = English; 197 | hasScannedForEncodings = 0; 198 | knownRegions = ( 199 | en, 200 | ); 201 | mainGroup = 83B5DC891DFEB48500AD38E7; 202 | productRefGroup = 83B5DC941DFEB48500AD38E7 /* Products */; 203 | projectDirPath = ""; 204 | projectRoot = ""; 205 | targets = ( 206 | 83B5DC921DFEB48500AD38E7 /* Fingered */, 207 | 83B5DC9B1DFEB48500AD38E7 /* FingeredTests */, 208 | ); 209 | }; 210 | /* End PBXProject section */ 211 | 212 | /* Begin PBXResourcesBuildPhase section */ 213 | 83B5DC911DFEB48500AD38E7 /* Resources */ = { 214 | isa = PBXResourcesBuildPhase; 215 | buildActionMask = 2147483647; 216 | files = ( 217 | ); 218 | runOnlyForDeploymentPostprocessing = 0; 219 | }; 220 | 83B5DC9A1DFEB48500AD38E7 /* Resources */ = { 221 | isa = PBXResourcesBuildPhase; 222 | buildActionMask = 2147483647; 223 | files = ( 224 | ); 225 | runOnlyForDeploymentPostprocessing = 0; 226 | }; 227 | /* End PBXResourcesBuildPhase section */ 228 | 229 | /* Begin PBXSourcesBuildPhase section */ 230 | 83B5DC8E1DFEB48500AD38E7 /* Sources */ = { 231 | isa = PBXSourcesBuildPhase; 232 | buildActionMask = 2147483647; 233 | files = ( 234 | 83B5DCB11DFEB5DA00AD38E7 /* Hand.swift in Sources */, 235 | 83B5DCB81DFEB85B00AD38E7 /* KeychainWrapper.m in Sources */, 236 | 83B5DCB31DFEB77400AD38E7 /* Ring.swift in Sources */, 237 | 83B5DCAF1DFEB49800AD38E7 /* Fingerable.swift in Sources */, 238 | ); 239 | runOnlyForDeploymentPostprocessing = 0; 240 | }; 241 | 83B5DC981DFEB48500AD38E7 /* Sources */ = { 242 | isa = PBXSourcesBuildPhase; 243 | buildActionMask = 2147483647; 244 | files = ( 245 | 83B5DCA21DFEB48500AD38E7 /* FingeredTests.swift in Sources */, 246 | ); 247 | runOnlyForDeploymentPostprocessing = 0; 248 | }; 249 | /* End PBXSourcesBuildPhase section */ 250 | 251 | /* Begin PBXTargetDependency section */ 252 | 83B5DC9F1DFEB48500AD38E7 /* PBXTargetDependency */ = { 253 | isa = PBXTargetDependency; 254 | target = 83B5DC921DFEB48500AD38E7 /* Fingered */; 255 | targetProxy = 83B5DC9E1DFEB48500AD38E7 /* PBXContainerItemProxy */; 256 | }; 257 | /* End PBXTargetDependency section */ 258 | 259 | /* Begin XCBuildConfiguration section */ 260 | 83B5DCA51DFEB48500AD38E7 /* Debug */ = { 261 | isa = XCBuildConfiguration; 262 | buildSettings = { 263 | ALWAYS_SEARCH_USER_PATHS = NO; 264 | CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES = YES; 265 | CLANG_ANALYZER_NONNULL = YES; 266 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 267 | CLANG_CXX_LIBRARY = "libc++"; 268 | CLANG_ENABLE_MODULES = YES; 269 | CLANG_ENABLE_OBJC_ARC = YES; 270 | CLANG_WARN_BOOL_CONVERSION = YES; 271 | CLANG_WARN_CONSTANT_CONVERSION = YES; 272 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 273 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 274 | CLANG_WARN_EMPTY_BODY = YES; 275 | CLANG_WARN_ENUM_CONVERSION = YES; 276 | CLANG_WARN_INFINITE_RECURSION = YES; 277 | CLANG_WARN_INT_CONVERSION = YES; 278 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 279 | CLANG_WARN_SUSPICIOUS_MOVES = YES; 280 | CLANG_WARN_UNREACHABLE_CODE = YES; 281 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 282 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 283 | COPY_PHASE_STRIP = NO; 284 | CURRENT_PROJECT_VERSION = 1; 285 | DEBUG_INFORMATION_FORMAT = dwarf; 286 | ENABLE_STRICT_OBJC_MSGSEND = YES; 287 | ENABLE_TESTABILITY = YES; 288 | GCC_C_LANGUAGE_STANDARD = gnu99; 289 | GCC_DYNAMIC_NO_PIC = NO; 290 | GCC_NO_COMMON_BLOCKS = YES; 291 | GCC_OPTIMIZATION_LEVEL = 0; 292 | GCC_PREPROCESSOR_DEFINITIONS = ( 293 | "DEBUG=1", 294 | "$(inherited)", 295 | ); 296 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 297 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 298 | GCC_WARN_UNDECLARED_SELECTOR = YES; 299 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 300 | GCC_WARN_UNUSED_FUNCTION = YES; 301 | GCC_WARN_UNUSED_VARIABLE = YES; 302 | IPHONEOS_DEPLOYMENT_TARGET = 10.1; 303 | MTL_ENABLE_DEBUG_INFO = YES; 304 | ONLY_ACTIVE_ARCH = YES; 305 | SDKROOT = iphoneos; 306 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 307 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 308 | TARGETED_DEVICE_FAMILY = "1,2"; 309 | VERSIONING_SYSTEM = "apple-generic"; 310 | VERSION_INFO_PREFIX = ""; 311 | }; 312 | name = Debug; 313 | }; 314 | 83B5DCA61DFEB48500AD38E7 /* Release */ = { 315 | isa = XCBuildConfiguration; 316 | buildSettings = { 317 | ALWAYS_SEARCH_USER_PATHS = NO; 318 | CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES = YES; 319 | CLANG_ANALYZER_NONNULL = YES; 320 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 321 | CLANG_CXX_LIBRARY = "libc++"; 322 | CLANG_ENABLE_MODULES = YES; 323 | CLANG_ENABLE_OBJC_ARC = YES; 324 | CLANG_WARN_BOOL_CONVERSION = YES; 325 | CLANG_WARN_CONSTANT_CONVERSION = YES; 326 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 327 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 328 | CLANG_WARN_EMPTY_BODY = YES; 329 | CLANG_WARN_ENUM_CONVERSION = YES; 330 | CLANG_WARN_INFINITE_RECURSION = YES; 331 | CLANG_WARN_INT_CONVERSION = YES; 332 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 333 | CLANG_WARN_SUSPICIOUS_MOVES = YES; 334 | CLANG_WARN_UNREACHABLE_CODE = YES; 335 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 336 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 337 | COPY_PHASE_STRIP = NO; 338 | CURRENT_PROJECT_VERSION = 1; 339 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 340 | ENABLE_NS_ASSERTIONS = NO; 341 | ENABLE_STRICT_OBJC_MSGSEND = YES; 342 | GCC_C_LANGUAGE_STANDARD = gnu99; 343 | GCC_NO_COMMON_BLOCKS = YES; 344 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 345 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 346 | GCC_WARN_UNDECLARED_SELECTOR = YES; 347 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 348 | GCC_WARN_UNUSED_FUNCTION = YES; 349 | GCC_WARN_UNUSED_VARIABLE = YES; 350 | IPHONEOS_DEPLOYMENT_TARGET = 10.1; 351 | MTL_ENABLE_DEBUG_INFO = NO; 352 | SDKROOT = iphoneos; 353 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 354 | TARGETED_DEVICE_FAMILY = "1,2"; 355 | VALIDATE_PRODUCT = YES; 356 | VERSIONING_SYSTEM = "apple-generic"; 357 | VERSION_INFO_PREFIX = ""; 358 | }; 359 | name = Release; 360 | }; 361 | 83B5DCA81DFEB48500AD38E7 /* Debug */ = { 362 | isa = XCBuildConfiguration; 363 | buildSettings = { 364 | CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES = YES; 365 | CLANG_ENABLE_MODULES = YES; 366 | CODE_SIGN_IDENTITY = ""; 367 | DEFINES_MODULE = YES; 368 | DEVELOPMENT_TEAM = HW27H6H98R; 369 | DYLIB_COMPATIBILITY_VERSION = 1; 370 | DYLIB_CURRENT_VERSION = 1; 371 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 372 | INFOPLIST_FILE = Fingered/Info.plist; 373 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 374 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 375 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 376 | PRODUCT_BUNDLE_IDENTIFIER = com.nodes.Fingered; 377 | PRODUCT_NAME = "$(TARGET_NAME)"; 378 | SKIP_INSTALL = YES; 379 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 380 | SWIFT_VERSION = 3.0.1; 381 | }; 382 | name = Debug; 383 | }; 384 | 83B5DCA91DFEB48500AD38E7 /* Release */ = { 385 | isa = XCBuildConfiguration; 386 | buildSettings = { 387 | CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES = YES; 388 | CLANG_ENABLE_MODULES = YES; 389 | CODE_SIGN_IDENTITY = ""; 390 | DEFINES_MODULE = YES; 391 | DEVELOPMENT_TEAM = HW27H6H98R; 392 | DYLIB_COMPATIBILITY_VERSION = 1; 393 | DYLIB_CURRENT_VERSION = 1; 394 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 395 | INFOPLIST_FILE = Fingered/Info.plist; 396 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 397 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 398 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 399 | PRODUCT_BUNDLE_IDENTIFIER = com.nodes.Fingered; 400 | PRODUCT_NAME = "$(TARGET_NAME)"; 401 | SKIP_INSTALL = YES; 402 | SWIFT_VERSION = 3.0.1; 403 | }; 404 | name = Release; 405 | }; 406 | 83B5DCAB1DFEB48500AD38E7 /* Debug */ = { 407 | isa = XCBuildConfiguration; 408 | buildSettings = { 409 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 410 | DEVELOPMENT_TEAM = HW27H6H98R; 411 | INFOPLIST_FILE = FingeredTests/Info.plist; 412 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 413 | PRODUCT_BUNDLE_IDENTIFIER = com.nodes.FingeredTests; 414 | PRODUCT_NAME = "$(TARGET_NAME)"; 415 | SWIFT_VERSION = 3.0; 416 | }; 417 | name = Debug; 418 | }; 419 | 83B5DCAC1DFEB48500AD38E7 /* Release */ = { 420 | isa = XCBuildConfiguration; 421 | buildSettings = { 422 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 423 | DEVELOPMENT_TEAM = HW27H6H98R; 424 | INFOPLIST_FILE = FingeredTests/Info.plist; 425 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 426 | PRODUCT_BUNDLE_IDENTIFIER = com.nodes.FingeredTests; 427 | PRODUCT_NAME = "$(TARGET_NAME)"; 428 | SWIFT_VERSION = 3.0; 429 | }; 430 | name = Release; 431 | }; 432 | /* End XCBuildConfiguration section */ 433 | 434 | /* Begin XCConfigurationList section */ 435 | 83B5DC8D1DFEB48500AD38E7 /* Build configuration list for PBXProject "Fingered" */ = { 436 | isa = XCConfigurationList; 437 | buildConfigurations = ( 438 | 83B5DCA51DFEB48500AD38E7 /* Debug */, 439 | 83B5DCA61DFEB48500AD38E7 /* Release */, 440 | ); 441 | defaultConfigurationIsVisible = 0; 442 | defaultConfigurationName = Release; 443 | }; 444 | 83B5DCA71DFEB48500AD38E7 /* Build configuration list for PBXNativeTarget "Fingered" */ = { 445 | isa = XCConfigurationList; 446 | buildConfigurations = ( 447 | 83B5DCA81DFEB48500AD38E7 /* Debug */, 448 | 83B5DCA91DFEB48500AD38E7 /* Release */, 449 | ); 450 | defaultConfigurationIsVisible = 0; 451 | }; 452 | 83B5DCAA1DFEB48500AD38E7 /* Build configuration list for PBXNativeTarget "FingeredTests" */ = { 453 | isa = XCConfigurationList; 454 | buildConfigurations = ( 455 | 83B5DCAB1DFEB48500AD38E7 /* Debug */, 456 | 83B5DCAC1DFEB48500AD38E7 /* Release */, 457 | ); 458 | defaultConfigurationIsVisible = 0; 459 | }; 460 | /* End XCConfigurationList section */ 461 | }; 462 | rootObject = 83B5DC8A1DFEB48500AD38E7 /* Project object */; 463 | } 464 | --------------------------------------------------------------------------------