├── 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 |
--------------------------------------------------------------------------------