Skip to content

Commit 838e638

Browse files
committed
add disableGenerateInit to NSMainModelActor
1 parent 69d204f commit 838e638

File tree

13 files changed

+227
-37
lines changed

13 files changed

+227
-37
lines changed

.vscode/launch.json

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{
2+
"configurations": [
3+
{
4+
"type": "lldb",
5+
"request": "launch",
6+
"args": [],
7+
"cwd": "${workspaceFolder:CoreDataEvolution}",
8+
"name": "Debug CoreDataEvolutionClient",
9+
"program": "${workspaceFolder:CoreDataEvolution}/.build/debug/CoreDataEvolutionClient",
10+
"preLaunchTask": "swift: Build Debug CoreDataEvolutionClient"
11+
},
12+
{
13+
"type": "lldb",
14+
"request": "launch",
15+
"args": [],
16+
"cwd": "${workspaceFolder:CoreDataEvolution}",
17+
"name": "Release CoreDataEvolutionClient",
18+
"program": "${workspaceFolder:CoreDataEvolution}/.build/release/CoreDataEvolutionClient",
19+
"preLaunchTask": "swift: Build Release CoreDataEvolutionClient"
20+
}
21+
]
22+
}

Package.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,5 +54,5 @@ let package = Package(
5454
),
5555
.executableTarget(name: "CoreDataEvolutionClient", dependencies: ["CoreDataEvolution"]),
5656
],
57-
swiftLanguageModes: [.version("6")]
57+
swiftLanguageModes: [.v6]
5858
)

README.md

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ SwiftData introduced modern concurrency features like `@ModelActor`, making it e
2727

2828
- **@NSModelActor Macro**
2929
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.
30+
31+
- **NSMainModelActor Macro**
32+
`NSMainModelActor` will provide the same functionality as `NSModelActor`, but it will be used to declare a class that runs on the main thread.
3033

3134
- **Elegant Actor-based Concurrency**
3235
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.
@@ -54,6 +57,46 @@ In this example, the `@NSModelActor` macro simplifies the setup, automatically c
5457

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

60+
You can disable the automatic generation of the constructor by using `disableGenerateInit`:
61+
62+
```swift
63+
@NSModelActor(disableGenerateInit: true)
64+
public actor DataHandler {
65+
let viewName: String
66+
67+
func createNemItem(_ timestamp: Date = .now, showThread: Bool = false) throws -> NSManagedObjectID {
68+
let item = Item(context: modelContext)
69+
item.timestamp = timestamp
70+
try modelContext.save()
71+
return item.objectID
72+
}
73+
74+
init(container: NSPersistentContainer, viewName: String) {
75+
modelContainer = container
76+
self.viewName = viewName
77+
let context = container.newBackgroundContext()
78+
context.name = viewName
79+
modelExecutor = .init(context: context)
80+
}
81+
}
82+
```
83+
84+
NSMainModelActor will provide the same functionality as NSModelActor, but it will be used to declare a class that runs on the main thread:
85+
86+
```swift
87+
@MainActor
88+
@NSMainModelActor
89+
final class DataHandler {
90+
func updateItem(identifier: NSManagedObjectID, timestamp: Date) throws {
91+
guard let item = self[identifier, as: Item.self] else {
92+
throw MyError.objectNotExist
93+
}
94+
item.timestamp = timestamp
95+
try modelContext.save()
96+
}
97+
}
98+
```
99+
57100
## Installation
58101

59102
You can add CoreDataEvolution to your project using Swift Package Manager by adding the following dependency to your `Package.swift` file:

Sources/CoreDataEvolution/Macros.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,14 @@
66
//
77

88
import Foundation
9+
import SwiftData
10+
11+
// MARK: - Core Data Macro
912

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

1417
@attached(member, names: named(modelExecutor), named(modelContainer), named(init))
1518
@attached(extension, conformances: NSMainModelActor)
16-
public macro NSMainModelActor() = #externalMacro(module: "CoreDataEvolutionMacros", type: "NSMainModelActorMacro")
19+
public macro NSMainModelActor(disableGenerateInit: Bool = false) = #externalMacro(module: "CoreDataEvolutionMacros", type: "NSMainModelActorMacro")
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
//
2+
// ------------------------------------------------
3+
// Original project: CoreDataEvolution
4+
// Created on 2024/10/30 by Fatbobman(东坡肘子)
5+
// X: @fatbobman
6+
// Mastodon: @fatbobman@mastodon.social
7+
// GitHub: @fatbobman
8+
// Blog: https://fatbobman.com
9+
// ------------------------------------------------
10+
// Copyright © 2024-present Fatbobman. All rights reserved.
11+
12+
import Foundation
13+
import SwiftData
14+
15+
@MainActor
16+
public protocol MainModelActorX: AnyObject {
17+
/// Provides access to the NSPersistentContainer associated with the NSMainModelActor.
18+
var modelContainer: ModelContainer { get }
19+
}
20+
21+
extension MainModelActorX {
22+
/// Exposes the view context for model operations.
23+
public var modelContext: ModelContext {
24+
modelContainer.mainContext
25+
}
26+
27+
/// Retrieves a model instance based on its identifier, cast to the specified type.
28+
///
29+
/// 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.
30+
///
31+
/// - Parameters:
32+
/// - id: The identifier of the model to fetch.
33+
/// - as: The type to which the fetched model should be cast.
34+
/// - Returns: The fetched model instance cast to the specified type, or nil if not found.
35+
public subscript<T>(id: PersistentIdentifier, as: T.Type) -> T? where T: PersistentModel {
36+
let predicate = #Predicate<T> {
37+
$0.persistentModelID == id
38+
}
39+
if let object: T = modelContext.registeredModel(for: id) {
40+
return object
41+
}
42+
let fetchDescriptor = FetchDescriptor<T>(predicate: predicate)
43+
let object: T? = try? modelContext.fetch(fetchDescriptor).first
44+
return object
45+
}
46+
}

Sources/CoreDataEvolution/NSMainModelActor.swift

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,15 @@
1111

1212
import CoreData
1313

14+
/// A protocol that defines the properties and methods for accessing a Core Data model in a main actor context.
1415
@MainActor
15-
public protocol NSMainModelActor {
16+
public protocol NSMainModelActor: AnyObject {
1617
/// The NSPersistentContainer for the NSMainModelActor
1718
var modelContainer: NSPersistentContainer { get }
1819
}
1920

2021
extension NSMainModelActor {
21-
/// The view context
22+
/// The view context for the NSMainModelActor
2223
public var modelContext: NSManagedObjectContext {
2324
modelContainer.viewContext
2425
}

Sources/CoreDataEvolution/NSModelActor.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import _Concurrency
1313
import CoreData
1414
import Foundation
1515

16+
/// A protocol that defines the properties and methods for accessing a Core Data model in a model actor context.
1617
public protocol NSModelActor: Actor {
1718
/// The NSPersistentContainer for the NSModelActor
1819
nonisolated var modelContainer: NSPersistentContainer { get }

Sources/CoreDataEvolution/NSModelObjectContextExecutor.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import _Concurrency
1313
import CoreData
1414

15+
/// A class that coordinates access to the model actor.
1516
public final class NSModelObjectContextExecutor: @unchecked Sendable, SerialExecutor {
1617
public final let context: NSManagedObjectContext
1718
public init(context: NSManagedObjectContext) {
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
//
2+
// ------------------------------------------------
3+
// Original project: CoreDataEvolution
4+
// Created on 2024/10/30 by Fatbobman(东坡肘子)
5+
// X: @fatbobman
6+
// Mastodon: @fatbobman@mastodon.social
7+
// GitHub: @fatbobman
8+
// Blog: https://fatbobman.com
9+
// ------------------------------------------------
10+
// Copyright © 2024-present Fatbobman. All rights reserved.
11+
12+
import Foundation
13+
import SwiftSyntax
14+
import SwiftSyntaxMacros
15+
16+
/// Determines whether to generate an initializer based on the attribute node.
17+
///
18+
/// This function checks the attribute node for an argument labeled "disableGenerateInit" with a boolean value.
19+
/// If such an argument is found and its value is false, the function returns false, indicating that an initializer should not be generated.
20+
/// Otherwise, it returns true, indicating that an initializer should be generated.
21+
///
22+
/// - Parameter node: The attribute node to check.
23+
/// - Returns: A boolean indicating whether to generate an initializer.
24+
func shouldGenerateInitializer(from node: AttributeSyntax) -> Bool {
25+
guard let argumentList = node.arguments?.as(LabeledExprListSyntax.self) else {
26+
return true // Default to true if no arguments are present.
27+
}
28+
29+
for argument in argumentList {
30+
if argument.label?.text == "disableGenerateInit",
31+
let booleanLiteral = argument.expression.as(BooleanLiteralExprSyntax.self)
32+
{
33+
return booleanLiteral.literal.text != "true" // Return false if "disableGenerateInit" is set to true.
34+
}
35+
}
36+
return true // Default to true if "disableGenerateInit" is not found or is set to false.
37+
}
38+
39+
/// Checks if the access level of the declared type is public.
40+
///
41+
/// This function iterates through the modifiers of the declaration to check if the "public" access level is specified.
42+
///
43+
/// - Parameter declaration: The declaration to check.
44+
/// - Returns: A boolean indicating whether the access level is public.
45+
func isPublic(from declaration: some DeclGroupSyntax) -> Bool {
46+
return declaration.modifiers.contains { modifier in
47+
modifier.name.text == "public" // Check if the "public" modifier is present.
48+
}
49+
}

Sources/CoreDataEvolutionMacros/NSMainModelActorMacro.swift

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -28,15 +28,21 @@ import SwiftSyntaxMacros
2828
/// final class DataHandler{}
2929
/// public let modelContainer: CoreData.NSPersistentContainer
3030
///
31-
/// public init(container: CoreData.NSPersistentContainer) {
32-
/// modelContainer = container
31+
/// public init(modelContainer: CoreData.NSPersistentContainer) {
32+
/// self.modelContainer = modelContainer
3333
/// }
3434
/// extension DataHandler: CoreDataEvolution.NSModelActor {
3535
/// }
3636
public enum NSMainModelActorMacro {}
3737

3838
extension NSMainModelActorMacro: ExtensionMacro {
39-
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] {
39+
public static func expansion(
40+
of _: SwiftSyntax.AttributeSyntax,
41+
attachedTo _: some SwiftSyntax.DeclGroupSyntax,
42+
providingExtensionsOf type: some SwiftSyntax.TypeSyntaxProtocol,
43+
conformingTo _: [SwiftSyntax.TypeSyntax],
44+
in _: some SwiftSyntaxMacros.MacroExpansionContext
45+
) throws -> [SwiftSyntax.ExtensionDeclSyntax] {
4046
let decl: DeclSyntax =
4147
"""
4248
extension \(type.trimmed): CoreDataEvolution.NSMainModelActor {}
@@ -51,15 +57,26 @@ extension NSMainModelActorMacro: ExtensionMacro {
5157
}
5258

5359
extension NSMainModelActorMacro: MemberMacro {
54-
public static func expansion(of _: AttributeSyntax, providingMembersOf _: some DeclGroupSyntax, conformingTo _: [TypeSyntax], in _: some MacroExpansionContext) throws -> [DeclSyntax] {
55-
[
60+
public static func expansion(
61+
of node: AttributeSyntax,
62+
providingMembersOf declaration: some DeclGroupSyntax,
63+
conformingTo _: [TypeSyntax],
64+
in _: some MacroExpansionContext
65+
) throws -> [DeclSyntax] {
66+
let generateInitializer = shouldGenerateInitializer(from: node)
67+
let accessModifier = isPublic(from: declaration) ? "public " : ""
68+
69+
let decl: DeclSyntax =
70+
"""
71+
\(raw: accessModifier)let modelContainer: CoreData.NSPersistentContainer
5672
"""
57-
public let modelContainer: CoreData.NSPersistentContainer
5873

59-
public init(container: CoreData.NSPersistentContainer) {
60-
modelContainer = container
74+
let initializer: DeclSyntax? = generateInitializer ?
75+
"""
76+
\(raw: accessModifier)init(modelContainer: CoreData.NSPersistentContainer) {
77+
self.modelContainer = modelContainer
6178
}
62-
""",
63-
]
79+
""" : nil
80+
return [decl] + (initializer.map { [$0] } ?? [])
6481
}
6582
}

0 commit comments

Comments
 (0)