Skip to content

Commit

Permalink
add disableGenerateInit to NSMainModelActor
Browse files Browse the repository at this point in the history
  • Loading branch information
fatbobman committed Oct 30, 2024
1 parent 69d204f commit 838e638
Show file tree
Hide file tree
Showing 13 changed files with 227 additions and 37 deletions.
22 changes: 22 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"configurations": [
{
"type": "lldb",
"request": "launch",
"args": [],
"cwd": "${workspaceFolder:CoreDataEvolution}",
"name": "Debug CoreDataEvolutionClient",
"program": "${workspaceFolder:CoreDataEvolution}/.build/debug/CoreDataEvolutionClient",
"preLaunchTask": "swift: Build Debug CoreDataEvolutionClient"
},
{
"type": "lldb",
"request": "launch",
"args": [],
"cwd": "${workspaceFolder:CoreDataEvolution}",
"name": "Release CoreDataEvolutionClient",
"program": "${workspaceFolder:CoreDataEvolution}/.build/release/CoreDataEvolutionClient",
"preLaunchTask": "swift: Build Release CoreDataEvolutionClient"
}
]
}
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -54,5 +54,5 @@ let package = Package(
),
.executableTarget(name: "CoreDataEvolutionClient", dependencies: ["CoreDataEvolution"]),
],
swiftLanguageModes: [.version("6")]
swiftLanguageModes: [.v6]
)
43 changes: 43 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ SwiftData introduced modern concurrency features like `@ModelActor`, making it e

- **@NSModelActor Macro**
The `@NSModelActor` macro simplifies Core Data concurrency, mirroring SwiftData’s `@ModelActor` macro. It generates the necessary boilerplate code to manage a Core Data stack within an actor, ensuring safe and efficient access to managed objects.

- **NSMainModelActor Macro**
`NSMainModelActor` will provide the same functionality as `NSModelActor`, but it will be used to declare a class that runs on the main thread.

- **Elegant Actor-based Concurrency**
CoreDataEvolution allows you to create actors with custom executors tied to Core Data contexts, ensuring that all operations within the actor are executed serially on the context’s thread.
Expand Down Expand Up @@ -54,6 +57,46 @@ In this example, the `@NSModelActor` macro simplifies the setup, automatically c

This approach allows you to safely integrate modern Swift concurrency mechanisms into your existing Core Data stack, enhancing performance and code clarity.

You can disable the automatic generation of the constructor by using `disableGenerateInit`:

```swift
@NSModelActor(disableGenerateInit: true)
public actor DataHandler {
let viewName: String

func createNemItem(_ 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
self.viewName = viewName
let context = container.newBackgroundContext()
context.name = viewName
modelExecutor = .init(context: context)
}
}
```

NSMainModelActor will provide the same functionality as NSModelActor, but it will be used to declare a class that runs on the main thread:

```swift
@MainActor
@NSMainModelActor
final class DataHandler {
func updateItem(identifier: NSManagedObjectID, timestamp: Date) throws {
guard let item = self[identifier, as: Item.self] else {
throw MyError.objectNotExist
}
item.timestamp = timestamp
try modelContext.save()
}
}
```

## Installation

You can add CoreDataEvolution to your project using Swift Package Manager by adding the following dependency to your `Package.swift` file:
Expand Down
5 changes: 4 additions & 1 deletion Sources/CoreDataEvolution/Macros.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,14 @@
//

import Foundation
import SwiftData

// MARK: - Core Data Macro

@attached(member, names: named(modelExecutor), named(modelContainer), named(init))
@attached(extension, conformances: NSModelActor)
public macro NSModelActor(disableGenerateInit: Bool = false) = #externalMacro(module: "CoreDataEvolutionMacros", type: "NSModelActorMacro")

@attached(member, names: named(modelExecutor), named(modelContainer), named(init))
@attached(extension, conformances: NSMainModelActor)
public macro NSMainModelActor() = #externalMacro(module: "CoreDataEvolutionMacros", type: "NSMainModelActorMacro")
public macro NSMainModelActor(disableGenerateInit: Bool = false) = #externalMacro(module: "CoreDataEvolutionMacros", type: "NSMainModelActorMacro")
46 changes: 46 additions & 0 deletions Sources/CoreDataEvolution/MainModelActor.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
//
// ------------------------------------------------
// Original project: CoreDataEvolution
// Created on 2024/10/30 by Fatbobman(东坡肘子)
// X: @fatbobman
// Mastodon: @[email protected]
// GitHub: @fatbobman
// Blog: https://fatbobman.com
// ------------------------------------------------
// Copyright © 2024-present Fatbobman. All rights reserved.

import Foundation
import SwiftData

@MainActor
public protocol MainModelActorX: AnyObject {
/// Provides access to the NSPersistentContainer associated with the NSMainModelActor.
var modelContainer: ModelContainer { get }
}

extension MainModelActorX {
/// Exposes the view context for model operations.
public var modelContext: ModelContext {
modelContainer.mainContext
}

/// Retrieves a model instance based on its identifier, cast to the specified type.
///
/// This method attempts to fetch a model instance from the context using the provided identifier. If the model is not found, it constructs a fetch descriptor with a predicate matching the identifier and attempts to fetch the model. The fetched model is then cast to the specified type.
///
/// - Parameters:
/// - id: The identifier of the model to fetch.
/// - as: The type to which the fetched model should be cast.
/// - Returns: The fetched model instance cast to the specified type, or nil if not found.
public subscript<T>(id: PersistentIdentifier, as: T.Type) -> T? where T: PersistentModel {
let predicate = #Predicate<T> {
$0.persistentModelID == id
}
if let object: T = modelContext.registeredModel(for: id) {
return object
}
let fetchDescriptor = FetchDescriptor<T>(predicate: predicate)
let object: T? = try? modelContext.fetch(fetchDescriptor).first
return object
}
}
5 changes: 3 additions & 2 deletions Sources/CoreDataEvolution/NSMainModelActor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,15 @@

import CoreData

/// A protocol that defines the properties and methods for accessing a Core Data model in a main actor context.
@MainActor
public protocol NSMainModelActor {
public protocol NSMainModelActor: AnyObject {
/// The NSPersistentContainer for the NSMainModelActor
var modelContainer: NSPersistentContainer { get }
}

extension NSMainModelActor {
/// The view context
/// The view context for the NSMainModelActor
public var modelContext: NSManagedObjectContext {
modelContainer.viewContext
}
Expand Down
1 change: 1 addition & 0 deletions Sources/CoreDataEvolution/NSModelActor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import _Concurrency
import CoreData
import Foundation

/// A protocol that defines the properties and methods for accessing a Core Data model in a model actor context.
public protocol NSModelActor: Actor {
/// The NSPersistentContainer for the NSModelActor
nonisolated var modelContainer: NSPersistentContainer { get }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import _Concurrency
import CoreData

/// A class that coordinates access to the model actor.
public final class NSModelObjectContextExecutor: @unchecked Sendable, SerialExecutor {
public final let context: NSManagedObjectContext
public init(context: NSManagedObjectContext) {
Expand Down
49 changes: 49 additions & 0 deletions Sources/CoreDataEvolutionMacros/Helper.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
//
// ------------------------------------------------
// Original project: CoreDataEvolution
// Created on 2024/10/30 by Fatbobman(东坡肘子)
// X: @fatbobman
// Mastodon: @[email protected]
// GitHub: @fatbobman
// Blog: https://fatbobman.com
// ------------------------------------------------
// Copyright © 2024-present Fatbobman. All rights reserved.

import Foundation
import SwiftSyntax
import SwiftSyntaxMacros

/// Determines whether to generate an initializer based on the attribute node.
///
/// This function checks the attribute node for an argument labeled "disableGenerateInit" with a boolean value.
/// If such an argument is found and its value is false, the function returns false, indicating that an initializer should not be generated.
/// Otherwise, it returns true, indicating that an initializer should be generated.
///
/// - Parameter node: The attribute node to check.
/// - Returns: A boolean indicating whether to generate an initializer.
func shouldGenerateInitializer(from node: AttributeSyntax) -> Bool {
guard let argumentList = node.arguments?.as(LabeledExprListSyntax.self) else {
return true // Default to true if no arguments are present.
}

for argument in argumentList {
if argument.label?.text == "disableGenerateInit",
let booleanLiteral = argument.expression.as(BooleanLiteralExprSyntax.self)
{
return booleanLiteral.literal.text != "true" // Return false if "disableGenerateInit" is set to true.
}
}
return true // Default to true if "disableGenerateInit" is not found or is set to false.
}

/// Checks if the access level of the declared type is public.
///
/// This function iterates through the modifiers of the declaration to check if the "public" access level is specified.
///
/// - Parameter declaration: The declaration to check.
/// - Returns: A boolean indicating whether the access level is public.
func isPublic(from declaration: some DeclGroupSyntax) -> Bool {
return declaration.modifiers.contains { modifier in
modifier.name.text == "public" // Check if the "public" modifier is present.
}
}
37 changes: 27 additions & 10 deletions Sources/CoreDataEvolutionMacros/NSMainModelActorMacro.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,21 @@ import SwiftSyntaxMacros
/// final class DataHandler{}
/// public let modelContainer: CoreData.NSPersistentContainer
///
/// public init(container: CoreData.NSPersistentContainer) {
/// modelContainer = container
/// public init(modelContainer: CoreData.NSPersistentContainer) {
/// self.modelContainer = modelContainer
/// }
/// extension DataHandler: CoreDataEvolution.NSModelActor {
/// }
public enum NSMainModelActorMacro {}

extension NSMainModelActorMacro: ExtensionMacro {
public static func expansion(of _: SwiftSyntax.AttributeSyntax, attachedTo _: some SwiftSyntax.DeclGroupSyntax, providingExtensionsOf type: some SwiftSyntax.TypeSyntaxProtocol, conformingTo _: [SwiftSyntax.TypeSyntax], in _: some SwiftSyntaxMacros.MacroExpansionContext) throws -> [SwiftSyntax.ExtensionDeclSyntax] {
public static func expansion(
of _: SwiftSyntax.AttributeSyntax,
attachedTo _: some SwiftSyntax.DeclGroupSyntax,
providingExtensionsOf type: some SwiftSyntax.TypeSyntaxProtocol,
conformingTo _: [SwiftSyntax.TypeSyntax],
in _: some SwiftSyntaxMacros.MacroExpansionContext
) throws -> [SwiftSyntax.ExtensionDeclSyntax] {
let decl: DeclSyntax =
"""
extension \(type.trimmed): CoreDataEvolution.NSMainModelActor {}
Expand All @@ -51,15 +57,26 @@ extension NSMainModelActorMacro: ExtensionMacro {
}

extension NSMainModelActorMacro: MemberMacro {
public static func expansion(of _: AttributeSyntax, providingMembersOf _: some DeclGroupSyntax, conformingTo _: [TypeSyntax], in _: some MacroExpansionContext) throws -> [DeclSyntax] {
[
public static func expansion(
of node: AttributeSyntax,
providingMembersOf declaration: some DeclGroupSyntax,
conformingTo _: [TypeSyntax],
in _: some MacroExpansionContext
) throws -> [DeclSyntax] {
let generateInitializer = shouldGenerateInitializer(from: node)
let accessModifier = isPublic(from: declaration) ? "public " : ""

let decl: DeclSyntax =
"""
\(raw: accessModifier)let modelContainer: CoreData.NSPersistentContainer
"""
public let modelContainer: CoreData.NSPersistentContainer

public init(container: CoreData.NSPersistentContainer) {
modelContainer = container
let initializer: DeclSyntax? = generateInitializer ?
"""
\(raw: accessModifier)init(modelContainer: CoreData.NSPersistentContainer) {
self.modelContainer = modelContainer
}
""",
]
""" : nil
return [decl] + (initializer.map { [$0] } ?? [])
}
}
35 changes: 16 additions & 19 deletions Sources/CoreDataEvolutionMacros/NSModelActorMacro.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,13 @@ import SwiftSyntaxMacros
public enum NSModelActorMacro {}

extension NSModelActorMacro: ExtensionMacro {
public static func expansion(of _: SwiftSyntax.AttributeSyntax, attachedTo _: some SwiftSyntax.DeclGroupSyntax, providingExtensionsOf type: some SwiftSyntax.TypeSyntaxProtocol, conformingTo _: [SwiftSyntax.TypeSyntax], in _: some SwiftSyntaxMacros.MacroExpansionContext) throws -> [SwiftSyntax.ExtensionDeclSyntax] {
public static func expansion(
of _: SwiftSyntax.AttributeSyntax,
attachedTo _: some SwiftSyntax.DeclGroupSyntax,
providingExtensionsOf type: some SwiftSyntax.TypeSyntaxProtocol,
conformingTo _: [SwiftSyntax.TypeSyntax],
in _: some SwiftSyntaxMacros.MacroExpansionContext
) throws -> [SwiftSyntax.ExtensionDeclSyntax] {
let decl: DeclSyntax =
"""
extension \(type.trimmed): CoreDataEvolution.NSModelActor {}
Expand All @@ -50,36 +56,27 @@ extension NSModelActorMacro: ExtensionMacro {
extension NSModelActorMacro: MemberMacro {
public static func expansion(
of node: AttributeSyntax,
providingMembersOf _: some DeclGroupSyntax,
providingMembersOf declaration: some DeclGroupSyntax,
conformingTo _: [TypeSyntax],
in _: some MacroExpansionContext
) throws -> [DeclSyntax] {
var generateInitializer = true
if let argumentList = node.arguments?.as(LabeledExprListSyntax.self) {
for argument in argumentList {
if argument.label?.text == "disableGenerateInit",
let booleanLiteral = argument.expression.as(BooleanLiteralExprSyntax.self)
{
generateInitializer = booleanLiteral.literal.text != "true"
}
}
}

let generateInitializer = shouldGenerateInitializer(from: node)
let accessModifier = isPublic(from: declaration) ? "public " : ""
let decl: DeclSyntax =
"""
public nonisolated let modelExecutor: CoreDataEvolution.NSModelObjectContextExecutor
public nonisolated let modelContainer: CoreData.NSPersistentContainer
\(raw: accessModifier)nonisolated let modelExecutor: CoreDataEvolution.NSModelObjectContextExecutor
\(raw: accessModifier)nonisolated let modelContainer: CoreData.NSPersistentContainer
"""
let initializer: DeclSyntax = generateInitializer ?
let initializer: DeclSyntax? = generateInitializer ?
"""
public init(container: CoreData.NSPersistentContainer) {
\(raw: accessModifier)init(container: CoreData.NSPersistentContainer) {
let context: NSManagedObjectContext
context = container.newBackgroundContext()
modelExecutor = CoreDataEvolution.NSModelObjectContextExecutor(context: context)
modelContainer = container
}
""" : ""
return [decl, initializer]
""" : nil
return [decl] + (initializer.map { [$0] } ?? [])
}
}
14 changes: 12 additions & 2 deletions Tests/CoreDataEvolutionTests/Helper/DataHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@
import CoreData
import CoreDataEvolution

@NSModelActor
actor DataHandler {
@NSModelActor(disableGenerateInit: true)
public actor DataHandler {
let viewName: String

func createNemItem(_ timestamp: Date = .now, showThread: Bool = false) throws -> NSManagedObjectID {
let item = Item(context: modelContext)
item.timestamp = timestamp
Expand Down Expand Up @@ -44,4 +46,12 @@ actor DataHandler {
func getItemCount() throws -> Int {
try getAllItems().count
}

init(container: NSPersistentContainer, viewName: String) {
modelContainer = container
self.viewName = viewName
let context = container.newBackgroundContext()
context.name = viewName
modelExecutor = .init(context: context)
}
}
4 changes: 2 additions & 2 deletions Tests/CoreDataEvolutionTests/NSModelActorTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import Testing
struct NSModelActorTests {
@Test func createNewItem() async throws {
let stack = TestStack()
let handler = DataHandler(container: stack.container)
let handler = DataHandler(container: stack.container, viewName: "hello")
let id = try await handler.createNemItem(showThread: true)
let count = try await handler.getItemCount()
#expect(count == 1)
Expand All @@ -17,7 +17,7 @@ struct NSModelActorTests {
@MainActor
@Test func createNewItemInMainActor() throws {
let stack = TestStack()
let handler = MainHandler(container: stack.container)
let handler = MainHandler(modelContainer: stack.container)
_ = try handler.createNemItem(showThread: true)
let count = try handler.getItemCount()
#expect(count == 1)
Expand Down

0 comments on commit 838e638

Please sign in to comment.