├── .github └── FUNDING.yml ├── .gitignore ├── LICENSE ├── Package.resolved ├── Package.swift ├── README.md ├── Sources ├── ModelActorX │ ├── Macros.swift │ ├── MainModelActor.swift │ ├── ModelActorX.docc │ │ └── ModelActorX.md │ └── module.swift └── ModelActorXMacros │ ├── Helper.swift │ ├── MainModelActorXMacro.swift │ ├── ModelActorXMacro.swift │ └── Plugins.swift └── Tests └── ModelActorXTests ├── Helper.swift ├── MainModelActorXTeets.swift ├── ModelActorXMainTest.swift └── ModelActorXTests.swift /.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 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | xcuserdata/ 5 | DerivedData/ 6 | .swiftpm/configuration/registries.json 7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 8 | .netrc 9 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "originHash" : "d80c49e0b88e085b0611e7d0e5f52b058c0039594143437c159b2c326de952eb", 3 | "pins" : [ 4 | { 5 | "identity" : "swift-syntax", 6 | "kind" : "remoteSourceControl", 7 | "location" : "https://github.com/apple/swift-syntax.git", 8 | "state" : { 9 | "revision" : "0687f71944021d616d34d922343dcef086855920", 10 | "version" : "600.0.1" 11 | } 12 | } 13 | ], 14 | "version" : 3 15 | } 16 | -------------------------------------------------------------------------------- /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 PackageDescription 6 | 7 | let package = Package( 8 | name: "ModelActorX", 9 | platforms: [ 10 | .iOS(.v17), 11 | .macOS(.v14), 12 | .tvOS(.v17), 13 | .watchOS(.v10), 14 | ], 15 | products: [ 16 | // Products define the executables and libraries a package produces, making them visible to other packages. 17 | .library( 18 | name: "ModelActorX", 19 | targets: ["ModelActorX"] 20 | ), 21 | ], 22 | dependencies: [ 23 | .package(url: "https://github.com/apple/swift-syntax.git", from: "600.0.0"), 24 | ], 25 | targets: [ 26 | // Targets are the basic building blocks of a package, defining a module or a test suite. 27 | // Targets can depend on other targets in this package and products from dependencies. 28 | .macro( 29 | name: "ModelActorXMacros", 30 | dependencies: [ 31 | .product(name: "SwiftSyntaxMacros", package: "swift-syntax"), 32 | .product(name: "SwiftCompilerPlugin", package: "swift-syntax"), 33 | ] 34 | ), 35 | .target( 36 | name: "ModelActorX", 37 | dependencies: [ 38 | "ModelActorXMacros", 39 | ] 40 | ), 41 | .testTarget( 42 | name: "ModelActorXTests", 43 | dependencies: ["ModelActorX"] 44 | ), 45 | ], 46 | swiftLanguageModes: [.v6] 47 | ) 48 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ModelActorX 2 | 3 | ![Swift 6](https://img.shields.io/badge/Swift-6-orange?logo=swift) [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE) 4 | 5 | ModelActorX is a Swift library that provides custom macros `ModelActorX` and `MainModelActorX` to enhance and extend the functionality of SwiftData's `ModelActor`. These macros offer additional flexibility by allowing developers to control the generation of initializers and to declare additional variables within actors and classes. 6 | 7 | --- 8 | 9 | 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. 10 | 11 | --- 12 | 13 | ## Features 14 | 15 | - **ModelActorX Macro**: Similar to SwiftData's `ModelActor`, with an added `disableGenerateInit` parameter to control initializer generation. 16 | - **MainModelActorX Macro**: Generates a class running on the `MainActor`, ideal for main-thread operations, and uses `ModelContainer`'s `mainContext`. 17 | - **Custom Initializers**: Ability to declare additional variables and pass them through custom initializers when `disableGenerateInit` is set to `true`. 18 | - **Seamless Integration**: Designed to work seamlessly with SwiftData and existing Swift projects. 19 | 20 | ## Installation 21 | 22 | ### Swift Package Manager 23 | 24 | Add ModelActorX to your project using Swift Package Manager. In your `Package.swift` file, add: 25 | 26 | ```swift 27 | dependencies: [ 28 | .package(url: "https://github.com/fatbobman/ModelActorX.git", from: "0.1.0") 29 | ] 30 | ``` 31 | 32 | Alternatively, in Xcode: 33 | 34 | 1. Go to **File** > **Add Packages...** 35 | 2. Enter the repository URL: `https://github.com/fatbobman/ModelActorX.git` 36 | 3. Follow the prompts to add the package to your project. 37 | 38 | ## Usage 39 | 40 | ### ModelActorX 41 | 42 | The `ModelActorX` macro is used to define an actor with functionality similar to SwiftData's `ModelActor`. The key difference is the `disableGenerateInit` parameter, which, when set to `true`, prevents the automatic generation of an initializer. This allows you to declare additional variables and provide a custom initializer. 43 | 44 | #### Basic Usage 45 | 46 | ```swift 47 | @ModelActorX 48 | actor DataHandler { 49 | func newItem(date: Date) throws -> PersistentIdentifier { 50 | let item = Item(timestamp: date) 51 | modelContext.insert(item) 52 | try modelContext.save() 53 | return item.persistentModelID 54 | } 55 | 56 | func getTimestampFromItemID(_ itemID: PersistentIdentifier) -> Date? { 57 | return self[itemID, as: Item.self]?.timestamp 58 | } 59 | } 60 | ``` 61 | 62 | #### Custom Initializer 63 | 64 | ```swift 65 | @ModelActorX(disableGenerateInit: true) 66 | actor DataHandler1 { 67 | let date: Date 68 | 69 | func newItem() throws -> PersistentIdentifier { 70 | let item = Item(timestamp: date) 71 | modelContext.insert(item) 72 | try modelContext.save() 73 | return item.persistentModelID 74 | } 75 | 76 | func getTimestampFromItemID(_ itemID: PersistentIdentifier) -> Date? { 77 | return self[itemID, as: Item.self]?.timestamp 78 | } 79 | 80 | init(container: ModelContainer, date: Date) { 81 | self.date = date 82 | modelContainer = container 83 | let modelContext = ModelContext(modelContainer) 84 | modelExecutor = DefaultSerialModelExecutor(modelContext: modelContext) 85 | } 86 | } 87 | ``` 88 | 89 | ### MainModelActorX 90 | 91 | The `MainModelActorX` macro is used to generate a class that runs on the `MainActor`. This is particularly useful for UI updates or any operations that need to be performed on the main thread. The generated class uses the `mainContext` from `ModelContainer`. 92 | 93 | #### Basic Usage 94 | 95 | ```swift 96 | @MainActor 97 | @MainModelActorX 98 | final class MainDataHandler { 99 | func newItem(date: Date) throws -> PersistentIdentifier { 100 | let item = Item(timestamp: date) 101 | modelContext.insert(item) 102 | try modelContext.save() 103 | return item.persistentModelID 104 | } 105 | 106 | func getTimestampFromItemID(_ itemID: PersistentIdentifier) -> Date? { 107 | return self[itemID, as: Item.self]?.timestamp 108 | } 109 | } 110 | ``` 111 | 112 | #### Custom Initializer 113 | 114 | ```swift 115 | @MainActor 116 | @MainModelActorX(disableGenerateInit: true) 117 | final class MainDataHandler1 { 118 | let date: Date 119 | 120 | func newItem() throws -> PersistentIdentifier { 121 | let item = Item(timestamp: date) 122 | modelContext.insert(item) 123 | try modelContext.save() 124 | return item.persistentModelID 125 | } 126 | 127 | func getTimestampFromItemID(_ itemID: PersistentIdentifier) -> Date? { 128 | return self[itemID, as: Item.self]?.timestamp 129 | } 130 | 131 | init(container: ModelContainer, date: Date) { 132 | self.date = date 133 | modelContainer = container 134 | } 135 | } 136 | ``` 137 | 138 | ### Using ModelActorX with MainActor 139 | 140 | `@ModelActorX` also provides a constructor declared with `@MainActor`. When you use this constructor to generate an actor, it will directly utilize the `mainContext` (view context), and the entire actor will run on the main thread. The key difference from `@MainModelActorX` is that the type remains an `actor`. This means that existing code built upon ModelActor does not require modification—the calls will still retain `await`. 141 | 142 | This approach might be an ideal temporary solution before iOS 18 addresses the responsiveness issues related to updates using `@ModelActor`. 143 | 144 | ```swift 145 | @ModelActorX 146 | actor DataHandler {} 147 | 148 | Task{ @MainActor in 149 | let handler = DataHandler(mainContext: container.mainContext) // Use the view context for construction 150 | await handler.updateItem(id: id) // Even on the main thread, you can still use `await` 151 | } 152 | ``` 153 | 154 | ## Testing Examples 155 | 156 | ### ModelActorXTests 157 | 158 | ```swift 159 | struct ModelActorXTests { 160 | @Test func example1() async throws { 161 | let container = createContainer() 162 | let handler = DataHandler(modelContainer: container) 163 | let now = Date.now 164 | let id = try await handler.newItem(date: now) 165 | let date = await handler.getTimestampFromItemID(id) 166 | #expect(date == now) 167 | } 168 | 169 | @Test func example2() async throws { 170 | let container = createContainer() 171 | let now = Date.now 172 | let handler = DataHandler1(container: container, date: now) 173 | let id = try await handler.newItem() 174 | let date = await handler.getTimestampFromItemID(id) 175 | #expect(date == now) 176 | } 177 | } 178 | ``` 179 | 180 | ### MainModelActorXTests 181 | 182 | ```swift 183 | @MainActor 184 | struct MainModelActorXTests { 185 | @Test 186 | func test1() async throws { 187 | let container = createContainer() 188 | let handler = MainDataHandler(modelContainer: container) 189 | let now = Date.now 190 | let id = try handler.newItem(date: now) 191 | let date = handler.getTimestampFromItemID(id) 192 | #expect(date == now) 193 | } 194 | 195 | @Test 196 | func test2() async throws { 197 | let container = createContainer() 198 | let now = Date.now 199 | let handler = MainDataHandler1(container: container, date: now) 200 | let id = try handler.newItem() 201 | let date = handler.getTimestampFromItemID(id) 202 | #expect(date == now) 203 | } 204 | } 205 | ``` 206 | 207 | ## Parameters 208 | 209 | ### ModelActorX Macro 210 | 211 | - `disableGenerateInit: Bool` (optional): Controls whether an initializer is automatically generated. Default is `false`. 212 | 213 | ### MainModelActorX Macro 214 | 215 | - `disableGenerateInit: Bool` (optional): Controls whether an initializer is automatically generated. Default is `false`. 216 | 217 | ## Requirements 218 | 219 | - Swift 6 220 | - iOS 17.0 / macOS 14 / tvOS 17 / watchOS 10 or later 221 | 222 | ## License 223 | 224 | ModelActorX is released under the MIT License. See [LICENSE](LICENSE) for details. 225 | 226 | ## Acknowledgements 227 | 228 | - Inspired by SwiftData's `ModelActor` functionality. 229 | - Core Data Version: [CoreDataEvolution](https://github.com/fatbobman/CoreDataEvolution) 230 | - Thanks to the Swift community for continuous support and contributions. 231 | 232 | ## Contributing 233 | 234 | Contributions are welcome! If you have ideas for improvements or find bugs, please open an issue or submit a pull request. 235 | 236 | [![Buy Me A Coffee](https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png)](https://buymeacoffee.com/fatbobman) 237 | -------------------------------------------------------------------------------- /Sources/ModelActorX/Macros.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ------------------------------------------------ 3 | // Original project: ModelActorX 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 | @attached(member, names: named(modelExecutor), named(modelContainer), named(init)) 16 | @attached(extension, conformances: ModelActor) 17 | public macro ModelActorX(disableGenerateInit: Bool = false) = #externalMacro(module: "ModelActorXMacros", type: "ModelActorXMacro") 18 | 19 | @attached(member, names: named(modelExecutor), named(modelContainer), named(init)) 20 | @attached(extension, conformances: MainModelActorX) 21 | public macro MainModelActorX(disableGenerateInit: Bool = false) = #externalMacro(module: "ModelActorXMacros", type: "MainModelActorXMacro") 22 | -------------------------------------------------------------------------------- /Sources/ModelActorX/MainModelActor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ------------------------------------------------ 3 | // Original project: ModelActorX 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 | /// A protocol for managing SwiftData model container on the main thread 16 | /// Provides access to ModelContainer and ModelContext 17 | @MainActor 18 | public protocol MainModelActorX: AnyObject { 19 | /// The model container for SwiftData 20 | /// Used to manage data persistence and context creation 21 | var modelContainer: ModelContainer { get } 22 | } 23 | 24 | extension MainModelActorX { 25 | /// The main thread model context 26 | /// Used to perform data operations like querying, inserting, updating, and deleting on the main thread 27 | /// - Important: This context should only be used on the main thread 28 | public var modelContext: ModelContext { 29 | modelContainer.mainContext 30 | } 31 | 32 | /// Returns a model object of the specified type for the given persistent identifier 33 | /// - Parameters: 34 | /// - id: The persistent identifier of the model object to find 35 | /// - type: The type of model to return 36 | /// - Returns: An instance of the specified type if found, nil otherwise 37 | /// - Note: First attempts to find the object in registered models, then performs a database query if not found 38 | public subscript(id: PersistentIdentifier, as type: T.Type) -> T? where T: PersistentModel { 39 | let predicate = #Predicate { 40 | $0.persistentModelID == id 41 | } 42 | if let object: T = modelContext.registeredModel(for: id) { 43 | return object 44 | } 45 | let fetchDescriptor = FetchDescriptor(predicate: predicate) 46 | let object: T? = try? modelContext.fetch(fetchDescriptor).first 47 | return object 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Sources/ModelActorX/ModelActorX.docc/ModelActorX.md: -------------------------------------------------------------------------------- 1 | # ``ModelActorX`` 2 | 3 | ModelActorX is a Swift library that provides custom macros `ModelActorX` and `MainModelActorX` to enhance and extend the functionality of SwiftData's `ModelActor`. These macros offer additional flexibility by allowing developers to control the generation of initializers and to declare additional variables within actors and classes. 4 | 5 | ## Features 6 | 7 | - **ModelActorX Macro**: Similar to SwiftData's `ModelActor`, with an added `disableGenerateInit` parameter to control initializer generation. 8 | - **MainModelActorX Macro**: Generates a class running on the `MainActor`, ideal for main-thread operations, and uses `ModelContainer`'s `mainContext`. 9 | - **Custom Initializers**: Ability to declare additional variables and pass them through custom initializers when `disableGenerateInit` is set to `true`. 10 | - **Seamless Integration**: Designed to work seamlessly with SwiftData and existing Swift projects. 11 | 12 | ## Installation 13 | 14 | ### Swift Package Manager 15 | 16 | Add ModelActorX to your project using Swift Package Manager. In your `Package.swift` file, add: 17 | 18 | ```swift 19 | dependencies: [ 20 | .package(url: "https://github.com/fatbobman/ModelActorX.git", from: "0.1.0") 21 | ] 22 | ``` 23 | 24 | Alternatively, in Xcode: 25 | 26 | 1. Go to **File** > **Add Packages...** 27 | 2. Enter the repository URL: `https://github.com/fatbobman/ModelActorX.git` 28 | 3. Follow the prompts to add the package to your project. 29 | 30 | ## Usage 31 | 32 | ### ModelActorX 33 | 34 | The `ModelActorX` macro is used to define an actor with functionality similar to SwiftData's `ModelActor`. The key difference is the `disableGenerateInit` parameter, which, when set to `true`, prevents the automatic generation of an initializer. This allows you to declare additional variables and provide a custom initializer. 35 | 36 | #### Basic Usage 37 | 38 | ```swift 39 | @ModelActorX 40 | actor DataHandler { 41 | func newItem(date: Date) throws -> PersistentIdentifier { 42 | let item = Item(timestamp: date) 43 | modelContext.insert(item) 44 | try modelContext.save() 45 | return item.persistentModelID 46 | } 47 | 48 | func getTimestampFromItemID(_ itemID: PersistentIdentifier) -> Date? { 49 | return self[itemID, as: Item.self]?.timestamp 50 | } 51 | } 52 | ``` 53 | 54 | #### Custom Initializer 55 | 56 | ```swift 57 | @ModelActorX(disableGenerateInit: true) 58 | actor DataHandler1 { 59 | let date: Date 60 | 61 | func newItem() throws -> PersistentIdentifier { 62 | let item = Item(timestamp: date) 63 | modelContext.insert(item) 64 | try modelContext.save() 65 | return item.persistentModelID 66 | } 67 | 68 | func getTimestampFromItemID(_ itemID: PersistentIdentifier) -> Date? { 69 | return self[itemID, as: Item.self]?.timestamp 70 | } 71 | 72 | init(container: ModelContainer, date: Date) { 73 | self.date = date 74 | modelContainer = container 75 | let modelContext = ModelContext(modelContainer) 76 | modelExecutor = DefaultSerialModelExecutor(modelContext: modelContext) 77 | } 78 | } 79 | ``` 80 | 81 | ### MainModelActorX 82 | 83 | The `MainModelActorX` macro is used to generate a class that runs on the `MainActor`. This is particularly useful for UI updates or any operations that need to be performed on the main thread. The generated class uses the `mainContext` from `ModelContainer`. 84 | 85 | #### Basic Usage 86 | 87 | ```swift 88 | @MainActor 89 | @MainModelActorX 90 | final class MainDataHandler { 91 | func newItem(date: Date) throws -> PersistentIdentifier { 92 | let item = Item(timestamp: date) 93 | modelContext.insert(item) 94 | try modelContext.save() 95 | return item.persistentModelID 96 | } 97 | 98 | func getTimestampFromItemID(_ itemID: PersistentIdentifier) -> Date? { 99 | return self[itemID, as: Item.self]?.timestamp 100 | } 101 | } 102 | ``` 103 | 104 | #### Custom Initializer 105 | 106 | ```swift 107 | @MainActor 108 | @MainModelActorX(disableGenerateInit: true) 109 | final class MainDataHandler1 { 110 | let date: Date 111 | 112 | func newItem() throws -> PersistentIdentifier { 113 | let item = Item(timestamp: date) 114 | modelContext.insert(item) 115 | try modelContext.save() 116 | return item.persistentModelID 117 | } 118 | 119 | func getTimestampFromItemID(_ itemID: PersistentIdentifier) -> Date? { 120 | return self[itemID, as: Item.self]?.timestamp 121 | } 122 | 123 | init(container: ModelContainer, date: Date) { 124 | self.date = date 125 | modelContainer = container 126 | } 127 | } 128 | ``` 129 | 130 | ### Using ModelActorX with MainActor 131 | 132 | `@ModelActorX` also provides a constructor declared with `@MainActor`. When you use this constructor to generate an actor, it will directly utilize the `mainContext` (view context), and the entire actor will run on the main thread. The key difference from `@MainModelActorX` is that the type remains an `actor`. This means that existing code built upon ModelActor does not require modification—the calls will still retain `await`. 133 | 134 | This approach might be an ideal temporary solution before iOS 18 addresses the responsiveness issues related to updates using `@ModelActor`. 135 | 136 | ```swift 137 | @ModelActorX 138 | actor DataHandler {} 139 | 140 | Task{ @MainActor in 141 | let handler = DataHandler(mainContext: container.mainContext) // Use the view context for construction 142 | await handler.updateItem(id: id) // Even on the main thread, you can still use `await` 143 | } 144 | ``` 145 | 146 | ## Testing Examples 147 | 148 | ### ModelActorXTests 149 | 150 | ```swift 151 | struct ModelActorXTests { 152 | @Test func example1() async throws { 153 | let container = createContainer() 154 | let handler = DataHandler(modelContainer: container) 155 | let now = Date.now 156 | let id = try await handler.newItem(date: now) 157 | let date = await handler.getTimestampFromItemID(id) 158 | #expect(date == now) 159 | } 160 | 161 | @Test func example2() async throws { 162 | let container = createContainer() 163 | let now = Date.now 164 | let handler = DataHandler1(container: container, date: now) 165 | let id = try await handler.newItem() 166 | let date = await handler.getTimestampFromItemID(id) 167 | #expect(date == now) 168 | } 169 | } 170 | ``` 171 | 172 | ### MainModelActorXTests 173 | 174 | ```swift 175 | @MainActor 176 | struct MainModelActorXTests { 177 | @Test 178 | func test1() async throws { 179 | let container = createContainer() 180 | let handler = MainDataHandler(modelContainer: container) 181 | let now = Date.now 182 | let id = try handler.newItem(date: now) 183 | let date = handler.getTimestampFromItemID(id) 184 | #expect(date == now) 185 | } 186 | 187 | @Test 188 | func test2() async throws { 189 | let container = createContainer() 190 | let now = Date.now 191 | let handler = MainDataHandler1(container: container, date: now) 192 | let id = try handler.newItem() 193 | let date = handler.getTimestampFromItemID(id) 194 | #expect(date == now) 195 | } 196 | } 197 | ``` 198 | 199 | ## Parameters 200 | 201 | ### ModelActorX Macro 202 | 203 | - `disableGenerateInit: Bool` (optional): Controls whether an initializer is automatically generated. Default is `false`. 204 | 205 | ### MainModelActorX Macro 206 | 207 | - `disableGenerateInit: Bool` (optional): Controls whether an initializer is automatically generated. Default is `false`. 208 | 209 | ## Requirements 210 | 211 | - Swift 6 212 | - iOS 17.0 / macOS 14 / tvOS 17 / watchOS 10 or later 213 | -------------------------------------------------------------------------------- /Sources/ModelActorX/module.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ------------------------------------------------ 3 | // Original project: ModelActorX 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 | @_exported import SwiftData 13 | -------------------------------------------------------------------------------- /Sources/ModelActorXMacros/Helper.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ------------------------------------------------ 3 | // Original project: ModelActorX 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 | /// Get the status of generating the initializer 17 | func shouldGenerateInitializer(from node: AttributeSyntax) -> Bool { 18 | guard let argumentList = node.arguments?.as(LabeledExprListSyntax.self) else { 19 | return true 20 | } 21 | 22 | for argument in argumentList { 23 | if argument.label?.text == "disableGenerateInit", 24 | let booleanLiteral = argument.expression.as(BooleanLiteralExprSyntax.self) 25 | { 26 | return booleanLiteral.literal.text != "true" 27 | } 28 | } 29 | return true 30 | } 31 | 32 | /// Get the access level of the declared type, whether it is public 33 | func isPublic(from declaration: some DeclGroupSyntax) -> Bool { 34 | return declaration.modifiers.contains { modifier in 35 | modifier.name.text == "public" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Sources/ModelActorXMacros/MainModelActorXMacro.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ------------------------------------------------ 3 | // Original project: ModelActorX 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 | /// The SwiftData version corresponding to NSMainModelActorX of CoreDataEvolution 17 | /// Helps declare a class that runs in MainActor and provides access to ModelContainer 18 | /// 19 | /// @MainModelActorX 20 | /// @MainActor 21 | /// public final class DataHandler {} 22 | /// 23 | /// will expand to 24 | /// 25 | /// @MainModelActorX 26 | /// @MainActor 27 | /// public final class DataHandler{} 28 | /// public let modelContainer: SwiftData.ModelContainer 29 | /// 30 | /// public init(modelContainer: SwiftData.ModelContainer) { 31 | /// self.modelContainer = modelContainer 32 | /// } 33 | /// extension DataHandler: ModelActorX.MainModelActorX { 34 | /// } 35 | public enum MainModelActorXMacro {} 36 | 37 | extension MainModelActorXMacro: ExtensionMacro { 38 | public static func expansion( 39 | of _: SwiftSyntax.AttributeSyntax, 40 | attachedTo _: some SwiftSyntax.DeclGroupSyntax, 41 | providingExtensionsOf type: some SwiftSyntax.TypeSyntaxProtocol, 42 | conformingTo _: [SwiftSyntax.TypeSyntax], 43 | in _: some SwiftSyntaxMacros.MacroExpansionContext 44 | ) throws -> [SwiftSyntax.ExtensionDeclSyntax] { 45 | let decl: DeclSyntax = 46 | """ 47 | extension \(type.trimmed): ModelActorX.MainModelActorX {} 48 | """ 49 | 50 | guard let extensionDecl = decl.as(ExtensionDeclSyntax.self) else { 51 | return [] 52 | } 53 | 54 | return [extensionDecl] 55 | } 56 | } 57 | 58 | extension MainModelActorXMacro: MemberMacro { 59 | public static func expansion( 60 | of node: AttributeSyntax, 61 | providingMembersOf declaration: some DeclGroupSyntax, 62 | conformingTo _: [TypeSyntax], 63 | in _: some MacroExpansionContext 64 | ) throws -> [DeclSyntax] { 65 | let generateInitializer = shouldGenerateInitializer(from: node) 66 | let accessModifier = isPublic(from: declaration) ? "public " : "" 67 | 68 | let decl: DeclSyntax = 69 | """ 70 | \(raw: accessModifier)let modelContainer: SwiftData.ModelContainer 71 | """ 72 | 73 | let initializer: DeclSyntax? = generateInitializer ? 74 | """ 75 | \(raw: accessModifier)init(modelContainer: SwiftData.ModelContainer) { 76 | self.modelContainer = modelContainer 77 | } 78 | """ : nil 79 | return [decl] + (initializer.map { [$0] } ?? []) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /Sources/ModelActorXMacros/ModelActorXMacro.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ------------------------------------------------ 3 | // Original project: ModelActorX 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 | /// An alternative to SwiftData's @ModelActor, with the only difference being the provision of the `disableGenerateInit` parameter, allowing for the non-generation of the constructor method 17 | /// 18 | /// @ModelActorX 19 | /// actor DataHandler {} 20 | /// 21 | /// will expand to 22 | /// 23 | /// @NSModelActor 24 | /// actor DataHandler{} 25 | /// nonisolated let modelExecutor: any SwiftData.ModelExecutor 26 | /// nonisolated let modelContainer: SwiftData.ModelContainer 27 | /// 28 | /// init(container: CoreData.NSPersistentContainer) { 29 | /// let modelContext = ModelContext(modelContainer) 30 | /// self.modelExecutor = DefaultSerialModelExecutor(modelContext: modelContext) 31 | /// self.modelContainer = modelContainer 32 | /// } 33 | /// 34 | /// @MainActor 35 | /// init(mainContext: SwiftData.ModelContext) { 36 | /// self.modelExecutor = DefaultSerialModelExecutor(modelContext: mainContext) 37 | /// self.modelContainer = mainContext.container 38 | /// } 39 | /// 40 | /// extension DataHandler: SwiftData.ModelActor { 41 | /// } 42 | public enum ModelActorXMacro {} 43 | 44 | extension ModelActorXMacro: ExtensionMacro { 45 | public static func expansion( 46 | of _: SwiftSyntax.AttributeSyntax, 47 | attachedTo _: some SwiftSyntax.DeclGroupSyntax, 48 | providingExtensionsOf type: some SwiftSyntax.TypeSyntaxProtocol, 49 | conformingTo _: [SwiftSyntax.TypeSyntax], 50 | in _: some SwiftSyntaxMacros.MacroExpansionContext 51 | ) throws -> [SwiftSyntax.ExtensionDeclSyntax] { 52 | let decl: DeclSyntax = 53 | """ 54 | extension \(type.trimmed): SwiftData.ModelActor {} 55 | """ 56 | 57 | guard let extensionDecl = decl.as(ExtensionDeclSyntax.self) else { 58 | return [] 59 | } 60 | 61 | return [extensionDecl] 62 | } 63 | } 64 | 65 | extension ModelActorXMacro: MemberMacro { 66 | public static func expansion( 67 | of node: AttributeSyntax, 68 | providingMembersOf declaration: some DeclGroupSyntax, 69 | conformingTo _: [TypeSyntax], 70 | in _: some MacroExpansionContext 71 | ) throws -> [DeclSyntax] { 72 | let generateInitializer = shouldGenerateInitializer(from: node) 73 | let accessModifier = isPublic(from: declaration) ? "public " : "" 74 | 75 | let decl: DeclSyntax = 76 | """ 77 | \(raw: accessModifier)nonisolated let modelExecutor: any SwiftData.ModelExecutor 78 | \(raw: accessModifier)nonisolated let modelContainer: SwiftData.ModelContainer 79 | 80 | """ 81 | let initializer: DeclSyntax? = generateInitializer ? 82 | """ 83 | \(raw: accessModifier)init(modelContainer: SwiftData.ModelContainer) { 84 | let modelContext = ModelContext(modelContainer) 85 | self.modelExecutor = DefaultSerialModelExecutor(modelContext: modelContext) 86 | self.modelContainer = modelContainer 87 | } 88 | """ : nil 89 | 90 | let mainActorInitializer: DeclSyntax? = generateInitializer ? 91 | """ 92 | @MainActor 93 | \(raw: accessModifier)init(mainContext: SwiftData.ModelContext) { 94 | self.modelExecutor = DefaultSerialModelExecutor(modelContext: mainContext) 95 | self.modelContainer = mainContext.container 96 | } 97 | """ : nil 98 | return [decl] + (initializer.map { [$0] } ?? []) + (mainActorInitializer.map { [$0] } ?? []) 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /Sources/ModelActorXMacros/Plugins.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ------------------------------------------------ 3 | // Original project: ModelActorX 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 SwiftCompilerPlugin 13 | import SwiftSyntaxMacros 14 | 15 | @main 16 | struct ModelActorXMacrosPlugin: CompilerPlugin { 17 | let providingMacros: [Macro.Type] = [ 18 | ModelActorXMacro.self, 19 | MainModelActorXMacro.self, 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /Tests/ModelActorXTests/Helper.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ------------------------------------------------ 3 | // Original project: ModelActorX 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 ModelActorX 14 | import SwiftData 15 | 16 | @Model 17 | final class Item { 18 | var timestamp: Date 19 | init(timestamp: Date) { 20 | self.timestamp = timestamp 21 | } 22 | } 23 | 24 | func createContainer(_ fileName: String = #file + #function, subDirectory: String = "TestTemp") -> ModelContainer { 25 | let tempURL = URL.temporaryDirectory 26 | if !FileManager.default.fileExists(atPath: tempURL.appendingPathComponent(subDirectory).path) { 27 | try? FileManager.default 28 | .createDirectory(at: tempURL.appendingPathComponent(subDirectory), withIntermediateDirectories: true) 29 | } 30 | let url = tempURL.appendingPathComponent(subDirectory).appendingPathComponent( 31 | fileName + ".sqlite" 32 | ) 33 | if FileManager.default.fileExists(atPath: url.path) { 34 | try? FileManager.default.removeItem(at: url) 35 | } 36 | 37 | let scheme = Schema([Item.self]) 38 | let configuration = ModelConfiguration(schema: scheme, url: url) 39 | let container = try! ModelContainer(for: Item.self, configurations: configuration) 40 | return container 41 | } 42 | -------------------------------------------------------------------------------- /Tests/ModelActorXTests/MainModelActorXTeets.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ------------------------------------------------ 3 | // Original project: ModelActorX 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 | @testable import ModelActorX 14 | import Testing 15 | 16 | @MainActor 17 | struct MainModelActorXTests { 18 | @Test 19 | func test1() async throws { 20 | let container = createContainer() 21 | let handler = MainDataHandler(modelContainer: container) 22 | let now = Date.now 23 | let id = try handler.newItem(date: now) 24 | let date = handler.getTimestampFromItemID(id) 25 | #expect(date == now) 26 | } 27 | 28 | @Test 29 | func test2() async throws { 30 | let container = createContainer() 31 | let now = Date.now 32 | let handler = MainDataHandler1(container: container, date: now) 33 | let id = try handler.newItem() 34 | let date = handler.getTimestampFromItemID(id) 35 | #expect(date == now) 36 | } 37 | } 38 | 39 | @MainActor 40 | @MainModelActorX 41 | final class MainDataHandler { 42 | func newItem(date: Date) throws -> PersistentIdentifier { 43 | let item = Item(timestamp: date) 44 | modelContext.insert(item) 45 | try modelContext.save() 46 | return item.persistentModelID 47 | } 48 | 49 | func getTimestampFromItemID(_ itemID: PersistentIdentifier) -> Date? { 50 | return self[itemID, as: Item.self]?.timestamp 51 | } 52 | } 53 | 54 | @MainActor 55 | @MainModelActorX(disableGenerateInit: true) 56 | final class MainDataHandler1 { 57 | let date: Date 58 | 59 | func newItem() throws -> PersistentIdentifier { 60 | let item = Item(timestamp: date) 61 | modelContext.insert(item) 62 | try modelContext.save() 63 | return item.persistentModelID 64 | } 65 | 66 | func getTimestampFromItemID(_ itemID: PersistentIdentifier) -> Date? { 67 | return self[itemID, as: Item.self]?.timestamp 68 | } 69 | 70 | init(container: ModelContainer, date: Date) { 71 | self.date = date 72 | modelContainer = container 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /Tests/ModelActorXTests/ModelActorXMainTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ------------------------------------------------ 3 | // Original project: ModelActorX 4 | // Created on 2024/11/1 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 | @testable import ModelActorX 14 | import Testing 15 | 16 | @MainActor 17 | struct ModelActorXMainTests { 18 | @Test func example1() async throws { 19 | let container = createContainer() 20 | 21 | let handler = DataHandlerMain(mainContext: container.mainContext) 22 | 23 | let now = Date.now 24 | let id = try await handler.newItem(date: now) 25 | let date = await handler.getTimestampFromItemID(id) 26 | #expect(date == now) 27 | } 28 | } 29 | 30 | @ModelActorX 31 | actor DataHandlerMain { 32 | func newItem(date: Date) throws -> PersistentIdentifier { 33 | let item = Item(timestamp: date) 34 | modelContext.insert(item) 35 | try modelContext.save() 36 | return item.persistentModelID 37 | } 38 | 39 | func getTimestampFromItemID(_ itemID: PersistentIdentifier) -> Date? { 40 | return self[itemID, as: Item.self]?.timestamp 41 | } 42 | 43 | func updateItem(id: PersistentIdentifier, date: Date) { 44 | guard let item = self[id, as: Item.self] else { return } 45 | item.timestamp = date 46 | try? modelContext.save() 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Tests/ModelActorXTests/ModelActorXTests.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | @testable import ModelActorX 3 | import Testing 4 | 5 | struct ModelActorXTests { 6 | @Test func example1() async throws { 7 | let container = createContainer() 8 | let handler = DataHandler(modelContainer: container) 9 | let now = Date.now 10 | let id = try await handler.newItem(date: now) 11 | let date = await handler.getTimestampFromItemID(id) 12 | #expect(date == now) 13 | } 14 | 15 | @Test func example2() async throws { 16 | let container = createContainer() 17 | let now = Date.now 18 | let handler = DataHandler1(container: container, date: now) 19 | 20 | let id = try await handler.newItem() 21 | let date = await handler.getTimestampFromItemID(id) 22 | #expect(date == now) 23 | } 24 | } 25 | 26 | @ModelActorX 27 | actor DataHandler { 28 | func newItem(date: Date) throws -> PersistentIdentifier { 29 | let item = Item(timestamp: date) 30 | modelContext.insert(item) 31 | try modelContext.save() 32 | return item.persistentModelID 33 | } 34 | 35 | func getTimestampFromItemID(_ itemID: PersistentIdentifier) -> Date? { 36 | return self[itemID, as: Item.self]?.timestamp 37 | } 38 | } 39 | 40 | @ModelActorX(disableGenerateInit: true) 41 | actor DataHandler1 { 42 | let date: Date 43 | 44 | func newItem() throws -> PersistentIdentifier { 45 | let item = Item(timestamp: date) 46 | modelContext.insert(item) 47 | try modelContext.save() 48 | return item.persistentModelID 49 | } 50 | 51 | func getTimestampFromItemID(_ itemID: PersistentIdentifier) -> Date? { 52 | return self[itemID, as: Item.self]?.timestamp 53 | } 54 | 55 | init(container: ModelContainer, date: Date) { 56 | self.date = date 57 | modelContainer = container 58 | let modelContext = ModelContext(modelContainer) 59 | modelExecutor = DefaultSerialModelExecutor(modelContext: modelContext) 60 | } 61 | } 62 | --------------------------------------------------------------------------------