Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions Sources/KamaalUI/Views/KFormBox.swift
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")

Copilot AI Oct 12, 2025

Copy link

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'.

Suggested change
Text("Gello")
Text("Hello")

Copilot uses AI. Check for mistakes.
}
}
159 changes: 159 additions & 0 deletions Sources/KamaalUtils/Keychain.swift
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(())
}
}
Loading