Five years ago, my initial motivation for learning Swift was simple: I wanted to develop an app to record and manage my own medical examination data to help with postoperative health management. It’s been four years since the release of version 2.0 of Health Notes. Looking back at that code now (over 95% of which was written four years ago), I find it hard to face—frankly speaking, I think it’s poorly written. This thought has been lingering in my mind, prompting me over the past two years to plan a complete refactoring of the app. Finally, in September of this year, I decided to embark on this refactoring journey.
During the refactoring process, I decided to extract some functional modules from the project and share them as open source. The main reasons for doing this are:
- Better code abstraction and organization through modularization
- Forcing myself to write more standardized code by sharing
- Hoping these modules can help other developers
This article will briefly introduce several libraries I’ve open-sourced over the past two months.
SimpleLogger
SimpleLogger is a lightweight logging module. Although its features are restrained—the most notable characteristic at present might be its support for Swift 6—it is an indispensable piece of infrastructure that I use in all my code modules.
Main features:
- Provides two built-in implementations (both support
Sendable
) - Supports custom backend extensions
- Allows flexible control of log output through environment variables
Basic usage:
import SimpleLogger
let logger: LoggerManagerProtocol = .default(subsystem: "com.example.app", category: "general")
logger.info("App started")
iCloudSyncStatusKit
In apps using Core Data with CloudKit, it’s crucial to be aware of the user’s iCloud account status and data synchronization progress in real time. Therefore, I modularized this requirement and packaged it into the iCloudSyncStatusKit library.
Responding to Data Sync Status
iCloudSyncStatusKit
supports tracking multiple sync statuses (such as importing
, exporting
, setup
, and idle
). Here’s how to display the current sync event in SwiftUI:
struct ContentView: View {
@StateObject var syncManager = SyncStatusManager()
var body: some View {
VStack {
Text("Sync Event: \(syncManager.syncEvent)")
}
}
}
Checking iCloud Account Status
This library also supports checking the device’s iCloud account status and performing corresponding actions based on the result. The following code shows how to use an asynchronous call to check the account status and handle the unavailable case:
let status = await syncManager.validateICloudAvailability { status, error in
// Handle the case where the iCloud account is unavailable using a closure
}
if status == .available {
// Start synchronization
} else {
// Handle the case where the iCloud account is unavailable
}
Handling Insufficient Space Situations
When instantiating SyncStatusManager
, you can provide a closure to handle the quotaExceeded
status (i.e., insufficient iCloud storage). For example, when synchronization fails and indicates insufficient space, you can prompt the user to clean up or expand storage:
let syncManager = SyncStatusManager {
if $0 == .quotaExceeded {
// Remind the user to clean up iCloud storage
}
}
It’s worth noting that situations of insufficient iCloud space are not always explicitly reported through error prompts and can only be handled as a limited mechanism.
The design idea of this library was inspired by this article: General Findings About NSPersistentCloudKitContainer
ObservableDefaults
ObservableDefaults is an @Observable
macro extension based on the Observation framework. It allows specific variables to be automatically associated with UserDefaults
keys and respond in real time to UserDefaults
content changes from any source.
In my article UserDefaults and Observation in SwiftUI: How to Achieve Precise Responsiveness, I delve into its implementation details.
In the current project, I mostly adopt the ”observe-first” mode (observeFirst
). In this mode, only properties marked with specific annotations will be associated with UserDefaults
, achieving precise data synchronization. Here is an example:
@ObservableDefaults(observeFirst: true)
public class Test2 {
// Automatically adds @ObservableOnly, supports observation but won't persist
public var name: String = "fat"
// Automatically adds @ObservableOnly
public var age = 109
// Properties that need to be persisted must be marked with @DefaultsBacked, and can specify the UserDefaults key name
@DefaultsBacked(userDefaultsKey: "myHeight")
public var height = 190
// This property is neither observed nor persisted
@Ignore
public var weight = 10
}
CoreDataEvolution
CoreDataEvolution was inspired by SwiftData’s @ModelActor
and introduces an elegant concurrent programming experience similar to SwiftData for Core Data. In my article Core Data Reform: Achieving Elegant Concurrency Operations like SwiftData, I discuss in detail the design philosophy and implementation principles of this library.
In actual use, I found that automatically generating constructors completely following the @ModelActor
pattern might limit the flexibility of the Actor
. In many scenarios, building data processing modules requires custom parameters. Therefore, I added the disableGenerateInit
parameter in @NSModelActor
. When this parameter is set to true
, automatic constructor generation is disabled. This seemingly minor change actually greatly enhances flexibility in practical development.
// disableGenerateInit defaults to false; if not provided, constructors are automatically generated
@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
}
// Custom constructor, supports more parameter configurations
init(container: NSPersistentContainer, viewName: String) {
modelContainer = container
let context = container.newBackgroundContext()
context.name = viewName // Added context naming logic
modelExecutor = .init(context: context)
}
}
CoreDataEvolution
also provides an NSMainModelActor
, allowing developers to declare classes running on the main thread using viewContext
.
ModelActorX
ModelActorX is an enhanced implementation of ModelActor
in SwiftData. This library not only allows control over automatic initializer generation through the disableGenerateInit
parameter but also provides an initializer method using the view context to create Actors. This approach specifically addresses the current issue in iOS 18 where data modifications on non-main threads (using @ModelActor
) do not trigger automatic view updates.
The @MainModelActorX
attribute enables developers to declare a actor that operates on the main thread using mainContext
. This implementation allows developers to address the responsiveness issue in iOS 18 without altering existing code. Once the official fix is released, developers can seamlessly switch back to the native ModelActor
implementation.
@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
only improves the responsiveness for data modifications made by developers within private contexts; it cannot resolve the issue of delayed responses to cloud-synced data changes in iOS 18.
Conclusion
This process of modularization and open-sourcing is, in a sense, a review and distillation of my five-year journey learning Swift. Through refactoring and sharing, not only can the code become more elegant and maintainable, but it also allows me to contribute to the Swift community.