ModelActorX is a Swift library that provides custom macros ModelActorX
and MainModelActorX
to enhance and extend the functionality of SwiftData's ModelActor
. These macros offer additional flexibility by allowing developers to control the generation of initializers and to declare additional variables within actors and classes.
Don't miss out on the latest updates and excellent articles about Swift, SwiftUI, Core Data, and SwiftData. Subscribe to Fatbobman's Swift Weekly and receive weekly insights and valuable content directly to your inbox.
- ModelActorX Macro: Similar to SwiftData's
ModelActor
, with an addeddisableGenerateInit
parameter to control initializer generation. - MainModelActorX Macro: Generates a class running on the
MainActor
, ideal for main-thread operations, and usesModelContainer
'smainContext
. - Custom Initializers: Ability to declare additional variables and pass them through custom initializers when
disableGenerateInit
is set totrue
. - Seamless Integration: Designed to work seamlessly with SwiftData and existing Swift projects.
Add ModelActorX to your project using Swift Package Manager. In your Package.swift
file, add:
dependencies: [
.package(url: "https://github.com/fatbobman/ModelActorX.git", from: "0.1.0")
]
Alternatively, in Xcode:
- Go to File > Add Packages...
- Enter the repository URL:
https://github.com/fatbobman/ModelActorX.git
- Follow the prompts to add the package to your project.
The ModelActorX
macro is used to define an actor with functionality similar to SwiftData's ModelActor
. The key difference is the disableGenerateInit
parameter, which, when set to true
, prevents the automatic generation of an initializer. This allows you to declare additional variables and provide a custom initializer.
@ModelActorX
actor DataHandler {
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
}
}
@ModelActorX(disableGenerateInit: true)
actor DataHandler1 {
let date: Date
func newItem() 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
}
init(container: ModelContainer, date: Date) {
self.date = date
modelContainer = container
let modelContext = ModelContext(modelContainer)
modelExecutor = DefaultSerialModelExecutor(modelContext: modelContext)
}
}
The MainModelActorX
macro is used to generate a class that runs on the MainActor
. This is particularly useful for UI updates or any operations that need to be performed on the main thread. The generated class uses the mainContext
from ModelContainer
.
@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
}
}
@MainActor
@MainModelActorX(disableGenerateInit: true)
final class MainDataHandler1 {
let date: Date
func newItem() 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
}
init(container: ModelContainer, date: Date) {
self.date = date
modelContainer = container
}
}
@ModelActorX
also provides a constructor declared with @MainActor
. When you use this constructor to generate an actor, it will directly utilize the mainContext
(view context), and the entire actor will run on the main thread. The key difference from @MainModelActorX
is that the type remains an actor
. This means that existing code built upon ModelActor does not require modification—the calls will still retain await
.
This approach might be an ideal temporary solution before iOS 18 addresses the responsiveness issues related to updates using @ModelActor
.
@ModelActorX
actor DataHandler {}
Task{ @MainActor in
let handler = DataHandler(mainContext: container.mainContext) // Use the view context for construction
await handler.updateItem(id: id) // Even on the main thread, you can still use `await`
}
struct ModelActorXTests {
@Test func example1() async throws {
let container = createContainer()
let handler = DataHandler(modelContainer: container)
let now = Date.now
let id = try await handler.newItem(date: now)
let date = await handler.getTimestampFromItemID(id)
#expect(date == now)
}
@Test func example2() async throws {
let container = createContainer()
let now = Date.now
let handler = DataHandler1(container: container, date: now)
let id = try await handler.newItem()
let date = await handler.getTimestampFromItemID(id)
#expect(date == now)
}
}
@MainActor
struct MainModelActorXTests {
@Test
func test1() async throws {
let container = createContainer()
let handler = MainDataHandler(modelContainer: container)
let now = Date.now
let id = try handler.newItem(date: now)
let date = handler.getTimestampFromItemID(id)
#expect(date == now)
}
@Test
func test2() async throws {
let container = createContainer()
let now = Date.now
let handler = MainDataHandler1(container: container, date: now)
let id = try handler.newItem()
let date = handler.getTimestampFromItemID(id)
#expect(date == now)
}
}
disableGenerateInit: Bool
(optional): Controls whether an initializer is automatically generated. Default isfalse
.
disableGenerateInit: Bool
(optional): Controls whether an initializer is automatically generated. Default isfalse
.
- Swift 6
- iOS 17.0 / macOS 14 / tvOS 17 / watchOS 10 or later
ModelActorX is released under the MIT License. See LICENSE for details.
- Inspired by SwiftData's
ModelActor
functionality. - Core Data Version: CoreDataEvolution
- Thanks to the Swift community for continuous support and contributions.
Contributions are welcome! If you have ideas for improvements or find bugs, please open an issue or submit a pull request.