-
Notifications
You must be signed in to change notification settings - Fork 0
Adding Keychain and FormBox #102
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| // | ||
| // KFormBox.swift | ||
| // KamaalSwift | ||
| // | ||
| // Created by Kamaal M Farah on 10/12/25. | ||
| // | ||
|
|
||
| import SwiftUI | ||
|
|
||
| public struct KFormBox<Content: View>: View { | ||
| private let title: String | ||
| private let minSize: CGSize | ||
|
|
||
| @ViewBuilder private let content: () -> Content | ||
|
|
||
| public init(title: String, minSize: CGSize, content: @escaping () -> Content) { | ||
| self.title = title | ||
| self.minSize = minSize | ||
| self.content = content | ||
| } | ||
|
|
||
| public init( | ||
| localizedTitle: LocalizedStringResource, | ||
| bundle: Bundle, | ||
| minSize: CGSize, | ||
| content: @escaping () -> Content, | ||
| ) { | ||
| self.init( | ||
| title: NSLocalizedString(localizedTitle.key, bundle: bundle, comment: ""), | ||
| minSize: minSize, | ||
| content: content, | ||
| ) | ||
| } | ||
|
|
||
| public var body: some View { | ||
| VStack { | ||
| GroupBox { | ||
| VStack { | ||
| Text(self.title) | ||
| .font(.title2) | ||
| .ktakeWidthEagerly(alignment: .leading) | ||
| self.content() | ||
| } | ||
| .padding(.vertical, 16) | ||
| .padding(.horizontal, 8) | ||
| } | ||
| .frame(width: self.minSize.width / 1.1) | ||
| } | ||
| .padding(.horizontal, 8) | ||
| .frame(minWidth: self.minSize.width + 8, minHeight: self.minSize.height + 8) | ||
| .navigationTitle(Text(self.title)) | ||
| } | ||
| } | ||
|
|
||
| #Preview { | ||
| KFormBox(title: "FormBox", minSize: .init(width: 200, height: 200)) { | ||
| Text("Gello") | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,159 @@ | ||
| // | ||
| // Keychain.swift | ||
| // KamaalSwift | ||
| // | ||
| // Created by Kamaal M Farah on 10/12/25. | ||
| // | ||
|
|
||
| import Security | ||
| import Foundation | ||
|
|
||
| /// Errors that can occur when setting data in the keychain. | ||
| public enum KeychainSetErrors: Error { | ||
| /// A general error occurred with the underlying Security framework. | ||
| case generalError(status: OSStatus) | ||
| } | ||
|
|
||
| /// Errors that can occur when retrieving data from the keychain. | ||
| public enum KeychainGetErrors: Error { | ||
| /// A general error occurred with the underlying Security framework. | ||
| case generalError(status: OSStatus) | ||
| } | ||
|
|
||
| /// Errors that can occur when deleting data from the keychain. | ||
| public enum KeychainDeleteErrors: Error { | ||
| /// A general error occurred with the underlying Security framework. | ||
| case generalError(status: OSStatus) | ||
| } | ||
|
|
||
| /// A utility for securely storing, retrieving, and deleting sensitive data in the system keychain. | ||
| /// | ||
| /// `Keychain` provides a simple interface for working with the iOS/macOS keychain to store | ||
| /// sensitive information like passwords, tokens, and other credentials. All data is stored | ||
| /// with the accessibility level `kSecAttrAccessibleWhenUnlocked`, meaning it's only accessible | ||
| /// when the device is unlocked. | ||
| public enum Keychain { | ||
| /// Stores data securely in the keychain for the specified key. | ||
| /// | ||
| /// If an item with the same key already exists, it will be updated with the new data. | ||
| /// The data is stored with accessibility level `kSecAttrAccessibleWhenUnlocked`. | ||
| /// | ||
| /// - Parameters: | ||
| /// - data: The data to store in the keychain. | ||
| /// - key: A unique identifier for the keychain item. | ||
| /// | ||
| /// - Returns: A `Result` indicating success or failure with a `KeychainSetErrors` error. | ||
| /// | ||
| /// - Example: | ||
| /// ```swift | ||
| /// let password = "mySecurePassword".data(using: .utf8)! | ||
| /// let result = Keychain.set(password, forKey: "user.password") | ||
| /// | ||
| /// switch result { | ||
| /// case .success: | ||
| /// print("Password saved successfully") | ||
| /// case .failure(let error): | ||
| /// print("Failed to save password: \(error)") | ||
| /// } | ||
| /// ``` | ||
| @discardableResult | ||
| public static func set(_ data: Data, forKey key: String) -> Result<Void, KeychainSetErrors> { | ||
| let query = | ||
| [ | ||
| kSecClass: kSecClassGenericPassword, | ||
| kSecAttrAccount: key, | ||
| kSecValueData: data, | ||
| kSecAttrAccessible: kSecAttrAccessibleWhenUnlocked, | ||
| ] as CFDictionary | ||
| let status = SecItemAdd(query, nil) | ||
| guard status != errSecDuplicateItem else { return self.update(data, forKey: key) } | ||
| guard status == errSecSuccess else { return .failure(.generalError(status: status)) } | ||
|
|
||
| return .success(()) | ||
| } | ||
|
|
||
| /// Retrieves data from the keychain for the specified key. | ||
| /// | ||
| /// If no item exists for the given key, the method returns `nil` wrapped in a success result. | ||
| /// | ||
| /// - Parameter key: The unique identifier for the keychain item to retrieve. | ||
| /// | ||
| /// - Returns: A `Result` containing the retrieved data (or `nil` if not found), or a `KeychainGetErrors` error. | ||
| /// | ||
| /// - Example: | ||
| /// ```swift | ||
| /// let result = Keychain.get(forKey: "user.password") | ||
| /// | ||
| /// switch result { | ||
| /// case .success(let data): | ||
| /// if let data = data, let password = String(data: data, encoding: .utf8) { | ||
| /// print("Retrieved password: \(password)") | ||
| /// } else { | ||
| /// print("No password found") | ||
| /// } | ||
| /// case .failure(let error): | ||
| /// print("Failed to retrieve password: \(error)") | ||
| /// } | ||
| /// ``` | ||
| public static func get(forKey key: String) -> Result<Data?, KeychainGetErrors> { | ||
| let query = | ||
| [ | ||
| kSecClass: kSecClassGenericPassword, | ||
| kSecAttrAccount: key, | ||
| kSecReturnData: true, | ||
| kSecMatchLimit: kSecMatchLimitOne, | ||
| ] as CFDictionary | ||
| var dataTypeRef: AnyObject? | ||
| let status = SecItemCopyMatching(query, &dataTypeRef) | ||
| guard status != errSecItemNotFound else { return .success(nil) } | ||
| guard status == errSecSuccess else { return .failure(.generalError(status: status)) } | ||
| guard let data = dataTypeRef as? Data else { return .success(nil) } | ||
|
|
||
| return .success(data) | ||
| } | ||
|
|
||
| /// Deletes the keychain item associated with the specified key. | ||
| /// | ||
| /// If no item exists for the given key, the method will fail with a `KeychainDeleteErrors` error. | ||
| /// | ||
| /// - Parameter key: The unique identifier for the keychain item to delete. | ||
| /// | ||
| /// - Returns: A `Result` indicating success or failure with a `KeychainDeleteErrors` error. | ||
| /// | ||
| /// - Example: | ||
| /// ```swift | ||
| /// let result = Keychain.delete(forKey: "user.password") | ||
| /// | ||
| /// switch result { | ||
| /// case .success: | ||
| /// print("Password deleted successfully") | ||
| /// case .failure(let error): | ||
| /// print("Failed to delete password: \(error)") | ||
| /// } | ||
| /// ``` | ||
| @discardableResult | ||
| public static func delete(forKey key: String) -> Result<Void, KeychainDeleteErrors> { | ||
| let query = | ||
| [ | ||
| kSecClass: kSecClassGenericPassword, | ||
| kSecAttrAccount: key, | ||
| ] as CFDictionary | ||
| let status = SecItemDelete(query) | ||
| guard status == errSecSuccess else { return .failure(.generalError(status: status)) } | ||
|
|
||
| return .success(()) | ||
| } | ||
|
|
||
| private static func update(_ data: Data, forKey key: String) -> Result<Void, KeychainSetErrors> { | ||
| let query = | ||
| [ | ||
| kSecClass: kSecClassGenericPassword, | ||
| kSecAttrAccount: key, | ||
| ] as CFDictionary | ||
| let attributes = [kSecValueData as String: data] as CFDictionary | ||
| let status = SecItemUpdate(query, attributes) | ||
| guard status == errSecSuccess else { return .failure(.generalError(status: status)) } | ||
|
|
||
| return .success(()) | ||
| } | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Corrected spelling of 'Gello' to 'Hello'.