19. 存储体系
存储方案必须分层:小数据用 UserDefaults,安全凭证用 Keychain,中大型数据用文件或数据库,缓存必须有过期策略。
一、UserDefaults:轻量配置
适用于开关、版本标记、登录态等小数据。
struct Settings {
private static let keyOnboarding = "onboarding.finished"
static var onboardingFinished: Bool {
get { UserDefaults.standard.bool(forKey: keyOnboarding) }
set { UserDefaults.standard.set(newValue, forKey: keyOnboarding) }
}
}
类型安全封装
@propertyWrapper
struct UserDefault<T> {
let key: String
let defaultValue: T
var wrappedValue: T {
get { UserDefaults.standard.object(forKey: key) as? T ?? defaultValue }
set { UserDefaults.standard.set(newValue, forKey: key) }
}
}
struct AppConfig {
@UserDefault(key: "theme", defaultValue: "light")
static var theme: String
}
二、文件系统:中等数据
1. 获取路径
enum DirectoryType {
case documents
case caches
}
func appDirectory(_ type: DirectoryType) -> URL {
let dir: FileManager.SearchPathDirectory = (type == .documents) ? .documentDirectory : .cachesDirectory
return FileManager.default.urls(for: dir, in: .userDomainMask)[0]
}
2. 保存 JSON 文件
struct Profile: Codable {
let id: Int
let name: String
}
func saveProfile(_ profile: Profile) throws {
let url = appDirectory(.documents).appendingPathComponent("profile.json")
let data = try JSONEncoder().encode(profile)
try data.write(to: url, options: .atomic)
}
func loadProfile() throws -> Profile {
let url = appDirectory(.documents).appendingPathComponent("profile.json")
let data = try Data(contentsOf: url)
return try JSONDecoder().decode(Profile.self, from: data)
}
三、Keychain:敏感信息
适合 Token、密码、证书。
import Security
final class KeychainStore {
func save(_ data: Data, service: String, account: String) {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecAttrAccount as String: account,
kSecValueData as String: data
]
SecItemDelete(query as CFDictionary)
SecItemAdd(query as CFDictionary, nil)
}
func load(service: String, account: String) -> Data? {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecAttrAccount as String: account,
kSecReturnData as String: true,
kSecMatchLimit as String: kSecMatchLimitOne
]
var result: AnyObject?
let status = SecItemCopyMatching(query as CFDictionary, &result)
return status == errSecSuccess ? result as? Data : nil
}
func delete(service: String, account: String) {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecAttrAccount as String: account
]
SecItemDelete(query as CFDictionary)
}
}
四、Core Data:结构化数据
1. 最小栈
import CoreData
final class CoreDataStack {
static let shared = CoreDataStack()
private init() {}
lazy var container: NSPersistentContainer = {
let container = NSPersistentContainer(name: "AppModel")
container.loadPersistentStores { _, error in
if let error { fatalError("CoreData error: \(error)") }
}
return container
}()
var context: NSManagedObjectContext { container.viewContext }
func save() {
guard context.hasChanges else { return }
try? context.save()
}
}
2. 保存与查询
// 假设实体名为 "Note",包含字段 "title"(String)
func insertNote(title: String) {
let ctx = CoreDataStack.shared.context
let note = NSEntityDescription.insertNewObject(forEntityName: "Note", into: ctx)
note.setValue(title, forKey: "title")
CoreDataStack.shared.save()
}
func fetchNotes() -> [String] {
let ctx = CoreDataStack.shared.context
let request = NSFetchRequest<NSManagedObject>(entityName: "Note")
let results = (try? ctx.fetch(request)) ?? []
return results.compactMap { $0.value(forKey: "title") as? String }
}
五、缓存层:内存 + 磁盘
1. 内存缓存
final class MemoryCache<T: AnyObject> {
private let cache = NSCache<NSString, T>()
func set(_ value: T, for key: String) { cache.setObject(value, forKey: key as NSString) }
func get(_ key: String) -> T? { cache.object(forKey: key as NSString) }
}
2. 磁盘缓存
final class DiskCache {
private let folder: URL
init(folderName: String) {
folder = appDirectory(.caches).appendingPathComponent(folderName)
try? FileManager.default.createDirectory(at: folder, withIntermediateDirectories: true)
}
func save(_ data: Data, key: String) throws {
let url = folder.appendingPathComponent(key)
try data.write(to: url, options: .atomic)
}
func load(key: String) -> Data? {
let url = folder.appendingPathComponent(key)
return try? Data(contentsOf: url)
}
}
六、数据迁移与版本控制
当模型升级,必须能平滑迁移。
struct StorageVersion {
static let key = "storage.version"
static var current: Int { 2 }
static func migrateIfNeeded() {
let old = UserDefaults.standard.integer(forKey: key)
guard old < current else { return }
// 执行迁移逻辑
UserDefaults.standard.set(current, forKey: key)
}
}
存储层一旦稳定,业务扩展会非常快。关键是把“数据类型 → 存储介质 → 生命周期”映射清楚。