Skip to content

Commit

Permalink
Merge pull request #71 from Abhiek187/feature/profile
Browse files Browse the repository at this point in the history
Optimize Keychain, upgrade to Swift 6, & use string catalog
  • Loading branch information
Abhiek187 authored Feb 23, 2025
2 parents 26c4896 + 4941f83 commit ccb608f
Show file tree
Hide file tree
Showing 46 changed files with 1,088 additions and 228 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ jobs:
- if: matrix.language == 'swift'
run: |
echo "Run, Build Application using script"
set -o pipefail && env NSUnbufferedIO=YES xcodebuild build -project "EZ Recipes/EZ Recipes.xcodeproj" -scheme "EZ Recipes" -disableAutomaticPackageResolution CODE_SIGNING_REQUIRED=NO CODE_SIGNING_ALLOWED=NO | xcbeautify --renderer github-actions
set -o pipefail && env NSUnbufferedIO=YES /Applications/Xcode_16.1.app/Contents/Developer/usr/bin/xcodebuild build -project "EZ Recipes/EZ Recipes.xcodeproj" -scheme "EZ Recipes" -disableAutomaticPackageResolution CODE_SIGNING_REQUIRED=NO CODE_SIGNING_ALLOWED=NO | xcbeautify --renderer github-actions
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
Expand Down
3 changes: 2 additions & 1 deletion .github/workflows/fastlane.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ jobs:
fail-fast: false
matrix:
# Escape spaces and parentheses in the device string
device: ["'iPhone SE (3rd generation)'", "'iPhone 15 Pro Max'", "'iPad Pro (11-inch) (4th generation)'"]
# Use supported simulators: /~https://github.com/actions/runner-images
device: ["'iPhone SE (3rd generation)'", "'iPhone 16 Pro Max'", "'iPad Pro 11-inch (M4)'"]

defaults:
run:
Expand Down
162 changes: 119 additions & 43 deletions EZ Recipes/EZ Recipes.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

272 changes: 196 additions & 76 deletions EZ Recipes/EZ Recipes/Helpers/Constants.swift

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion EZ Recipes/EZ Recipes/Helpers/CoreDataManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ struct CoreDataManager {
private static let logger = Logger(subsystem: Bundle.main.bundleIdentifier ?? Constants.appName, category: "CoreDataManager")

/// Initialize Core Data in-memory for SwiftUI previews & unit tests
static var preview: CoreDataManager = {
static let preview: CoreDataManager = {
let manager = CoreDataManager(inMemory: true)
let viewContext = manager.container.viewContext

Expand Down
53 changes: 21 additions & 32 deletions EZ Recipes/EZ Recipes/Helpers/KeychainManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
//

import Foundation
import OSLog
import Security

enum SecureStoreError: Error {
Expand All @@ -17,12 +18,15 @@ enum SecureStoreError: Error {
///
/// - Note: Keychain stored at `~/Library/Developer/CoreSimulator/Devices/_Device-UUID_/data/Library/Keychains`
/// (`/var/Keychains` on real devices) (`~/Library/Developer/Xcode/UserData/Previews/Simulator Devices/...` in previews) (Device-UUID and App-UUID gotten from `xcrun simctl get_app_container booted BUNDLE-ID data`)
class KeychainManager {
static let shared = KeychainManager()
struct KeychainManager {
private static let logger = Logger(subsystem: Bundle.main.bundleIdentifier ?? Constants.appName, category: "KeychainManager")
enum Key: String {
case token
}

private func setupQueryDictionary(forKey key: String) throws -> [CFString: Any] {
guard let keyData = key.data(using: .utf8) else {
print("Error! Could not convert the key to the expected format.")
private static func setupQueryDictionary(forKey key: Key) throws -> [CFString: Any] {
guard let keyData = key.rawValue.data(using: .utf8) else {
logger.error("Could not convert the key \"\(key.rawValue)\" to Data")
throw SecureStoreError.invalidContent
}

Expand All @@ -35,77 +39,62 @@ class KeychainManager {
.userPresence,
&error
) else {
print("Error! Could not create access control flags :: Error: \(String(describing: error))")
logger.error("Could not create access control flags :: Error: \(String(describing: error))")
throw SecureStoreError.invalidContent
}

// kSecClass defines the class of the keychain item
// We store user credentials in the keychain, so I use kSecClassGenericPassword for the value
// The kSecAttrAccount - keyData pair uniquely identify the account who will be accessing the keychain
return [
kSecClass: kSecClassGenericPassword, // genp table
kSecAttrAccount: keyData, // account == key
// kSecAttrAccessible: kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly,
kSecAttrAccessControl: accessControl
]
}

func save(entry: String, forKey key: String) throws {
guard !entry.isEmpty && !key.isEmpty else {
print("Can't add an empty string to the keychain")
throw SecureStoreError.invalidContent
}
// remove old value if any
// try delete(forKey: key)
static func save(entry: String, forKey key: Key) throws {
// Remove any existing entries for key to avoid errSecDuplicateItem
try? delete(key: key)

var queryDictionary = try setupQueryDictionary(forKey: key)

// add the value
queryDictionary[kSecValueData] = entry.data(using: .utf8)

let status = SecItemAdd(queryDictionary as CFDictionary, nil)
guard status == errSecSuccess else {
throw SecureStoreError.failure(error: status.error)
}

logger.debug("Successfully added entry for key \"\(key.rawValue)\" to the Keychain")
}

func retrieve(forKey key: String) throws -> String? {
guard !key.isEmpty else {
throw SecureStoreError.invalidContent
}

static func retrieve(forKey key: Key) throws -> String? {
var queryDictionary = try setupQueryDictionary(forKey: key)
// Set additional query attributes
queryDictionary[kSecReturnData] = kCFBooleanTrue // expecting result of type Data
queryDictionary[kSecMatchLimit] = kSecMatchLimitOne // limit the number of search results to one

var data: AnyObject?

// Returns one or more keychain items that match a search query, or copies attributes of specific keychain items
let status = SecItemCopyMatching(queryDictionary as CFDictionary, &data) // search query
let status = SecItemCopyMatching(queryDictionary as CFDictionary, &data)
guard status == errSecSuccess else {
throw SecureStoreError.failure(error: status.error)
}

guard let itemData = data as? Data,
let result = String(data: itemData, encoding: .utf8) else {
logger.error("Could not convert the value of key \"\(key.rawValue)\" to a String")
return nil
}

return result
}

func delete(forKey key: String) throws {
guard !key.isEmpty else {
print("Key must be valid")
throw SecureStoreError.invalidContent
}

static func delete(key: Key) throws {
let queryDictionary = try setupQueryDictionary(forKey: key)

let status = SecItemDelete(queryDictionary as CFDictionary)
guard status == errSecSuccess else {
throw SecureStoreError.failure(error: status.error)
}

logger.debug("Successfully deleted key \"\(key.rawValue)\" from the Keychain")
}
}
2 changes: 1 addition & 1 deletion EZ Recipes/EZ Recipes/Helpers/NetworkManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ struct NetworkManager: RecipeRepository {
private let session = Session(eventMonitors: [AFLogger()])
private let logger = Logger(subsystem: Bundle.main.bundleIdentifier ?? Constants.appName, category: "NetworkManager")

private func parseResponse<T: Decodable>(fromRequest request: DataRequest, method: String) async -> Result<T, RecipeError> {
private func parseResponse<T: Decodable & Sendable>(fromRequest request: DataRequest, method: String) async -> Result<T, RecipeError> {
do {
// If successful, the request can be decoded as a Recipe object
let response = try await request.serializingDecodable(T.self).value
Expand Down
4 changes: 1 addition & 3 deletions EZ Recipes/EZ Recipes/Helpers/OSStatusExtension.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,9 @@ import Foundation

extension OSStatus {
/// Convert an OSStatus code to a more human-readable error
/// - Returns: an `NSError` object with the error message, or `nil` if the status is success
/// - Returns: an `NSError` object with the error message
/// - Note: OSStatus codes can be found at https://www.osstatus.com/
var error: NSError {
// guard self != errSecSuccess else { return nil }

let message = SecCopyErrorMessageString(self, nil) as String? ?? "Unknown error"

return NSError(domain: NSOSStatusErrorDomain, code: Int(self), userInfo: [
Expand Down
2 changes: 1 addition & 1 deletion EZ Recipes/EZ Recipes/Helpers/RecipeRepository.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
//

// A protocol allows mock repositories to be created for tests
protocol RecipeRepository {
protocol RecipeRepository: Sendable {
static var shared: Self { get } // singleton

func getRecipes(withFilter filter: RecipeFilter) async -> Result<[Recipe], RecipeError>
Expand Down
11 changes: 5 additions & 6 deletions EZ Recipes/EZ Recipes/Helpers/UserDefaultsManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import OSLog
/// - Note: UserDefaults stored at `~/Library/Developer/CoreSimulator/Devices/_Device-UUID_/data/Containers/Data/Application/_App-UUID_/Library/Preferences`
/// (`/var/mobile/Containers/...` on real devices) (`~/Library/Developer/Xcode/UserData/Previews/Simulator Devices/...` in previews) (Device-UUID and App-UUID gotten from `xcrun simctl get_app_container booted BUNDLE-ID data`) (view plist file by running `/usr/libexec/PlistBuddy -c print PLIST-FILE`)
struct UserDefaultsManager {
private static let userDefaults = UserDefaults.standard
private static let logger = Logger(subsystem: Bundle.main.bundleIdentifier ?? Constants.appName, category: "UserDefaultsManager")
struct Keys {
static let terms = "terms"
Expand All @@ -22,10 +21,10 @@ struct UserDefaultsManager {
}

static func getTerms() -> [Term]? {
guard let termStorePlist = userDefaults.value(forKey: Keys.terms) as? Data else { return nil }
guard let termStorePlist = UserDefaults.standard.value(forKey: Keys.terms) as? Data else { return nil }
guard let termStore = try? PropertyListDecoder().decode(TermStore.self, from: termStorePlist) else {
logger.warning("Stored terms are corrupted, deleting them and retrieving a new set of terms...")
userDefaults.removeObject(forKey: Keys.terms)
UserDefaults.standard.removeObject(forKey: Keys.terms)
return nil
}

Expand All @@ -49,12 +48,12 @@ struct UserDefaultsManager {
logger.warning("Couldn't encode termStore to a property list: \(String(describing: termStore))")
return
}
userDefaults.set(termStorePlist, forKey: Keys.terms)
UserDefaults.standard.set(termStorePlist, forKey: Keys.terms)
logger.debug("Saved terms to UserDefaults!")
}

static func incrementRecipesViewed() {
let recipesViewed = userDefaults.integer(forKey: Keys.recipesViewed)
userDefaults.set(recipesViewed + 1, forKey: Keys.recipesViewed)
let recipesViewed = UserDefaults.standard.integer(forKey: Keys.recipesViewed)
UserDefaults.standard.set(recipesViewed + 1, forKey: Keys.recipesViewed)
}
}
18 changes: 18 additions & 0 deletions EZ Recipes/EZ Recipes/InfoPlist.xcstrings
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"sourceLanguage" : "en",
"strings" : {
"CFBundleName" : {
"comment" : "Bundle name",
"extractionState" : "extracted_with_value",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "new",
"value" : "EZ Recipes"
}
}
}
}
},
"version" : "1.0"
}
Loading

0 comments on commit ccb608f

Please sign in to comment.