五年前,我开始学习 Swift 的初衷很简单:想开发一个 App 来记录和管理自己的体检数据,帮助自己做好术后的健康管理。自【健康笔记】的 2.0 版本发布至今已有四年之久。如今回看这些代码(95% 以上都是四年前写的),总觉得难以面对 —— 坦白说,我认为写得很差。这个想法一直萦绕在心头,促使我在最近两年里一直计划着要彻底重构这个 App。终于在今年九月,我下定决心开始了这次重构。
在重构的过程中,我决定将项目中一些功能模块提取出来并开源分享。这样做主要有三个目的:
- 通过模块化,更好地对代码进行抽象和梳理
- 通过分享,倒逼自己写出更规范的代码
- 期望这些模块能对其他开发者有所帮助
本文将简要介绍这两个月来我开源的几个库。
SimpleLogger
SimpleLogger 是一个轻量级的日志模块。虽然它的特性很克制 —— 目前最显著的特点可能就是对 Swift 6 的支持,但作为一个在我所有代码模块中都会用到的基础设施,它的价值不容忽视。
主要特性:
- 提供两个内置实现(均支持 Sendable)
- 支持自定义后端扩展
- 可通过环境变量灵活控制日志输出
基本用法:
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
支持多种同步状态的跟踪(如 importing
、exporting
、setup
和 idle
)。以下是如何在 SwiftUI 中显示当前同步事件:
struct ContentView: View {
@StateObject var syncManager = SyncStatusManager()
var body: some View {
VStack {
Text("Sync Event: \(syncManager.syncEvent)")
}
}
}
检查 iCloud 账户状态
该库还支持检查设备的 iCloud 账户状态,并基于结果执行相应操作。以下代码展示了如何使用异步调用检查账户状态,并处理不可用情况:
let status = await syncManager.validateICloudAvailability { status, error in
// 使用闭包处理 iCloud 账户不可用的情况
}
if status == .available {
// 开始同步
} else {
// 处理 iCloud 账户不可用的情况
}
处理空间不足的情况
在实例化 SyncStatusManager
时,你可以提供一个闭包来处理 quotaExceeded
(即 iCloud 空间不足)状态。例如,当检测到同步失败且提示空间不足时,可以提醒用户进行清理或扩展存储:
let syncManager = SyncStatusManager {
if $0 == .quotaExceeded {
// 提醒用户清理 iCloud 空间
}
}
值得注意的是,iCloud 空间不足的情况并非总能通过明确的错误提示反馈,只能作为一种有限的机制来处理。
该库的设计思路受到了这篇文章的启发:General Findings About NSPersistentCloudKitContainer
ObservableDefaults
ObservableDefaults 是一个基于 Observation 框架的 @Observable
宏扩展,能够让特定变量与 UserDefaults
键自动关联,并且实时响应来自任何渠道的 UserDefaults
内容变更。
在我撰写的 SwiftUI 中的 UserDefaults 与 Observation:如何实现精准响应 一文中,对其实现细节进行了深入探讨。
在目前的项目中,我多数情况下会采用“观察优先”模式(observeFirst
),在这种模式下,只有经过特定标注的属性才会与 UserDefaults
关联,实现精确的数据同步。以下为一个示例:
@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
时,将禁用自动构造方法的生成。这一看似微小的改动,实则大大提高了实际开发中的灵活性。
// 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
参数控制是否自动生成构造方法,还引入了 @MainModelActorX
宏,专门解决目前 iOS 18 中在非主线程( 通过 @ModelActor
)上进行的数据修改无法触发视图自动更新的问题。
@MainModelActorX
允许开发者声明一个基于 mainContext
并运行在主线程上的类。这种实现让开发者可以在不大幅修改现有代码的前提下,解决 iOS 18 中的响应问题。待官方修复该问题后,代码可轻松切换回原生的 ModelActor
实现。
@MainActor
@MainModelActorX
final class 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
}
}
最后
这种模块化和开源的过程,某种程度上也是对我这五年来 Swift 学习历程的一次回顾和提炼。通过重构和分享,不仅能够让代码更加优雅和可维护,也能为 Swift 社区贡献一份力量。