精选文章

19. 存储体系

2018-06-14 · 存储

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)
    }
}

存储层一旦稳定,业务扩展会非常快。关键是把“数据类型 → 存储介质 → 生命周期”映射清楚。

JJ

作者简介

专注于内容创作、产品策略与设计实践。欢迎交流合作。

上一篇 下一篇