├── .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 | ![Swift 6](https://img.shields.io/badge/Swift-6-orange?logo=swift) ![iOS](https://img.shields.io/badge/iOS-17.0+-green) ![macOS](https://img.shields.io/badge/macOS-14.0+-green) ![watchOS](https://img.shields.io/badge/watchOS-10.0+-green) ![visionOS](https://img.shields.io/badge/visionOS-1.0+-green) ![tvOS](https://img.shields.io/badge/tvOS-17.0+-green) [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE) [![Ask DeepWiki](https://deepwiki.com/badge.svg)](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 | [![Star History Chart](https://api.star-history.com/svg?repos=fatbobman/CoreDataEvolution&type=Date)](https://star-history.com/#fatbobman/CoreDataEvolution&Date) 143 | --------------------------------------------------------------------------------