├── .vscode
├── settings.json
└── launch.json
├── .gitignore
├── .editorconfig
├── .swiftpm
└── xcode
│ ├── package.xcworkspace
│ └── xcshareddata
│ │ └── IDEWorkspaceChecks.plist
│ └── xcshareddata
│ └── xcschemes
│ ├── CoreDataEvolution.xcscheme
│ ├── CoreDataEvolutionMacros.xcscheme
│ └── CoreDataEvolution-Package.xcscheme
├── Sources
├── CoreDataEvolutionMacros
│ ├── Plugins.swift
│ ├── Helper.swift
│ ├── NSMainModelActorMacro.swift
│ └── NSModelActorMacro.swift
├── CoreDataEvolution
│ ├── module.swift
│ ├── Macros.swift
│ ├── NSMainModelActor.swift
│ ├── NSModelObjectContextExecutor.swift
│ ├── NSModelActor.swift
│ ├── MainModelActor.swift
│ └── CoreDataEvolution.docc
│ │ └── CoreDataEvolution.md
└── CoreDataEvolutionClient
│ └── main.swift
├── Package.resolved
├── Tests
└── CoreDataEvolutionTests
│ ├── Resources
│ └── TestModel.xcdatamodeld
│ │ └── TestModel.xcdatamodel
│ │ └── contents
│ ├── CoreDataEvolution-Package.xctestplan
│ ├── Helper
│ ├── Item.swift
│ ├── Container.swift
│ ├── MainHandler.swift
│ └── DataHandler.swift
│ └── NSModelActorTests.swift
├── .github
└── FUNDING.yml
├── LICENSE
├── Package.swift
├── .swiftformat
└── README.md
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {}
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | /.build
3 | /Packages
4 | xcuserdata/
5 | DerivedData/
6 | .swiftpm/
7 | .netrc
8 | .vscode/
9 | *.resolved
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | [*.swift]
2 | indent_style = space
3 | indent_size = 4
4 | tab_width = 4
5 | end_of_line = crlf
6 | insert_final_newline = false
7 | max_line_length = 120
8 | trim_trailing_whitespace = true
9 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Sources/CoreDataEvolutionMacros/Plugins.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Plugins.swift
3 | //
4 | //
5 | // Created by Yang Xu on 2024/4/9.
6 | //
7 |
8 | import SwiftCompilerPlugin
9 | import SwiftSyntaxMacros
10 |
11 | @main
12 | struct CoreDataEvolutionMacrosPlugin: CompilerPlugin {
13 | let providingMacros: [Macro.Type] = [
14 | NSModelActorMacro.self,
15 | NSMainModelActorMacro.self,
16 | ]
17 | }
18 |
--------------------------------------------------------------------------------
/Package.resolved:
--------------------------------------------------------------------------------
1 | {
2 | "originHash" : "f2b467ca01d012f18b77cd50068cadcf6b93a6bf6077bb9cf25e6d7bd2007095",
3 | "pins" : [
4 | {
5 | "identity" : "swift-syntax",
6 | "kind" : "remoteSourceControl",
7 | "location" : "https://github.com/swiftlang/swift-syntax",
8 | "state" : {
9 | "revision" : "0687f71944021d616d34d922343dcef086855920",
10 | "version" : "600.0.1"
11 | }
12 | }
13 | ],
14 | "version" : 3
15 | }
16 |
--------------------------------------------------------------------------------
/Sources/CoreDataEvolution/module.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ------------------------------------------------
3 | // Original project: CoreDataEvolution
4 | // Created on 2024/9/28 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 | @_exported import CoreData
13 |
--------------------------------------------------------------------------------
/Tests/CoreDataEvolutionTests/Resources/TestModel.xcdatamodeld/TestModel.xcdatamodel/contents:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/Sources/CoreDataEvolutionClient/main.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ------------------------------------------------
3 | // Original project: CoreDataEvolution
4 | // Created on 2024/10/6 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 |
13 | import Foundation
14 | import CoreDataEvolution
15 |
16 | @available(macOS 14.0, iOS 17.0, tvOS 17.0, macCatalyst 17.0, watchOS 10.0, visionOS 1.0, *)
17 | @NSModelActor
18 | actor DataHandler {}
--------------------------------------------------------------------------------
/Sources/CoreDataEvolution/Macros.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Macros.swift
3 | //
4 | //
5 | // Created by Yang Xu on 2024/4/9.
6 | //
7 |
8 | import Foundation
9 | import SwiftData
10 |
11 | // MARK: - Core Data Macro
12 |
13 | @attached(member, names: named(modelExecutor), named(modelContainer), named(init))
14 | @attached(extension, conformances: NSModelActor)
15 | public macro NSModelActor(disableGenerateInit: Bool = false) = #externalMacro(module: "CoreDataEvolutionMacros", type: "NSModelActorMacro")
16 |
17 | @attached(member, names: named(modelExecutor), named(modelContainer), named(init))
18 | @attached(extension, conformances: NSMainModelActor)
19 | public macro NSMainModelActor(disableGenerateInit: Bool = false) = #externalMacro(module: "CoreDataEvolutionMacros", type: "NSMainModelActorMacro")
20 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
4 | patreon: fatbobman
5 | open_collective: # Replace with a single Open Collective username
6 | ko_fi: # Replace with a single Ko-fi username
7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
9 | liberapay: # Replace with a single Liberapay username
10 | issuehunt: # Replace with a single IssueHunt username
11 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
12 | polar: # Replace with a single Polar username
13 | buy_me_a_coffee: fatbobman
14 | thanks_dev: # Replace with a single thanks.dev username
15 | custom: ['https://afdian.com','https://www.paypal.com/paypalme/fatbobman']
16 |
17 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
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 | }
--------------------------------------------------------------------------------
/Tests/CoreDataEvolutionTests/CoreDataEvolution-Package.xctestplan:
--------------------------------------------------------------------------------
1 | {
2 | "configurations" : [
3 | {
4 | "id" : "174B9628-1144-4305-8319-78EC92BD9520",
5 | "name" : "Test Scheme Action",
6 | "options" : {
7 |
8 | }
9 | }
10 | ],
11 | "defaultOptions" : {
12 | "commandLineArgumentEntries" : [
13 | {
14 | "argument" : "-com.apple.CoreData.ConcurrencyDebug 1"
15 | }
16 | ],
17 | "environmentVariableEntries" : [
18 |
19 | ],
20 | "targetForVariableExpansion" : {
21 | "containerPath" : "container:",
22 | "identifier" : "CoreDataEvolutionTests",
23 | "name" : "CoreDataEvolutionTests"
24 | }
25 | },
26 | "testTargets" : [
27 | {
28 | "target" : {
29 | "containerPath" : "container:",
30 | "identifier" : "CoreDataEvolutionTests",
31 | "name" : "CoreDataEvolutionTests"
32 | }
33 | }
34 | ],
35 | "version" : 1
36 | }
37 |
--------------------------------------------------------------------------------
/Tests/CoreDataEvolutionTests/Helper/Item.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ------------------------------------------------
3 | // Original project: CoreDataEvolution
4 | // Created on 2024/8/22 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 | @preconcurrency import CoreData
13 |
14 | @objc(Item)
15 | public class Item: NSManagedObject {}
16 | extension Item {
17 | @nonobjc public class func fetchRequest() -> NSFetchRequest- {
18 | return NSFetchRequest
- (entityName: "Item")
19 | }
20 |
21 | @NSManaged public var timestamp: Date?
22 | }
23 |
24 | extension Item {
25 | static let fetchAll: NSFetchRequest
- = {
26 | let request = NSFetchRequest
- (entityName: "Item")
27 | request.sortDescriptors = [.init(key: "timestamp", ascending: true)]
28 | return request
29 | }()
30 | }
31 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Fatbobman(东坡肘子)
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Sources/CoreDataEvolution/NSMainModelActor.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ------------------------------------------------
3 | // Original project: CoreDataEvolution
4 | // Created on 2024/9/20 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 CoreData
13 |
14 | /// A protocol that defines the properties and methods for accessing a Core Data model in a main actor context.
15 | @MainActor
16 | public protocol NSMainModelActor: AnyObject {
17 | /// The NSPersistentContainer for the NSMainModelActor
18 | var modelContainer: NSPersistentContainer { get }
19 | }
20 |
21 | extension NSMainModelActor {
22 | /// The view context for the NSMainModelActor
23 | public var modelContext: NSManagedObjectContext {
24 | modelContainer.viewContext
25 | }
26 |
27 | /// Returns the model for the specified identifier, downcast to the appropriate class.
28 | public subscript(id: NSManagedObjectID, as _: T.Type) -> T? where T: NSManagedObject {
29 | try? modelContext.existingObject(with: id) as? T
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/Sources/CoreDataEvolution/NSModelObjectContextExecutor.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ------------------------------------------------
3 | // Original project: CoreDataEvolution
4 | // Created on 2024/9/20 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 _Concurrency
13 | import CoreData
14 |
15 | /// A class that coordinates access to the model actor.
16 | public final class NSModelObjectContextExecutor: @unchecked Sendable, SerialExecutor {
17 | public final let context: NSManagedObjectContext
18 | public init(context: NSManagedObjectContext) {
19 | self.context = context
20 | }
21 |
22 | public func enqueue(_ job: consuming ExecutorJob) {
23 | let unownedJob = UnownedJob(job)
24 | let unownedExecutor = asUnownedSerialExecutor()
25 | context.perform {
26 | unownedJob.runSynchronously(on: unownedExecutor)
27 | }
28 | }
29 |
30 | public func asUnownedSerialExecutor() -> UnownedSerialExecutor {
31 | UnownedSerialExecutor(ordinary: self)
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/Tests/CoreDataEvolutionTests/Helper/Container.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ------------------------------------------------
3 | // Original project: CoreDataEvolution
4 | // Created on 2024/8/22 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 | @preconcurrency import CoreData
13 |
14 | import Foundation
15 |
16 | final class TestStack {
17 | @MainActor var viewContext: NSManagedObjectContext {
18 | container.viewContext
19 | }
20 |
21 | static let model: NSManagedObjectModel = {
22 | guard let modelURL = Bundle.module.url(forResource: "TestModel", withExtension: "momd"),
23 | let model = NSManagedObjectModel(contentsOf: modelURL)
24 | else {
25 | fatalError("Can't load DataModel")
26 | }
27 | return model
28 | }()
29 |
30 | let container: NSPersistentContainer
31 |
32 | init(url: URL = URL(fileURLWithPath: "/dev/null")) {
33 | container = NSPersistentContainer(name: "TestModel", managedObjectModel: Self.model)
34 | container.persistentStoreDescriptions.first!.url = url
35 | container.loadPersistentStores { _, error in
36 | if let error = error as NSError? {
37 | fatalError("Unresolved error \(error), \(error.userInfo)")
38 | }
39 | }
40 | container.viewContext.automaticallyMergesChangesFromParent = true
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/Tests/CoreDataEvolutionTests/Helper/MainHandler.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ------------------------------------------------
3 | // Original project: CoreDataEvolution
4 | // Created on 2024/9/20 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 CoreData
13 | import CoreDataEvolution
14 |
15 | @MainActor
16 | @NSMainModelActor
17 | final class MainHandler {
18 | func createNemItem(_ timestamp: Date = .now, showThread: Bool = false) throws -> NSManagedObjectID {
19 | let item = Item(context: modelContext)
20 | item.timestamp = timestamp
21 | if showThread {
22 | print(Thread.current)
23 | }
24 | try modelContext.save()
25 | return item.objectID
26 | }
27 |
28 | func delItem(_ item: Item) throws {
29 | modelContext.delete(item)
30 | try modelContext.save()
31 | }
32 |
33 | func delItem(_ itemID: NSManagedObjectID) throws {
34 | guard let item = try modelContext.existingObject(with: itemID) as? Item else {
35 | fatalError("Can't load model by ID:\(itemID)")
36 | }
37 | try delItem(item)
38 | }
39 |
40 | private func getAllItems() throws -> [Item] {
41 | let request = Item.fetchRequest()
42 | return try modelContext.fetch(request)
43 | }
44 |
45 | func getItemCount() throws -> Int {
46 | try getAllItems().count
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/Sources/CoreDataEvolution/NSModelActor.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ------------------------------------------------
3 | // Original project: CoreDataEvolution
4 | // Created on 2024/4/9 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 _Concurrency
13 | import CoreData
14 | import Foundation
15 |
16 | /// A protocol that defines the properties and methods for accessing a Core Data model in a model actor context.
17 | public protocol NSModelActor: Actor {
18 | /// The NSPersistentContainer for the NSModelActor
19 | nonisolated var modelContainer: NSPersistentContainer { get }
20 |
21 | /// The executor that coordinates access to the model actor.
22 | nonisolated var modelExecutor: NSModelObjectContextExecutor { get }
23 | }
24 |
25 | extension NSModelActor {
26 | /// The optimized, unonwned reference to the model actor's executor.
27 | public nonisolated var unownedExecutor: UnownedSerialExecutor {
28 | modelExecutor.asUnownedSerialExecutor()
29 | }
30 |
31 | /// The context that serializes any code running on the model actor.
32 | public var modelContext: NSManagedObjectContext {
33 | modelExecutor.context
34 | }
35 |
36 | /// Returns the model for the specified identifier, downcast to the appropriate class.
37 | public subscript(id: NSManagedObjectID, as _: T.Type) -> T? where T: NSManagedObject {
38 | try? modelContext.existingObject(with: id) as? T
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/Tests/CoreDataEvolutionTests/Helper/DataHandler.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ------------------------------------------------
3 | // Original project: CoreDataEvolution
4 | // Created on 2024/8/22 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 CoreData
13 | import CoreDataEvolution
14 |
15 | @NSModelActor(disableGenerateInit: true)
16 | public actor DataHandler {
17 | let viewName: String
18 |
19 | func createNemItem(_ timestamp: Date = .now, showThread: Bool = false) throws -> NSManagedObjectID {
20 | let item = Item(context: modelContext)
21 | item.timestamp = timestamp
22 | if showThread {
23 | print(Thread.current)
24 | }
25 | try modelContext.save()
26 | return item.objectID
27 | }
28 |
29 | func delItem(_ item: Item) throws {
30 | modelContext.delete(item)
31 | try modelContext.save()
32 | }
33 |
34 | func delItem(_ itemID: NSManagedObjectID) throws {
35 | guard let item = self[itemID, as: Item.self] else {
36 | fatalError("Can't load model by ID:\(itemID)")
37 | }
38 | try delItem(item)
39 | }
40 |
41 | private func getAllItems() throws -> [Item] {
42 | let request = Item.fetchRequest()
43 | return try modelContext.fetch(request)
44 | }
45 |
46 | func getItemCount() throws -> Int {
47 | try getAllItems().count
48 | }
49 |
50 | init(container: NSPersistentContainer, viewName: String) {
51 | modelContainer = container
52 | self.viewName = viewName
53 | let context = container.newBackgroundContext()
54 | context.name = viewName
55 | modelExecutor = .init(context: context)
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/Sources/CoreDataEvolution/MainModelActor.swift:
--------------------------------------------------------------------------------
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(id: PersistentIdentifier, as: T.Type) -> T? where T: PersistentModel {
36 | let predicate = #Predicate {
37 | $0.persistentModelID == id
38 | }
39 | if let object: T = modelContext.registeredModel(for: id) {
40 | return object
41 | }
42 | let fetchDescriptor = FetchDescriptor(predicate: predicate)
43 | let object: T? = try? modelContext.fetch(fetchDescriptor).first
44 | return object
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version: 6.0
2 | // The swift-tools-version declares the minimum version of Swift required to build this package.
3 |
4 | import CompilerPluginSupport
5 | import Foundation
6 | import PackageDescription
7 |
8 | let package = Package(
9 | name: "CoreDataEvolution",
10 | platforms: [
11 | .iOS(.v17),
12 | .macOS(.v14),
13 | .tvOS(.v17),
14 | .watchOS(.v10),
15 | ],
16 | products: [
17 | // Products define the executables and libraries a package produces, making them visible to other packages.
18 | .library(
19 | name: "CoreDataEvolution",
20 | targets: ["CoreDataEvolution"]
21 | ),
22 | .executable(
23 | name: "CoreDataEvolutionClient",
24 | targets: ["CoreDataEvolutionClient"]
25 | ),
26 | ],
27 | dependencies: [
28 | .package(url: "https://github.com/swiftlang/swift-syntax", "509.0.0"..<"602.0.0")
29 | ],
30 | targets: [
31 | // Targets are the basic building blocks of a package, defining a module or a test suite.
32 | // Targets can depend on other targets in this package and products from dependencies.
33 | .macro(
34 | name: "CoreDataEvolutionMacros",
35 | dependencies: [
36 | .product(name: "SwiftSyntaxMacros", package: "swift-syntax"),
37 | .product(name: "SwiftCompilerPlugin", package: "swift-syntax"),
38 | ]
39 | ),
40 | .target(
41 | name: "CoreDataEvolution",
42 | dependencies: [
43 | "CoreDataEvolutionMacros"
44 | ]
45 | ),
46 | .testTarget(
47 | name: "CoreDataEvolutionTests",
48 | dependencies: [
49 | "CoreDataEvolution",
50 | ],
51 | resources: [
52 | .process("Resources"),
53 | ]
54 | ),
55 | .executableTarget(name: "CoreDataEvolutionClient", dependencies: ["CoreDataEvolution"]),
56 | ],
57 | swiftLanguageModes: [.v6]
58 | )
59 |
--------------------------------------------------------------------------------
/Sources/CoreDataEvolutionMacros/Helper.swift:
--------------------------------------------------------------------------------
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 | }
--------------------------------------------------------------------------------
/.swiftformat:
--------------------------------------------------------------------------------
1 | --acronyms ID,URL,UUID
2 | --allman false
3 | --anonymousforeach convert
4 | --assetliterals visual-width
5 | --asynccapturing
6 | --beforemarks
7 | --binarygrouping 4,8
8 | --callsiteparen default
9 | --categorymark "MARK: %c"
10 | --classthreshold 0
11 | --closingparen balanced
12 | --closurevoid remove
13 | --commas always
14 | --complexattrs preserve
15 | --computedvarattrs preserve
16 | --condassignment after-property
17 | --conflictmarkers reject
18 | --dateformat system
19 | --decimalgrouping 3,6
20 | --doccomments before-declarations
21 | --elseposition same-line
22 | --emptybraces no-space
23 | --enumnamespaces always
24 | --enumthreshold 0
25 | --exponentcase lowercase
26 | --exponentgrouping disabled
27 | --extensionacl on-declarations
28 | --extensionlength 0
29 | --extensionmark "MARK: - %t + %c"
30 | --fractiongrouping disabled
31 | --fragment false
32 | --funcattributes preserve
33 | --generictypes
34 | --groupedextension "MARK: %c"
35 | --guardelse auto
36 | --header ignore
37 | --hexgrouping 4,8
38 | --hexliteralcase uppercase
39 | --ifdef indent
40 | --importgrouping alpha
41 | --indent 4
42 | --indentcase false
43 | --indentstrings false
44 | --initcodernil false
45 | --lifecycle
46 | --lineaftermarks true
47 | --linebreaks lf
48 | --markcategories true
49 | --markextensions always
50 | --marktypes always
51 | --maxwidth none
52 | --modifierorder
53 | --nevertrailing
54 | --nilinit remove
55 | --noncomplexattrs
56 | --nospaceoperators
57 | --nowrapoperators
58 | --octalgrouping 4,8
59 | --onelineforeach ignore
60 | --operatorfunc spaced
61 | --organizationmode visibility
62 | --organizetypes actor,class,enum,struct
63 | --patternlet hoist
64 | --ranges spaced
65 | --redundanttype infer-locals-only
66 | --self remove
67 | --selfrequired
68 | --semicolons inline
69 | --shortoptionals always
70 | --smarttabs enabled
71 | --someany true
72 | --storedvarattrs preserve
73 | --stripunusedargs always
74 | --structthreshold 0
75 | --tabwidth unspecified
76 | --throwcapturing
77 | --timezone system
78 | --trailingclosures
79 | --trimwhitespace always
80 | --typeattributes preserve
81 | --typeblanklines remove
82 | --typedelimiter space-after
83 | --typemark "MARK: - %t"
84 | --voidtype void
85 | --wraparguments preserve
86 | --wrapcollections preserve
87 | --wrapconditions preserve
88 | --wrapeffects preserve
89 | --wrapenumcases always
90 | --wrapparameters default
91 | --wrapreturntype preserve
92 | --wrapternary default
93 | --wraptypealiases preserve
94 | --xcodeindentation enabled
95 | --yodaswap always
96 | --disable blankLineAfterImports,blankLinesBetweenChainedFunctions
97 | --enable isEmpty
98 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/xcshareddata/xcschemes/CoreDataEvolution.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
9 |
10 |
16 |
22 |
23 |
24 |
25 |
26 |
32 |
33 |
43 |
44 |
50 |
51 |
57 |
58 |
59 |
60 |
62 |
63 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/xcshareddata/xcschemes/CoreDataEvolutionMacros.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
9 |
10 |
16 |
22 |
23 |
24 |
25 |
26 |
32 |
33 |
43 |
44 |
50 |
51 |
57 |
58 |
59 |
60 |
62 |
63 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/Sources/CoreDataEvolutionMacros/NSMainModelActorMacro.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ------------------------------------------------
3 | // Original project: CoreDataEvolution
4 | // Created on 2024/9/20 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 | /// The ModelActor in SwiftData corresponding to the Core Data version.
17 | /// Only run in MainActor and using viewContext
18 | /// An interface for providing mutually-exclusive access to the attributes of a conforming model.
19 | ///
20 | /// @NSMainModelActor
21 | /// @MainActor
22 | /// final class DataHandler {}
23 | ///
24 | /// will expand to
25 | ///
26 | /// @NSModelActor
27 | /// @MainActor
28 | /// final class DataHandler{}
29 | /// public let modelContainer: CoreData.NSPersistentContainer
30 | ///
31 | /// public init(modelContainer: CoreData.NSPersistentContainer) {
32 | /// self.modelContainer = modelContainer
33 | /// }
34 | /// extension DataHandler: CoreDataEvolution.NSModelActor {
35 | /// }
36 | public enum NSMainModelActorMacro {}
37 |
38 | extension NSMainModelActorMacro: ExtensionMacro {
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] {
46 | let decl: DeclSyntax =
47 | """
48 | extension \(type.trimmed): CoreDataEvolution.NSMainModelActor {}
49 | """
50 |
51 | guard let extensionDecl = decl.as(ExtensionDeclSyntax.self) else {
52 | return []
53 | }
54 |
55 | return [extensionDecl]
56 | }
57 | }
58 |
59 | extension NSMainModelActorMacro: MemberMacro {
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
72 | """
73 |
74 | let initializer: DeclSyntax? = generateInitializer ?
75 | """
76 | \(raw: accessModifier)init(modelContainer: CoreData.NSPersistentContainer) {
77 | self.modelContainer = modelContainer
78 | }
79 | """ : nil
80 | return [decl] + (initializer.map { [$0] } ?? [])
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/Sources/CoreDataEvolutionMacros/NSModelActorMacro.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NSModelActorMacro.swift
3 | //
4 | //
5 | // Created by Yang Xu on 2024/4/9.
6 | //
7 |
8 | import Foundation
9 | import SwiftSyntax
10 | import SwiftSyntaxMacros
11 |
12 | /// The ModelActor in SwiftData corresponding to the Core Data version.
13 | /// An interface for providing mutually-exclusive access to the attributes of a conforming model.
14 | ///
15 | /// @NSModelActor
16 | /// actor DataHandler {}
17 | ///
18 | /// will expand to
19 | ///
20 | /// @NSModelActor
21 | /// actor DataHandler{}
22 | /// public nonisolated let modelExecutor: CoreDataEvolution.NSModelObjectContextExecutor
23 | /// public nonisolated let modelContainer: CoreData.NSPersistentContainer
24 | ///
25 | /// public init(container: CoreData.NSPersistentContainer, mode: ActorContextMode = .newBackground) {
26 | /// let context: NSManagedObjectContext
27 | /// context = container.newBackgroundContext()
28 | /// modelExecutor = CoreDataEvolution.NSModelObjectContextExecutor(context: context)
29 | /// modelContainer = container
30 | /// }
31 | /// extension DataHandler: CoreDataEvolution.NSModelActor {
32 | /// }
33 | public enum NSModelActorMacro {}
34 |
35 | extension NSModelActorMacro: ExtensionMacro {
36 | public static func expansion(
37 | of _: SwiftSyntax.AttributeSyntax,
38 | attachedTo _: some SwiftSyntax.DeclGroupSyntax,
39 | providingExtensionsOf type: some SwiftSyntax.TypeSyntaxProtocol,
40 | conformingTo _: [SwiftSyntax.TypeSyntax],
41 | in _: some SwiftSyntaxMacros.MacroExpansionContext
42 | ) throws -> [SwiftSyntax.ExtensionDeclSyntax] {
43 | let decl: DeclSyntax =
44 | """
45 | extension \(type.trimmed): CoreDataEvolution.NSModelActor {}
46 | """
47 |
48 | guard let extensionDecl = decl.as(ExtensionDeclSyntax.self) else {
49 | return []
50 | }
51 |
52 | return [extensionDecl]
53 | }
54 | }
55 |
56 | extension NSModelActorMacro: MemberMacro {
57 | public static func expansion(
58 | of node: AttributeSyntax,
59 | providingMembersOf declaration: some DeclGroupSyntax,
60 | conformingTo _: [TypeSyntax],
61 | in _: some MacroExpansionContext
62 | ) throws -> [DeclSyntax] {
63 | let generateInitializer = shouldGenerateInitializer(from: node)
64 | let accessModifier = isPublic(from: declaration) ? "public " : ""
65 | let decl: DeclSyntax =
66 | """
67 | \(raw: accessModifier)nonisolated let modelExecutor: CoreDataEvolution.NSModelObjectContextExecutor
68 | \(raw: accessModifier)nonisolated let modelContainer: CoreData.NSPersistentContainer
69 |
70 | """
71 | let initializer: DeclSyntax? = generateInitializer ?
72 | """
73 | \(raw: accessModifier)init(container: CoreData.NSPersistentContainer) {
74 | let context: NSManagedObjectContext
75 | context = container.newBackgroundContext()
76 | modelExecutor = CoreDataEvolution.NSModelObjectContextExecutor(context: context)
77 | modelContainer = container
78 | }
79 | """ : nil
80 | return [decl] + (initializer.map { [$0] } ?? [])
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/Tests/CoreDataEvolutionTests/NSModelActorTests.swift:
--------------------------------------------------------------------------------
1 | import CoreDataEvolution
2 | import Testing
3 |
4 | /// Test suite for NSModelActor functionality
5 | /// This struct contains tests that verify the behavior of actors decorated with @NSModelActor and @NSMainModelActor macros
6 | struct NSModelActorTests {
7 | /// Test case for creating and managing Core Data items using a background actor
8 | /// This test verifies that:
9 | /// 1. Items can be created successfully in a background context
10 | /// 2. The item count is tracked correctly
11 | /// 3. Items can be deleted properly
12 | /// 4. Thread safety is maintained through the actor model
13 | @Test func createNewItem() async throws {
14 | // Initialize the test Core Data stack
15 | // TestStack provides a pre-configured NSPersistentContainer for testing
16 | let stack = TestStack()
17 |
18 | // Create a DataHandler actor instance with custom executor
19 | // The DataHandler is decorated with @NSModelActor(disableGenerateInit: true)
20 | // This creates an actor that operates on a background thread with its own managed object context
21 | let handler = DataHandler(container: stack.container, viewName: "hello")
22 |
23 | // Create a new item asynchronously using the actor
24 | // The showThread parameter enables thread information logging for debugging
25 | // Returns the NSManagedObjectID of the created item
26 | let id = try await handler.createNemItem(showThread: true)
27 |
28 | // Verify that exactly one item was created
29 | // This tests the actor's ability to perform read operations safely
30 | let count = try await handler.getItemCount()
31 | #expect(count == 1)
32 |
33 | // Delete the created item using its object ID
34 | // This tests the actor's ability to perform delete operations safely
35 | try await handler.delItem(id)
36 |
37 | // Verify that the item was successfully deleted
38 | // The count should return to zero after deletion
39 | let newCount = try await handler.getItemCount()
40 | #expect(newCount == 0)
41 | }
42 |
43 | /// Test case for creating Core Data items using a main thread actor
44 | /// This test verifies that:
45 | /// 1. Main thread actors work correctly with @NSMainModelActor
46 | /// 2. Items can be created synchronously on the main thread
47 | /// 3. Thread information can be logged for verification
48 | @MainActor
49 | @Test func createNewItemInMainActor() throws {
50 | // Initialize the test Core Data stack
51 | let stack = TestStack()
52 |
53 | // Create a MainHandler instance decorated with @NSMainModelActor
54 | // Unlike DataHandler, this operates on the main thread and doesn't require async/await
55 | // The MainHandler provides the same Core Data operations but runs synchronously on the main thread
56 | let handler = MainHandler(modelContainer: stack.container)
57 |
58 | // Create a new item synchronously on the main thread
59 | // The showThread parameter will log main thread information
60 | // Since this is a synchronous operation, we discard the returned object ID
61 | _ = try handler.createNemItem(showThread: true)
62 |
63 | // Verify that the item was created successfully
64 | // This operation also runs synchronously on the main thread
65 | let count = try handler.getItemCount()
66 | #expect(count == 1)
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/Sources/CoreDataEvolution/CoreDataEvolution.docc/CoreDataEvolution.md:
--------------------------------------------------------------------------------
1 | # ``CoreDataEvolution``
2 |
3 | Revolutionizing Core Data with SwiftData-inspired Concurrent Operations
4 |
5 | ## Overview
6 |
7 | CoreDataEvolution is a library aimed at modernizing Core Data by incorporating the elegance and safety of SwiftData-style concurrency. This library is designed to simplify and enhance Core Data's handling of multithreading, drawing inspiration from SwiftData's `@ModelActor` feature, enabling efficient, safe, and scalable operations.
8 |
9 | ## Motivation
10 |
11 | SwiftData introduced modern concurrency features like `@ModelActor`, making it easier to handle concurrent data access with safety guaranteed by the compiler. However, SwiftData's platform requirements and limited maturity in certain areas have deterred many developers from adopting it. CoreDataEvolution bridges the gap, bringing SwiftData's advanced design into the Core Data world for developers who are still reliant on Core Data.
12 |
13 | ## Key Features
14 |
15 | ### Custom Executors for Core Data Actors
16 |
17 | Using Swift 5.9's new `SerialExecutor` and `ExecutorJob` protocols, CoreDataEvolution provides custom executors that ensure all operations on managed objects are performed on the appropriate thread associated with their managed object context.
18 |
19 | ### @NSModelActor Macro
20 |
21 | 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.
22 |
23 | ### NSMainModelActor Macro
24 |
25 | `NSMainModelActor` provides the same functionality as `NSModelActor`, but is used to declare a class that runs on the main thread.
26 |
27 | ### Elegant Actor-based Concurrency
28 |
29 | 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.
30 |
31 | ## Basic Usage
32 |
33 | Here's how you can use CoreDataEvolution to manage concurrent Core Data operations with an actor:
34 |
35 | ```swift
36 | import CoreDataEvolution
37 |
38 | @NSModelActor
39 | actor DataHandler {
40 | func updateItem(identifier: NSManagedObjectID, timestamp: Date) throws {
41 | guard let item = self[identifier, as: Item.self] else {
42 | throw MyError.objectNotExist
43 | }
44 | item.timestamp = timestamp
45 | try modelContext.save()
46 | }
47 | }
48 | ```
49 |
50 | In this example, the `@NSModelActor` macro simplifies the setup, automatically creating the required executor and Core Data stack inside the actor. Developers can then focus on their business logic without worrying about concurrency pitfalls.
51 |
52 | ## Advanced Usage
53 |
54 | ### Custom Initialization
55 |
56 | You can disable the automatic generation of the constructor by using `disableGenerateInit`:
57 |
58 | ```swift
59 | @NSModelActor(disableGenerateInit: true)
60 | public actor DataHandler {
61 | let viewName: String
62 |
63 | func createNewItem(_ timestamp: Date = .now, showThread: Bool = false) throws -> NSManagedObjectID {
64 | let item = Item(context: modelContext)
65 | item.timestamp = timestamp
66 | try modelContext.save()
67 | return item.objectID
68 | }
69 |
70 | init(container: NSPersistentContainer, viewName: String) {
71 | modelContainer = container
72 | self.viewName = viewName
73 | let context = container.newBackgroundContext()
74 | context.name = viewName
75 | modelExecutor = .init(context: context)
76 | }
77 | }
78 | ```
79 |
80 | ### Main Thread Operations
81 |
82 | NSMainModelActor provides the same functionality as NSModelActor, but for operations that need to run on the main thread:
83 |
84 | ```swift
85 | @MainActor
86 | @NSMainModelActor
87 | final class DataHandler {
88 | func updateItem(identifier: NSManagedObjectID, timestamp: Date) throws {
89 | guard let item = self[identifier, as: Item.self] else {
90 | throw MyError.objectNotExist
91 | }
92 | item.timestamp = timestamp
93 | try modelContext.save()
94 | }
95 | }
96 | ```
97 |
98 | ## Installation
99 |
100 | Add CoreDataEvolution to your project using Swift Package Manager:
101 |
102 | ```swift
103 | dependencies: [
104 | .package(url: "https://github.com/fatbobman/CoreDataEvolution.git", .upToNextMajor(from: "0.3.0"))
105 | ]
106 | ```
107 |
108 | Then import the module:
109 |
110 | ```swift
111 | import CoreDataEvolution
112 | ```
113 |
114 | ## System Requirements
115 |
116 | - iOS 17.0+ / macOS 14.0+ / watchOS 10.0+ / visionOS 1.0+ / tvOS 17.0+
117 | - Swift 6.0
118 |
119 | > Important: Due to system limitations, custom executors and `SerialExecutor` are only available on iOS 17/macOS 14 and later.
120 |
121 | ## Topics
122 |
123 | ### Actors
124 |
125 | - ``NSModelActor``
126 | - ``NSMainModelActor``
127 |
128 | ### Executors
129 |
130 | - Custom executors for Core Data contexts
131 | - Thread-safe operations
132 |
133 | ### Migration
134 |
135 | - Migrating from traditional Core Data patterns
136 | - SwiftData compatibility considerations
137 |
138 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/xcshareddata/xcschemes/CoreDataEvolution-Package.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
9 |
10 |
16 |
22 |
23 |
24 |
30 |
36 |
37 |
38 |
44 |
50 |
51 |
52 |
53 |
54 |
59 |
60 |
63 |
64 |
65 |
66 |
68 |
74 |
75 |
76 |
77 |
78 |
88 |
90 |
96 |
97 |
98 |
99 |
103 |
104 |
105 |
106 |
112 |
114 |
120 |
121 |
122 |
123 |
125 |
126 |
129 |
130 |
131 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # CoreDataEvolution
2 |
3 |       [](LICENSE) [](https://deepwiki.com/fatbobman/CoreDataEvolution)
4 |
5 | ## Revolutionizing Core Data with SwiftData-inspired Concurrent Operations
6 |
7 | Welcome to CoreDataEvolution, a library aimed at modernizing Core Data by incorporating the elegance and safety of SwiftData-style concurrency. This library is designed to simplify and enhance Core Data’s handling of multithreading, drawing inspiration from SwiftData's `@ModelActor` feature, enabling efficient, safe, and scalable operations.
8 |
9 | ---
10 |
11 | Don't miss out on the latest updates and excellent articles about Swift, SwiftUI, Core Data, and SwiftData. Subscribe to **[Fatbobman's Swift Weekly](https://weekly.fatbobman.com)** and receive weekly insights and valuable content directly to your inbox.
12 |
13 | ---
14 |
15 | ## Motivation
16 |
17 | SwiftData introduced modern concurrency features like `@ModelActor`, making it easier to handle concurrent data access with safety guaranteed by the compiler. However, SwiftData's platform requirements and limited maturity in certain areas have deterred many developers from adopting it. CoreDataEvolution bridges the gap, bringing SwiftData’s advanced design into the Core Data world for developers who are still reliant on Core Data.
18 |
19 | * [Core Data Reform: Achieving Elegant Concurrency Operations like SwiftData](https://fatbobman.com/en/posts/core-data-reform-achieving-elegant-concurrency-operations-like-swiftdata/)
20 | * [Practical SwiftData: Building SwiftUI Applications with Modern Approaches](https://fatbobman.com/en/posts/practical-swiftdata-building-swiftui-applications-with-modern-approaches/)
21 | * [Concurrent Programming in SwiftData](https://fatbobman.com/en/posts/concurret-programming-in-swiftdata/)
22 |
23 | ## Key Features
24 |
25 | - **Custom Executors for Core Data Actors**
26 | Using Swift 5.9's new `SerialExecutor` and `ExecutorJob` protocols, CoreDataEvolution provides custom executors that ensure all operations on managed objects are performed on the appropriate thread associated with their managed object context.
27 |
28 | - **@NSModelActor Macro**
29 | 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.
33 |
34 | - **Elegant Actor-based Concurrency**
35 | 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.
36 |
37 | ## Example Usage
38 |
39 | Here’s how you can use CoreDataEvolution to manage concurrent Core Data operations with an actor:
40 |
41 | ```swift
42 | import CoreDataEvolution
43 |
44 | @NSModelActor
45 | actor DataHandler {
46 | func updateItem(identifier: NSManagedObjectID, timestamp: Date) throws {
47 | guard let item = self[identifier, as: Item.self] else {
48 | throw MyError.objectNotExist
49 | }
50 | item.timestamp = timestamp
51 | try modelContext.save()
52 | }
53 | }
54 | ```
55 |
56 | In this example, the `@NSModelActor` macro simplifies the setup, automatically creating the required executor and Core Data stack inside the actor. Developers can then focus on their business logic without worrying about concurrency pitfalls.
57 |
58 | This approach allows you to safely integrate modern Swift concurrency mechanisms into your existing Core Data stack, enhancing performance and code clarity.
59 |
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 |
100 | ## Installation
101 |
102 | You can add CoreDataEvolution to your project using Swift Package Manager by adding the following dependency to your `Package.swift` file:
103 |
104 | ```swift
105 | dependencies: [
106 | .package(url: "https://github.com/fatbobman/CoreDataEvolution.git", .upToNextMajor(from: "0.3.0"))
107 | ]
108 | ```
109 |
110 | Then, import the module into your Swift files:
111 |
112 | ```swift
113 | import CoreDataEvolution
114 | ```
115 |
116 | ## System Requirements
117 |
118 | - iOS 17.0+ / macOS 14.0+
119 | - Swift 6.0
120 |
121 | Note: Due to system limitations, custom executors and `SerialExecutor` are only available on iOS 17/macOS 14 and later.
122 |
123 | ## Contributing
124 |
125 | We welcome contributions! Whether you want to report issues, propose new features, or contribute to the code, feel free to open issues or pull requests on the GitHub repository.
126 |
127 | ## License
128 |
129 | CoreDataEvolution is available under the MIT license. See the LICENSE file for more information.
130 |
131 | ## Acknowledgments
132 |
133 | Special thanks to the Swift community for their continuous support and contributions.
134 |
135 | ## Support the project
136 |
137 | - [🎉 Subscribe to my Swift Weekly](https://weekly.fatbobman.com)
138 | - [☕️ Buy Me A Coffee](https://buymeacoffee.com/fatbobman)
139 |
140 | ## Star History
141 |
142 | [](https://star-history.com/#fatbobman/CoreDataEvolution&Date)
143 |
--------------------------------------------------------------------------------