feat: 实现数据迁移和用户信息管理优化

- 在AppDelegate中集成数据迁移管理器,支持从UserDefaults迁移到Keychain。
- 重构UserInfoManager,使用Keychain存储用户信息,增加内存缓存以提升性能。
- 添加API加载效果视图,增强用户体验。
- 更新SplashFeature以支持自动登录和认证状态检查。
- 语言设置迁移至Keychain,确保用户设置的安全性。
This commit is contained in:
edwinQQQ
2025-07-10 17:20:20 +08:00
parent 6084ade9ea
commit 4a1b814902
15 changed files with 1773 additions and 354 deletions

View File

@@ -0,0 +1,362 @@
import Foundation
import Security
/// Keychain
///
/// UserDefaults
/// Codable
///
///
/// - iOS Keychain
/// - Codable
/// -
/// - 线
/// - 访
final class KeychainManager {
// MARK: -
static let shared = KeychainManager()
private init() {}
// MARK: -
private let service: String = {
return Bundle.main.bundleIdentifier ?? "com.yana.app"
}()
private let accessGroup: String? = nil // App Group
// MARK: -
enum KeychainError: Error, LocalizedError {
case dataConversionFailed
case encodingFailed(Error)
case decodingFailed(Error)
case keychainOperationFailed(OSStatus)
case itemNotFound
case duplicateItem
case invalidParameters
var errorDescription: String? {
switch self {
case .dataConversionFailed:
return "数据转换失败"
case .encodingFailed(let error):
return "编码失败: \(error.localizedDescription)"
case .decodingFailed(let error):
return "解码失败: \(error.localizedDescription)"
case .keychainOperationFailed(let status):
return "Keychain 操作失败: \(status)"
case .itemNotFound:
return "未找到指定项目"
case .duplicateItem:
return "项目已存在"
case .invalidParameters:
return "无效参数"
}
}
}
// MARK: - 访
enum AccessLevel {
case whenUnlocked // 访
case whenUnlockedThisDeviceOnly // 访
case afterFirstUnlock // 访
case afterFirstUnlockThisDeviceOnly // 访
var attribute: CFString {
switch self {
case .whenUnlocked:
return kSecAttrAccessibleWhenUnlocked
case .whenUnlockedThisDeviceOnly:
return kSecAttrAccessibleWhenUnlockedThisDeviceOnly
case .afterFirstUnlock:
return kSecAttrAccessibleAfterFirstUnlock
case .afterFirstUnlockThisDeviceOnly:
return kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
}
}
}
// MARK: -
/// Codable Keychain
/// - Parameters:
/// - object: Codable
/// - key:
/// - accessLevel: 访访
/// - Throws: KeychainError
func store<T: Codable>(_ object: T, forKey key: String, accessLevel: AccessLevel = .whenUnlockedThisDeviceOnly) throws {
// 1. Data
let data: Data
do {
data = try JSONEncoder().encode(object)
} catch {
throw KeychainError.encodingFailed(error)
}
// 2.
var query = baseQuery(forKey: key)
query[kSecValueData] = data
query[kSecAttrAccessible] = accessLevel.attribute
// 3.
SecItemDelete(query as CFDictionary)
// 4.
let status = SecItemAdd(query as CFDictionary, nil)
guard status == errSecSuccess else {
throw KeychainError.keychainOperationFailed(status)
}
print("🔐 Keychain 存储成功: \(key)")
}
/// Keychain Codable
/// - Parameters:
/// - type:
/// - key:
/// - Returns: nil
/// - Throws: KeychainError
func retrieve<T: Codable>(_ type: T.Type, forKey key: String) throws -> T? {
// 1.
var query = baseQuery(forKey: key)
query[kSecReturnData] = true
query[kSecMatchLimit] = kSecMatchLimitOne
// 2.
var result: AnyObject?
let status = SecItemCopyMatching(query as CFDictionary, &result)
// 3.
switch status {
case errSecSuccess:
guard let data = result as? Data else {
throw KeychainError.dataConversionFailed
}
// 4.
do {
let object = try JSONDecoder().decode(type, from: data)
print("🔐 Keychain 读取成功: \(key)")
return object
} catch {
throw KeychainError.decodingFailed(error)
}
case errSecItemNotFound:
return nil
default:
throw KeychainError.keychainOperationFailed(status)
}
}
/// Keychain
/// - Parameters:
/// - object:
/// - key:
/// - Throws: KeychainError
func update<T: Codable>(_ object: T, forKey key: String) throws {
// 1.
let data: Data
do {
data = try JSONEncoder().encode(object)
} catch {
throw KeychainError.encodingFailed(error)
}
// 2.
let query = baseQuery(forKey: key)
let updateAttributes: [CFString: Any] = [
kSecValueData: data
]
// 3.
let status = SecItemUpdate(query as CFDictionary, updateAttributes as CFDictionary)
switch status {
case errSecSuccess:
print("🔐 Keychain 更新成功: \(key)")
case errSecItemNotFound:
//
try store(object, forKey: key)
default:
throw KeychainError.keychainOperationFailed(status)
}
}
/// Keychain
/// - Parameter key:
/// - Throws: KeychainError
func delete(forKey key: String) throws {
let query = baseQuery(forKey: key)
let status = SecItemDelete(query as CFDictionary)
switch status {
case errSecSuccess:
print("🔐 Keychain 删除成功: \(key)")
case errSecItemNotFound:
//
break
default:
throw KeychainError.keychainOperationFailed(status)
}
}
/// Keychain
/// - Parameter key:
/// - Returns:
func exists(forKey key: String) -> Bool {
var query = baseQuery(forKey: key)
query[kSecReturnData] = false
query[kSecMatchLimit] = kSecMatchLimitOne
let status = SecItemCopyMatching(query as CFDictionary, nil)
return status == errSecSuccess
}
/// Keychain
/// - Throws: KeychainError
func clearAll() throws {
let query: [CFString: Any] = [
kSecClass: kSecClassGenericPassword,
kSecAttrService: service
]
let status = SecItemDelete(query as CFDictionary)
switch status {
case errSecSuccess, errSecItemNotFound:
print("🔐 Keychain 清除完成")
default:
throw KeychainError.keychainOperationFailed(status)
}
}
// MARK: -
///
/// - Parameter key:
/// - Returns:
private func baseQuery(forKey key: String) -> [CFString: Any] {
var query: [CFString: Any] = [
kSecClass: kSecClassGenericPassword,
kSecAttrService: service,
kSecAttrAccount: key
]
if let accessGroup = accessGroup {
query[kSecAttrAccessGroup] = accessGroup
}
return query
}
}
// MARK: - 便
extension KeychainManager {
/// Keychain
/// - Parameters:
/// - string:
/// - key:
/// - accessLevel: 访
/// - Throws: KeychainError
func storeString(_ string: String, forKey key: String, accessLevel: AccessLevel = .whenUnlockedThisDeviceOnly) throws {
try store(string, forKey: key, accessLevel: accessLevel)
}
/// Keychain
/// - Parameter key:
/// - Returns:
/// - Throws: KeychainError
func retrieveString(forKey key: String) throws -> String? {
return try retrieve(String.self, forKey: key)
}
/// Keychain
/// - Parameters:
/// - data:
/// - key:
/// - accessLevel: 访
/// - Throws: KeychainError
func storeData(_ data: Data, forKey key: String, accessLevel: AccessLevel = .whenUnlockedThisDeviceOnly) throws {
var query = baseQuery(forKey: key)
query[kSecValueData] = data
query[kSecAttrAccessible] = accessLevel.attribute
SecItemDelete(query as CFDictionary)
let status = SecItemAdd(query as CFDictionary, nil)
guard status == errSecSuccess else {
throw KeychainError.keychainOperationFailed(status)
}
}
/// Keychain
/// - Parameter key:
/// - Returns:
/// - Throws: KeychainError
func retrieveData(forKey key: String) throws -> Data? {
var query = baseQuery(forKey: key)
query[kSecReturnData] = true
query[kSecMatchLimit] = kSecMatchLimitOne
var result: AnyObject?
let status = SecItemCopyMatching(query as CFDictionary, &result)
switch status {
case errSecSuccess:
return result as? Data
case errSecItemNotFound:
return nil
default:
throw KeychainError.keychainOperationFailed(status)
}
}
}
// MARK: -
#if DEBUG
extension KeychainManager {
///
/// - Returns:
func debugListAllKeys() -> [String] {
let query: [CFString: Any] = [
kSecClass: kSecClassGenericPassword,
kSecAttrService: service,
kSecReturnAttributes: true,
kSecMatchLimit: kSecMatchLimitAll
]
var result: AnyObject?
let status = SecItemCopyMatching(query as CFDictionary, &result)
guard status == errSecSuccess,
let items = result as? [[CFString: Any]] else {
return []
}
return items.compactMap { item in
item[kSecAttrAccount] as? String
}
}
///
func debugPrintAllKeys() {
let keys = debugListAllKeys()
print("🔐 Keychain 中存储的键:")
for key in keys {
print(" - \(key)")
}
}
}
#endif