从项目重构说起:五个 Swift 模块分享

发表于

为您每周带来有关 Swift 和 SwiftUI 的精选资讯!

五年前,我开始学习 Swift 的初衷很简单:想开发一个 App 来记录和管理自己的体检数据,帮助自己做好术后的健康管理。自【健康笔记】的 2.0 版本发布至今已有四年之久。如今回看这些代码(95% 以上都是四年前写的),总觉得难以面对 —— 坦白说,我认为写得很差。这个想法一直萦绕在心头,促使我在最近两年里一直计划着要彻底重构这个 App。终于在今年九月,我下定决心开始了这次重构。

在重构的过程中,我决定将项目中一些功能模块提取出来并开源分享。这样做主要有三个目的:

  • 通过模块化,更好地对代码进行抽象和梳理
  • 通过分享,倒逼自己写出更规范的代码
  • 期望这些模块能对其他开发者有所帮助

本文将简要介绍这两个月来我开源的几个库。

SimpleLogger

SimpleLogger 是一个轻量级的日志模块。虽然它的特性很克制 —— 目前最显著的特点可能就是对 Swift 6 的支持,但作为一个在我所有代码模块中都会用到的基础设施,它的价值不容忽视。

主要特性:

  • 提供两个内置实现(均支持 Sendable)
  • 支持自定义后端扩展
  • 可通过环境变量灵活控制日志输出

基本用法:

Swift
import SimpleLogger

let logger: LoggerManagerProtocol = .default(subsystem: "com.example.app", category: "general")
logger.info("App started")

iCloudSyncStatusKit

在使用 Core Data with CloudKit 的 App 中,实时了解用户设备的 iCloud 账户状态及数据同步进度至关重要。为此,我将这一需求模块化并封装为 iCloudSyncStatusKit 库。

数据同步状态的响应

iCloudSyncStatusKit 支持多种同步状态的跟踪(如 importingexportingsetupidle)。以下是如何在 SwiftUI 中显示当前同步事件:

Swift
struct ContentView: View {
    @StateObject var syncManager = SyncStatusManager()

    var body: some View {
        VStack {
            Text("Sync Event: \(syncManager.syncEvent)")
        }
    }
}

检查 iCloud 账户状态

该库还支持检查设备的 iCloud 账户状态,并基于结果执行相应操作。以下代码展示了如何使用异步调用检查账户状态,并处理不可用情况:

Swift
let status = await syncManager.validateICloudAvailability { status, error in
    // 使用闭包处理 iCloud 账户不可用的情况                      
}

if status == .available {
    // 开始同步
} else {
    // 处理 iCloud 账户不可用的情况
}

处理空间不足的情况

在实例化 SyncStatusManager 时,你可以提供一个闭包来处理 quotaExceeded(即 iCloud 空间不足)状态。例如,当检测到同步失败且提示空间不足时,可以提醒用户进行清理或扩展存储:

Swift
let syncManager = SyncStatusManager { 
    if $0 == .quotaExceeded {
        // 提醒用户清理 iCloud 空间
    }
}

值得注意的是,iCloud 空间不足的情况并非总能通过明确的错误提示反馈,只能作为一种有限的机制来处理。

该库的设计思路受到了这篇文章的启发:General Findings About NSPersistentCloudKitContainer

ObservableDefaults

ObservableDefaults 是一个基于 Observation 框架的 @Observable 宏扩展,能够让特定变量与 UserDefaults 键自动关联,并且实时响应来自任何渠道的 UserDefaults 内容变更

在我撰写的 SwiftUI 中的 UserDefaults 与 Observation:如何实现精准响应 一文中,对其实现细节进行了深入探讨。

在目前的项目中,我多数情况下会采用“观察优先”模式(observeFirst),在这种模式下,只有经过特定标注的属性才会与 UserDefaults 关联,实现精确的数据同步。以下为一个示例:

Swift
@ObservableDefaults(observeFirst: true)
public class Test2 {
    // 自动添加 @ObservableOnly,支持观察但不会持久化
    public var name: String = "fat"

    // 自动添加 @ObservableOnly
    public var age = 109

    // 需要持久化的属性必须使用 @DefaultsBacked 标注,并可指定 UserDefaults 的键名
    @DefaultsBacked(userDefaultsKey: "myHeight")
    public var height = 190

    // 该属性不受观察,也不会持久化
    @Ignore
    public var weight = 10
}

CoreDataEvolution

CoreDataEvolution 的设计灵感来自 SwiftData 的 @ModelActor,为 Core Data 引入了类似 SwiftData 的优雅并发编程体验。在我的文章 Core Data 改革:实现 SwiftData 般的优雅并发操作 中,我详细探讨了该库的设计理念及其实现原理。

在实际使用过程中,我发现完全按照 @ModelActor 的模式自动生成构造方法,可能限制了 Actor 的灵活性。在很多场景下,构建数据处理模块时需要自定义参数,因此我在 @NSModelActor 中添加了 disableGenerateInit 参数。当该参数设为 true 时,将禁用自动构造方法的生成。这一看似微小的改动,实则大大提高了实际开发中的灵活性。

Swift
// disableGenerateInit 默认为 false,不提供时会自动生成构造方法
@NSModelActor(disableGenerateInit: true) 
public actor DataHandler {
    func createNewItem(_ timestamp: Date = .now, showThread: Bool = false) throws -> NSManagedObjectID {
        let item = Item(context: modelContext)
        item.timestamp = timestamp
        try modelContext.save()
        return item.objectID
    }

    // 自定义构造方法,支持更多参数配置
    init(container: NSPersistentContainer, viewName: String) {
        modelContainer = container
        let context = container.newBackgroundContext()
        context.name = viewName // 添加了上下文命名逻辑
        modelExecutor = .init(context: context)
    }
}

CoreDataEvolution 还提供了一个 NSMainModelActor,允许开发者使用 viewContext 声明运行在主线程上的类。

ModelActorX

ModelActorX 是对 SwiftData 中 ModelActor 的增强实现。该库不仅支持通过 disableGenerateInit 参数控制是否自动生成构造方法,还提供了使用视图上下文创建 Actor 的构造方法,专门解决目前 iOS 18 中在非主线程( 通过 @ModelActor)上进行的数据修改无法触发视图自动更新的问题。

@MainModelActorX 允许开发者声明一个基于 mainContext 并运行在主线程上的 actor。这种实现让开发者可以在不修改现有代码的前提下,解决 iOS 18 中的响应问题。待官方修复该问题后,代码可轻松切换回原生的 ModelActor 实现。

Swift
@ModelActorX
actor MainDataHandler {
    func newItem(date: Date) throws -> PersistentIdentifier {
        let item = Item(timestamp: date)
        modelContext.insert(item)
        try modelContext.save()
        return item.persistentModelID
    }

    func getTimestampFromItemID(_ itemID: PersistentIdentifier) -> Date? {
        return self[itemID, as: Item.self]?.timestamp
    }
  
    func updateItem(id: PersistentIdentifier, date: Date) {
        guard let item = self[id, as: Item.self] else { return }
        item.timestamp = date
        try? modelContext.save()
    }
}

// Usage

Task{ @MainActor in
   let handler = DataHandler(mainContext: container.mainContext) // Use the view context for construction
   await handler.updateItem(id: id, date: .now) // Even on the main thread, you can still use `await`
}

ModelActorX 仅能改善开发者在私有上下文中进行数据更改时的响应效果,对于 iOS 18 中云同步数据的变化无法及时响应的问题仍然无能为力。

最后

这种模块化和开源的过程,某种程度上也是对我这五年来 Swift 学习历程的一次回顾和提炼。通过重构和分享,不仅能够让代码更加优雅和可维护,也能为 Swift 社区贡献一份力量。

每周一晚,与全球开发者同步,掌握 Swift & SwiftUI 最新动向
可随时退订,干净无 spam