├── .github └── workflows │ └── swift.yml ├── .gitignore ├── .spi.yml ├── CONTRIBUTING.md ├── LICENSE ├── Package.swift ├── README.md ├── Sources ├── ManagedModelMacros │ ├── CompilerPlugin.swift │ ├── MacroDiagnostic.swift │ ├── ModelMacro │ │ ├── GenerateInitializers.swift │ │ ├── MetadataSlot.swift │ │ ├── ModelExtensions.swift │ │ ├── ModelMacro.swift │ │ ├── ModelMemberAttributes.swift │ │ └── ModelMembers.swift │ ├── Models │ │ ├── ModelInitializers.swift │ │ └── ModelProperty.swift │ ├── PropertyMacros │ │ ├── AttributeMacro.swift │ │ ├── PersistedPropertyMacro.swift │ │ ├── RelationshipMacro.swift │ │ └── TransientMacro.swift │ └── Utilities │ │ ├── AttributeTypes.swift │ │ ├── ClassDeclSyntax+Extras.swift │ │ ├── ContextHelpers.swift │ │ ├── DeclHelpers.swift │ │ ├── DeclModifierListSyntax+Extras.swift │ │ ├── ExpressionType.swift │ │ └── TypeSyntax+Extras.swift └── ManagedModels │ ├── Container │ ├── ModelConfiguration.swift │ ├── ModelContainer.swift │ ├── NSPersistentContainer+Data.swift │ └── NSPersistentStoreDescription+Data.swift │ ├── Context │ ├── ModelContext.swift │ └── NSManagedObjectContext+Data.swift │ ├── Documentation.docc │ ├── DifferencesToSwiftData.md │ ├── Documentation.md │ ├── FAQ.md │ ├── GettingStarted.md │ ├── Links.md │ └── Who.md │ ├── Migration │ ├── MigrationStage.swift │ └── SchemaMigrationPlan.swift │ ├── ModelMacroDefinition.swift │ ├── PersistentModel │ ├── PersistentIdentifier.swift │ ├── PersistentModel+KVC.swift │ ├── PersistentModel.swift │ ├── PropertyMetadata.swift │ └── RelationshipCollection.swift │ ├── ReExports.swift │ ├── Schema │ ├── Attribute.swift │ ├── AttributeOption.swift │ ├── Entity.swift │ ├── Relationship.swift │ ├── RelationshipOption.swift │ ├── RelationshipTargetType.swift │ ├── Schema.swift │ ├── SchemaProperty.swift │ ├── SchemaVersion.swift │ └── VersionedSchema.swift │ ├── SchemaCompatibility │ ├── CodableTransformer.swift │ ├── CoreDataPrimitiveValue.swift │ ├── NSAttributeDescription+Data.swift │ ├── NSAttributeType+Data.swift │ ├── NSDeleteRule+Data.swift │ ├── NSEntityDescription+Data.swift │ ├── NSManagedObjectModel+Data.swift │ ├── NSPropertyDescription+Data.swift │ └── NSRelationshipDescription+Data.swift │ ├── SchemaGeneration │ ├── NSEntityDescription+Generation.swift │ ├── NSRelationshipDescription+Inverse.swift │ └── SchemaBuilder.swift │ ├── SwiftUI │ ├── FetchRequest+Extras.swift │ ├── ModelContainer+SwiftUI.swift │ └── ModelContext+SwiftUI.swift │ └── Utilities │ ├── AnyOptional.swift │ ├── NSSortDescriptors+Extras.swift │ └── OrderedSet.swift └── Tests ├── ManagedModelMacrosTests └── ManagedModelMacrosTests.swift └── ManagedModelTests ├── BasicModelTests.swift ├── CodablePropertiesTests.swift ├── CodableRawRepresentableTests.swift ├── CoreDataAssumptionsTests.swift ├── FetchRequestTests.swift ├── Fixtures.swift ├── ObjCMarkedPropertiesTests.swift ├── RelationshipSetupTests.swift ├── SchemaGenerationTests.swift ├── Schemas ├── AdvancedCodablePropertiesSchema.swift ├── CodablePropertySchema.swift ├── ExpandedPersonAddressSchema.swift ├── PersonAddressOptionalToOne.swift ├── PersonAddressSchema.swift ├── PersonAddressSchemaNoInverse.swift ├── ToDoListSchema.swift └── TransformablePropertySchema.swift ├── SwiftUITestCase.swift └── TransformablePropertiesTests.swift /.github/workflows/swift.yml: -------------------------------------------------------------------------------- 1 | name: Build and Test 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | jobs: 8 | xcode15: 9 | runs-on: macos-13 10 | steps: 11 | - name: Select latest available Xcode 12 | uses: maxim-lobanov/setup-xcode@v1.5.1 13 | with: 14 | xcode-version: '15.2.0' 15 | - name: Checkout Repository 16 | uses: actions/checkout@v4 17 | - name: Build Swift Debug Package 18 | run: swift build -c debug 19 | - name: Build Swift Release Package 20 | run: swift build -c release 21 | - name: Run Tests 22 | run: swift test 23 | xcode16: 24 | runs-on: macos-latest 25 | steps: 26 | - name: Select latest available Xcode 27 | uses: maxim-lobanov/setup-xcode@v1.5.1 28 | with: 29 | xcode-version: latest 30 | - name: Checkout Repository 31 | uses: actions/checkout@v4 32 | - name: Build Swift Debug Package 33 | run: swift build -c debug 34 | - name: Build Swift Release Package 35 | run: swift build -c release 36 | - name: Run Tests 37 | run: swift test 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | DerivedData/ 7 | .swiftpm/config/registries.json 8 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 9 | .netrc 10 | .swiftpm 11 | TestGeneration.txt 12 | .docker.build 13 | .DS_Store 14 | Package.resolved 15 | 16 | -------------------------------------------------------------------------------- /.spi.yml: -------------------------------------------------------------------------------- 1 | version: 1 2 | builder: 3 | configs: 4 | - documentation_targets: [ ManagedModels ] 5 | 6 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Legal 2 | 3 | By submitting a pull request, you represent that you have the right to license 4 | your contribution to ZeeZide GmbH and the community, and agree by submitting the 5 | patch that your contributions are licensed under the Apache 2.0 license. 6 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.9 2 | import PackageDescription 3 | import CompilerPluginSupport 4 | 5 | #if swift(>=5.10) 6 | let settings = [ SwiftSetting.enableExperimentalFeature("StrictConcurrency") ] 7 | #else 8 | let settings = [ SwiftSetting ]() 9 | #endif 10 | 11 | let package = Package( 12 | name: "ManagedModels", 13 | 14 | platforms: [ .macOS(.v11), .iOS(.v13), .tvOS(.v13), .watchOS(.v6) ], 15 | products: [ 16 | .library(name: "ManagedModels", targets: [ "ManagedModels" ]) 17 | ], 18 | dependencies: [ 19 | .package(url: "https://github.com/swiftlang/swift-syntax.git", 20 | "509.0.0"..<"600.0.1") 21 | ], 22 | targets: [ 23 | .target( 24 | name: "ManagedModels", 25 | dependencies: [ "ManagedModelMacros" ], 26 | swiftSettings: settings 27 | ), 28 | 29 | .macro( 30 | name: "ManagedModelMacros", 31 | dependencies: [ 32 | .product(name: "SwiftSyntaxMacros", package: "swift-syntax"), 33 | .product(name: "SwiftCompilerPlugin", package: "swift-syntax") 34 | ] 35 | ), 36 | 37 | .testTarget( 38 | name: "ManagedModelTests", 39 | dependencies: [ "ManagedModels" ] 40 | ), 41 | 42 | .testTarget( 43 | name: "ManagedModelMacrosTests", 44 | dependencies: [ 45 | "ManagedModelMacros", 46 | .product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax"), 47 | ] 48 | ), 49 | ] 50 | ) 51 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

ManagedModels for CoreData 2 | 4 |

5 | 6 | > Instead of wrapping CoreData, use it directly :-) 7 | 8 | The key thing **ManagedModels** provides is a `@Model` macro, 9 | that works similar (but not identical) to the SwiftData 10 | [`@Model`](https://developer.apple.com/documentation/swiftdata/model()) 11 | macro. 12 | It generates an 13 | [`NSManagedObjectModel`](https://developer.apple.com/documentation/coredata/nsmanagedobjectmodel) 14 | straight from the code. I.e. no CoreData modeler / data model file is necessary. 15 | 16 | A small sample model: 17 | ```swift 18 | @Model class Item: NSManagedObject { 19 | var timestamp : Date 20 | var title : String? 21 | } 22 | ``` 23 | 24 |
25 | The full CoreData template application converted to ManagedModels
26 | 27 | ```swift 28 | import SwiftUI 29 | import ManagedModels 30 | 31 | @Model class Item: NSManagedObject { 32 | var timestamp : Date 33 | } 34 | 35 | struct ContentView: View { 36 | 37 | @Environment(\.modelContext) private var viewContext 38 | 39 | @FetchRequest(sort: \.timestamp, animation: .default) 40 | private var items: FetchedResults 41 | 42 | var body: some View { 43 | NavigationView { 44 | List { 45 | ForEach(items) { item in 46 | NavigationLink { 47 | Text("Item at \(item.timestamp!, format: .dateTime)") 48 | } label: { 49 | Text("\(item.timestamp!, format: .dateTime)") 50 | } 51 | } 52 | .onDelete(perform: deleteItems) 53 | } 54 | .toolbar { 55 | ToolbarItem(placement: .navigationBarTrailing) { 56 | EditButton() 57 | } 58 | ToolbarItem { 59 | Button(action: addItem) { 60 | Label("Add Item", systemImage: "plus") 61 | } 62 | } 63 | } 64 | Text("Select an item") 65 | } 66 | } 67 | 68 | private func addItem() { 69 | withAnimation { 70 | let newItem = Item(context: viewContext) 71 | newItem.timestamp = Date() 72 | try! viewContext.save() 73 | } 74 | } 75 | 76 | private func deleteItems(offsets: IndexSet) { 77 | withAnimation { 78 | offsets.map { items[$0] }.forEach(viewContext.delete) 79 | try! viewContext.save() 80 | } 81 | } 82 | } 83 | 84 | #Preview { 85 | ContentView() 86 | .modelContainer(for: Item.self, inMemory: true) 87 | } 88 | ``` 89 | 90 |
91 | 92 | 93 | > This is *not* intended as a replacement implementation of 94 | > [SwiftData](https://developer.apple.com/documentation/swiftdata). 95 | > I.e. the API is kept _similar_ to SwiftData, but not exactly the same. 96 | > It doesn't try to hide CoreData, but rather provides utilities to work *with* 97 | > CoreData in a similar way to SwiftData. 98 | 99 | A full To-Do list application example: 100 | [ManagedToDos.app](https://github.com/Data-swift/ManagedToDosApp). 101 | 102 | Blog article describing the thing: [`@Model` for CoreData](https://www.alwaysrightinstitute.com/managedmodels/). 103 | 104 | #### Requirements 105 | 106 | The macro implementation requires Xcode 15/Swift 5.9 for compilation. 107 | The generated code itself though should backport way back to 108 | iOS 10 / macOS 10.12 though (when `NSPersistentContainer` was introduced). 109 | 110 | Package URL: 111 | ``` 112 | https://github.com/Data-swift/ManagedModels.git 113 | ``` 114 | 115 | ManagedModels has no other dependencies. 116 | 117 | 118 | #### Differences to SwiftData 119 | 120 | - The model class must explicitly inherit from 121 | [`NSManagedObject`](https://developer.apple.com/documentation/coredata/nsmanagedobject) 122 | (superclasses can't be added by macros), 123 | e.g. `@Model class Person: NSManagedObject`. 124 | - Uses the CoreData `@FetchRequest` property wrapper instead `@Query`. 125 | - Doesn't use the new 126 | [Observation](https://developer.apple.com/documentation/observation) 127 | framework (which requires iOS 17+), but uses 128 | [ObservableObject](https://developer.apple.com/documentation/combine/observableobject) 129 | (which is directly supported by CoreData). 130 | 131 | 132 | #### TODO 133 | 134 | - [ ] Figure out whether we can do ordered attributes: [Issue #1](https://github.com/Data-swift/ManagedModels/issues/1). 135 | - [ ] Support for "autosave": [Issue #3](https://github.com/Data-swift/ManagedModels/issues/3) 136 | - [ ] Support transformable types, not sure they work right yet: [Issue #4](https://github.com/Data-swift/ManagedModels/issues/4) 137 | - [ ] Generate property initializers if the user didn't specify any inits: [Issue #5](https://github.com/Data-swift/ManagedModels/issues/5) 138 | - [ ] Support SchemaMigrationPlan/MigrationStage: [Issue #6](https://github.com/Data-swift/ManagedModels/issues/6) 139 | - [ ] Write more tests. 140 | - [ ] Write DocC docs: [Issue #7](https://github.com/Data-swift/ManagedModels/issues/7), [Issue #8](https://github.com/Data-swift/ManagedModels/issues/8) 141 | - [ ] Support for entity inheritance: [Issue #9](https://github.com/Data-swift/ManagedModels/issues/9) 142 | - [ ] Add support for originalName/versionHash in `@Model`: [Issue 10](https://github.com/Data-swift/ManagedModels/issues/10) 143 | - [ ] Generate "To Many" accessor function prototypes (`addItemToGroup` etc): [Issue 11](https://github.com/Data-swift/ManagedModels/issues/11) 144 | - [x] Foundation Predicate support (would require iOS 17+) - this seems actually supported by CoreData! 145 | - [ ] SwiftUI `@Query` property wrapper/macro?: [Issue 12](https://github.com/Data-swift/ManagedModels/issues/12) 146 | - [ ] Figure out all the cloud sync options SwiftData has and whether CoreData 147 | can do them: [Issue 13](https://github.com/Data-swift/ManagedModels/issues/13) 148 | - [x] Archiving/Unarchiving, required for migration. 149 | - [x] Figure out whether we can add support for array toMany properties: [Issue #2](https://github.com/Data-swift/ManagedModels/issues/2) 150 | - [x] Generate `fetchRequest()` class function. 151 | - [x] Figure out whether we can allow initialized properties 152 | (`var title = "No Title"`): [Issue 14](https://github.com/Data-swift/ManagedModels/issues/14) 153 | 154 | Pull requests are very welcome! 155 | Even just DocC documentation or more tests would be welcome contributions. 156 | 157 | 158 | #### Links 159 | 160 | - [ManagedModels](https://github.com/Data-swift/ManagedModels/) 161 | - [ManagedToDos.app](https://github.com/Data-swift/ManagedToDosApp) 162 | - Blog article: [`@Model` for CoreData](https://www.alwaysrightinstitute.com/managedmodels/) 163 | - [Northwind for ManagedModels](https://github.com/Northwind-swift/NorthwindManagedModels) 164 | (more complex example, schema with many entities and a prefilled DB for 165 | testing) 166 | - Apple: 167 | - [CoreData](https://developer.apple.com/documentation/coredata) 168 | - [SwiftData](https://developer.apple.com/documentation/swiftdata) 169 | - [Meet SwiftData](https://developer.apple.com/videos/play/wwdc2023/10187) 170 | - [Build an App with SwiftData](https://developer.apple.com/videos/play/wwdc2023/10154) 171 | - [Model your Schema with SwiftData](https://developer.apple.com/videos/play/wwdc2023/10195) 172 | - [Enterprise Objects Framework](https://en.wikipedia.org/wiki/Enterprise_Objects_Framework) / aka EOF 173 | - [Developer Guide](https://developer.apple.com/library/archive/documentation/LegacyTechnologies/WebObjects/WebObjects_4.5/System/Documentation/Developer/EnterpriseObjects/DevGuide/EOFDevGuide.pdf) 174 | - [Lighter.swift](https://github.com/Lighter-swift), typesafe and superfast 175 | [SQLite](https://www.sqlite.org) Swift tooling. 176 | - [ZeeQL](http://zeeql.io), prototype of an 177 | [EOF](https://en.wikipedia.org/wiki/Enterprise_Objects_Framework) for Swift, 178 | with many database backends. 179 | 180 | 181 | #### Disclaimer 182 | 183 | SwiftData and SwiftUI are trademarks owned by Apple Inc. Software maintained as 184 | a part of the this project is not affiliated with Apple Inc. 185 | 186 | 187 | ### Who 188 | 189 | ManagedModels are brought to you by 190 | [Helge Heß](https://github.com/helje5/) / [ZeeZide](https://zeezide.de). 191 | We like feedback, GitHub stars, cool contract work, 192 | presumably any form of praise you can think of. 193 | -------------------------------------------------------------------------------- /Sources/ManagedModelMacros/CompilerPlugin.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2023 ZeeZide GmbH. 4 | // 5 | 6 | import SwiftCompilerPlugin 7 | import SwiftSyntaxMacros 8 | 9 | @main 10 | struct ModelsPlugin: CompilerPlugin { 11 | let providingMacros: [ Macro.Type ] = [ 12 | TransientMacro .self, 13 | AttributeMacro .self, 14 | RelationshipMacro .self, 15 | PersistedPropertyMacro.self, 16 | ModelMacro .self 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /Sources/ManagedModelMacros/MacroDiagnostic.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2023 ZeeZide GmbH. 4 | // 5 | 6 | import SwiftDiagnostics 7 | 8 | enum MacroDiagnostic: String, DiagnosticMessage, Swift.Error { 9 | 10 | case modelMacroCanOnlyBeAppliedOnNSManagedObjects 11 | 12 | case multipleAttributeMacrosAppliedOnProperty 13 | case multipleRelationshipMacrosAppliedOnProperty 14 | case attributeAndRelationshipMacrosAppliedOnProperty 15 | case propertyNameIsForbidden 16 | 17 | case propertyHasNeitherTypeNorInit 18 | 19 | case multipleMembersInAttributesMacroCall 20 | 21 | case unexpectedTypeForExtension 22 | 23 | var message: String { 24 | switch self { 25 | case .modelMacroCanOnlyBeAppliedOnNSManagedObjects: 26 | "The @Model macro can only be applied on classes that inherit from NSManagedObject." 27 | 28 | case .propertyNameIsForbidden: 29 | "This property name cannot be used in NSManagedObject types, " + 30 | "it is a system property." 31 | 32 | case .multipleAttributeMacrosAppliedOnProperty: 33 | "Multiple @Attribute macros applied on property." 34 | case .multipleRelationshipMacrosAppliedOnProperty: 35 | "Multiple @Relationship macros applied on property." 36 | case .attributeAndRelationshipMacrosAppliedOnProperty: 37 | "Both @Attribute and @Relationship macros applied on property." 38 | 39 | case .propertyHasNeitherTypeNorInit: 40 | "Property has neither type nor initializer?" 41 | 42 | case .multipleMembersInAttributesMacroCall: 43 | "Compiler issue, multiple members in attributes macro." 44 | case .unexpectedTypeForExtension: 45 | "Compiler issue, unexpected type passed into extension." 46 | } 47 | } 48 | 49 | var diagnosticID: SwiftDiagnostics.MessageID { 50 | .init(domain: "ModelsMacro", id: rawValue) 51 | } 52 | 53 | var severity: SwiftDiagnostics.DiagnosticSeverity { 54 | .error 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Sources/ManagedModelMacros/ModelMacro/GenerateInitializers.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2023 ZeeZide GmbH. 4 | // 5 | 6 | import SwiftCompilerPlugin 7 | import SwiftSyntax 8 | import SwiftSyntaxBuilder 9 | import SwiftSyntaxMacros 10 | import SwiftDiagnostics 11 | 12 | extension ModelMacro { 13 | 14 | static func generateInitializers( 15 | for classDecl : ClassDeclSyntax, 16 | access : String, 17 | modelClassName : TokenSyntax, 18 | properties : [ ModelProperty ], 19 | initializers : [ ModelInitializer ] 20 | ) -> [ DeclSyntax ] 21 | { 22 | var newInitializers = [ DeclSyntax ]() 23 | 24 | if !properties.isEmpty && initializers.isEmpty { 25 | if let decl = generatePropertyInitializer( 26 | for: classDecl, access: access, modelClassName: modelClassName, 27 | properties: properties 28 | ) { 29 | newInitializers.append(decl) 30 | } 31 | } 32 | 33 | // This is needed to make it available when the user writes an own 34 | // designated initializer alongside! 35 | // Wrong: This is always needed, otherwise we get an: 36 | // > Fatal error: Use of unimplemented initializer 37 | // > 'init(entity:insertInto:)' for class XYZ. 38 | newInitializers.append( 39 | """ 40 | /// Initialize a `\(modelClassName)` object, optionally providing an 41 | /// `NSManagedObjectContext` it should be inserted into. 42 | /// - Parameters: 43 | // - entity: An `NSEntityDescription` describing the object. 44 | // - context: An `NSManagedObjectContext` the object should be inserted into. 45 | \(raw: access)override init(entity: CoreData.NSEntityDescription, insertInto context: NSManagedObjectContext?) 46 | { 47 | super.init(entity: entity, insertInto: context) 48 | } 49 | """ 50 | ) 51 | 52 | // This has to be a convenience init because the user might add an own 53 | // required init, e.g.: 54 | // init(name: String) { 55 | // self.init() // he has to call this, but this breaks! 56 | // self.name = name 57 | // } 58 | // The problem is that designated initializers cannot delegate to other 59 | // designated initializers. 60 | newInitializers.append( 61 | """ 62 | /// Initialize a `\(modelClassName)` object, optionally providing an 63 | /// `NSManagedObjectContext` it should be inserted into. 64 | /// - Parameters: 65 | // - context: An `NSManagedObjectContext` the object should be inserted into. 66 | \(raw: access)init(context: CoreData.NSManagedObjectContext?) { 67 | super.init(entity: Self.entity(), insertInto: context) 68 | } 69 | """ 70 | ) 71 | 72 | if !initializers.hasNoArgumentInitializer { 73 | newInitializers.append( 74 | """ 75 | /// Initialize a `\(modelClassName)` object w/o inserting it into a 76 | /// context. 77 | \(raw: access)init() { 78 | super.init(entity: Self.entity(), insertInto: nil) 79 | } 80 | """ 81 | ) 82 | } 83 | 84 | return newInitializers 85 | } 86 | 87 | private static func generatePropertyInitializer( 88 | for classDecl : ClassDeclSyntax, 89 | access : String, 90 | modelClassName : TokenSyntax, 91 | properties : [ ModelProperty ] 92 | ) -> DeclSyntax? 93 | { 94 | // This is only called if the user has specified no initializers. Synthesize 95 | // one for (all!) the stored properties. 96 | 97 | #if false // TODO! 98 | for member : MemberBlockItemSyntax in classDecl.memberBlock.members { 99 | guard let variables = member.decl.as(VariableDeclSyntax.self), 100 | !variables.isStaticOrClass else 101 | { 102 | continue 103 | } 104 | 105 | // Each binding is a variable in a declaration list, 106 | // e.g. `let a = 5, b = 6`, the `a` and `b` would be bindings. 107 | for binding : PatternBindingSyntax in variables.bindings { 108 | guard let pattern = binding.pattern.as(IdentifierPatternSyntax.self), 109 | let type : TypeSyntax = binding.typeAnnotation?.type else 110 | { 111 | continue 112 | } 113 | let name = pattern.identifier.trimmed.text 114 | 115 | // Type is String or Set
etc. 116 | // Emit initializer if available? Might use non-public things? 117 | // TODO: How to build the AST for the signature? 118 | 119 | print("Name:", name) 120 | print(" Type:", type) 121 | print(" Init:", binding.initializer?.value) 122 | } 123 | } 124 | #endif 125 | return nil 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /Sources/ManagedModelMacros/ModelMacro/MetadataSlot.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2023 ZeeZide GmbH. 4 | // 5 | 6 | import SwiftSyntax 7 | import SwiftSyntaxBuilder 8 | 9 | extension ModelMacro { 10 | 11 | static func generateMetadataSlot( 12 | access : String, 13 | modelClassName : TokenSyntax, 14 | properties : [ ModelProperty ] 15 | ) -> DeclSyntax 16 | { 17 | func generateInfo(for property: ModelProperty) -> ExprSyntax? { 18 | guard let valueType = property.valueType else { return nil } 19 | guard valueType.isKnownAttributePropertyType else { return nil } 20 | let cleanValueType = valueType.replacingImplicitlyUnwrappedOptionalTypes().trimmed 21 | return "CoreData.NSAttributeDescription(name: \(literal: property.name), valueType: \(cleanValueType).self)" 22 | } 23 | 24 | func attributeInfo(for property: ModelProperty, 25 | attribute syntax: AttributeSyntax) 26 | -> ExprSyntax? 27 | { 28 | // Note: We still want empty prop objects, because they still tell the 29 | // type of a property! 30 | let valueType : TypeSyntax = property.valueType? 31 | .replacingImplicitlyUnwrappedOptionalTypes().trimmed ?? "Any" 32 | var fallback: ExprSyntax { 33 | "CoreData.NSAttributeDescription(name: \(literal: property.name), valueType: \(valueType).self)" 34 | } 35 | guard let arguments = syntax.arguments else { return fallback } 36 | guard case .argumentList(var labeledExpressions) = arguments else { 37 | return fallback 38 | } 39 | 40 | // Enrich w/ more data 41 | labeledExpressions.append(.init( 42 | label: "name", expression: ExprSyntax("\(literal: property.name)") 43 | )) 44 | labeledExpressions.append(.init( 45 | label: "valueType", expression: ExprSyntax("\(valueType).self") 46 | )) 47 | 48 | return ExprSyntax(FunctionCallExprSyntax(callee: ExprSyntax("CoreData.NSAttributeDescription")) { 49 | labeledExpressions 50 | }) 51 | } 52 | 53 | func relationshipInfo(for property: ModelProperty, 54 | attribute syntax: AttributeSyntax) -> ExprSyntax? 55 | { 56 | // Note: We still want empty prop objects, because they still tell the 57 | // type of a property! 58 | let valueType : TypeSyntax = property.valueType? 59 | .replacingImplicitlyUnwrappedOptionalTypes().trimmed ?? "Any" 60 | var fallback: ExprSyntax { 61 | "CoreData.NSRelationshipDescription(name: \(literal: property.name), valueType: \(valueType).self)" 62 | } 63 | guard let arguments = syntax.arguments else { return fallback } 64 | guard case .argumentList(var labeledExpressions) = arguments else { 65 | return fallback 66 | } 67 | 68 | // Enrich w/ more data 69 | labeledExpressions.append(.init( 70 | label: "name", expression: ExprSyntax("\(literal: property.name)") 71 | )) 72 | labeledExpressions.append(.init( 73 | label: "valueType", expression: ExprSyntax("\(valueType).self") 74 | )) 75 | 76 | return ExprSyntax(FunctionCallExprSyntax(callee: ExprSyntax("CoreData.NSRelationshipDescription")) { 77 | labeledExpressions 78 | }) 79 | } 80 | 81 | func metadata(for property: ModelProperty) -> ExprSyntax { 82 | let metaExpr : ExprSyntax? = switch property.type { 83 | case .plain: generateInfo(for: property) 84 | case .attribute (let attribute, isTransformable: _): 85 | attributeInfo (for: property, attribute: attribute) 86 | case .relationship(let attribute): 87 | relationshipInfo(for: property, attribute: attribute) 88 | } 89 | 90 | let initExpr = property.initExpression ?? "nil" 91 | return """ 92 | 93 | .init(name: \(literal: property.name), keypath: \\\(raw: modelClassName.trimmed).\(raw: property.name), 94 | defaultValue: \(initExpr), 95 | metadata: \(metaExpr ?? "nil")) 96 | """ 97 | } 98 | 99 | let fields = ArrayExprSyntax(expressions: properties 100 | .filter({ !$0.isTransient}) 101 | .map(metadata(for:)) 102 | ) 103 | return 104 | """ 105 | \(raw: access)static let schemaMetadata : [ CoreData.NSManagedObjectModel.PropertyMetadata ] = \(fields) 106 | """ 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /Sources/ManagedModelMacros/ModelMacro/ModelExtensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2023 ZeeZide GmbH. 4 | // 5 | 6 | import SwiftCompilerPlugin 7 | import SwiftSyntax 8 | import SwiftSyntaxBuilder 9 | import SwiftSyntaxMacros 10 | 11 | extension ModelMacro: ExtensionMacro { // @attached(extension, conformances:..) 12 | 13 | /* 14 | @attached(extension, conformances: // the protocols we add automagically 15 | PersistentModel 16 | ) 17 | */ 18 | public static func expansion( 19 | of macroNode : AttributeSyntax, 20 | attachedTo declaration : some DeclGroupSyntax, // the class 21 | providingExtensionsOf type : some TypeSyntaxProtocol, // the classtype 22 | conformingTo conformancesToAdd : [ TypeSyntax ], 23 | in context : some MacroExpansionContext 24 | ) throws -> [ ExtensionDeclSyntax ] 25 | { 26 | guard let classDecl = declaration.as(ClassDeclSyntax.self) else { 27 | context.diagnose(.modelMacroCanOnlyBeAppliedOnNSManagedObjects, 28 | on: macroNode) 29 | return [] // TBD: rather throw? 30 | } 31 | 32 | // The protocol types in like (as a workaround for protocols below) 33 | // `class Model: Codable, CustomStringConvertible` 34 | let inheritedTypeNames = classDecl.inheritedTypeNames 35 | 36 | guard inheritedTypeNames.contains("NSManagedObject") || 37 | inheritedTypeNames.contains("CoreData.NSManagedObject") else 38 | { 39 | context.diagnose(.modelMacroCanOnlyBeAppliedOnNSManagedObjects, 40 | on: macroNode) 41 | return [] // TBD: rather throw? 42 | } 43 | 44 | // Already contains the protocol conformance. 45 | guard !inheritedTypeNames.contains("PersistentModel") && 46 | !inheritedTypeNames.contains("ManagedModels.PersistentModel") else 47 | { 48 | return [] 49 | } 50 | 51 | // Those are supposed to be the protocols that need to be processed. 52 | // If they are missing, they are supposed to be applied already? 53 | // But this doesn't always work, i.e. sometimes they are empty but still 54 | // missing! (e.g. in tests?) 55 | // So this is for the situation in which the protocols _are_ passed in, 56 | // we still need add in the empty scenario. 57 | if !conformancesToAdd.isEmpty { // this isn't always filled? 58 | var stillNeeded = false 59 | for conformance in conformancesToAdd { 60 | guard let id = conformance.as(IdentifierTypeSyntax.self) else { 61 | assertionFailure("Unexpected conformance? \(conformance)") 62 | continue 63 | } 64 | let name = id.name.trimmed.text 65 | if name == "PersistentModel" || 66 | name == "ManagedModels.PersistentModel" 67 | { 68 | stillNeeded = true 69 | } 70 | else { 71 | assertionFailure("Unexpected conformance: \(name)") 72 | } 73 | } 74 | guard stillNeeded else { return [] } 75 | } 76 | return [ 77 | try ExtensionDeclSyntax( 78 | "extension \(type): ManagedModels.PersistentModel" 79 | ) {} 80 | ] 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /Sources/ManagedModelMacros/ModelMacro/ModelMacro.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2023 ZeeZide GmbH. 4 | // 5 | 6 | public enum ModelMacro { 7 | } 8 | -------------------------------------------------------------------------------- /Sources/ManagedModelMacros/ModelMacro/ModelMemberAttributes.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2023 ZeeZide GmbH. 4 | // 5 | 6 | import SwiftCompilerPlugin 7 | import SwiftSyntax 8 | import SwiftSyntaxBuilder 9 | import SwiftSyntaxMacros 10 | 11 | /** 12 | * Attaches attributes, `@_PersistedProperty`, to members. 13 | * 14 | * This is attached to tracked properties: 15 | * - `@_PersistedProperty` 16 | */ 17 | extension ModelMacro: MemberAttributeMacro { // @attached(memberAttribute) 18 | 19 | public static func expansion( 20 | of macroNode : AttributeSyntax, 21 | attachedTo declaration : some DeclGroupSyntax, 22 | providingAttributesFor member : some DeclSyntaxProtocol, 23 | in context : some MacroExpansionContext 24 | ) throws -> [ AttributeSyntax ] 25 | { 26 | guard declaration.is(ClassDeclSyntax.self) else { 27 | context.diagnose(.modelMacroCanOnlyBeAppliedOnNSManagedObjects, 28 | on: macroNode) 29 | return [] // TBD: rather throw? 30 | } 31 | 32 | // This is an array because the `member` declaration can contain multiple 33 | // bindings, e.g.: `var street, city, country : String`. 34 | // Those are NOT all the properties of the `declaration` (e.g. the class). 35 | var properties = [ ModelProperty ]() 36 | addModelProperties(in: member, to: &properties, 37 | context: context) 38 | 39 | guard let property = properties.first else { return [] } // other member 40 | 41 | if properties.count > 1 { 42 | context.diagnose(.multipleMembersInAttributesMacroCall, on: macroNode) 43 | return [] 44 | } 45 | 46 | guard !property.isTransient else { return [] } 47 | 48 | /* 49 | // property.declaredValueType is nil, but we detect some 50 | var firstname = "Jason" 51 | // property.declaredValueType is set 52 | var lastname : String 53 | */ 54 | let isRelationship: Bool 55 | if case .relationship(_) = property.type { 56 | isRelationship = true 57 | } else { 58 | isRelationship = false 59 | } 60 | let addAtObjC = isRelationship 61 | || (property.valueType?.canBeRepresentedInObjectiveC ?? false) 62 | 63 | // We'd like @objc, but we don't know which ones to attach it to? 64 | // https://github.com/Data-swift/ManagedModels/issues/36 65 | return addAtObjC 66 | ? [ "@_PersistedProperty", "@objc" ] 67 | : [ "@_PersistedProperty" ] 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Sources/ManagedModelMacros/ModelMacro/ModelMembers.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2023 ZeeZide GmbH. 4 | // 5 | 6 | import SwiftCompilerPlugin 7 | import SwiftSyntax 8 | import SwiftSyntaxBuilder 9 | import SwiftSyntaxMacros 10 | import SwiftDiagnostics 11 | 12 | /** 13 | * ```swift 14 | * @attached(member, names: // Those are the names we add 15 | * named(init), // Initializers. 16 | * named(schemaMetadata), // The metadata. 17 | * named(_$originalName), 18 | * named(_$hashModifier) 19 | * ) 20 | * ``` 21 | */ 22 | extension ModelMacro: MemberMacro { // @attached(member, names:...) 23 | 24 | public static func expansion( 25 | of macroNode : AttributeSyntax, 26 | providingMembersOf declaration : some DeclGroupSyntax, 27 | in context : some MacroExpansionContext 28 | ) throws -> [ DeclSyntax ] 29 | { 30 | guard let classDecl = declaration.as(ClassDeclSyntax.self) else { 31 | context.diagnose(.modelMacroCanOnlyBeAppliedOnNSManagedObjects, 32 | on: macroNode) 33 | return [] // TBD: rather throw? 34 | } 35 | 36 | let modelClassName = classDecl.name.trimmed 37 | 38 | let properties = findModelProperties(in: classDecl, 39 | errorNode: macroNode, context: context) 40 | let access = classDecl.isPublicOrOpen ? "public " : "" 41 | 42 | var newMembers = generateInitializers( 43 | for: classDecl, 44 | access: access, modelClassName: modelClassName, 45 | properties: properties, 46 | initializers: findInitializers(in: classDecl) 47 | ) 48 | 49 | let metadata = generateMetadataSlot( 50 | access: access, 51 | modelClassName: modelClassName, 52 | properties: properties 53 | ) 54 | newMembers.append(DeclSyntax(metadata)) 55 | 56 | // TODO: Lookup `originalName` parameter in `macroNode` 57 | newMembers.append( 58 | """ 59 | \(raw: access)static let _$originalName : String? = nil 60 | """ 61 | ) 62 | newMembers.append( 63 | """ 64 | \(raw: access)static let _$hashModifier : String? = nil 65 | """ 66 | ) 67 | 68 | return newMembers 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Sources/ManagedModelMacros/Models/ModelInitializers.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2023 ZeeZide GmbH. 4 | // 5 | 6 | import SwiftSyntax 7 | import SwiftSyntaxMacros 8 | 9 | struct ModelInitializer { 10 | 11 | /// Whether the initializer is a convenience initializer. 12 | let isConvenience : Bool 13 | 14 | /// Just the keyword parts of the selector, empty for wildcard. 15 | let parameterKeywords : [ String ] 16 | 17 | let numberOfParametersWithoutDefaults : Int 18 | } 19 | 20 | extension Collection where Element == ModelInitializer { 21 | 22 | /// Either has a plain `init()` or an init that has all parameters w/ a 23 | /// default (e.g. `init(title: String = "")`) which can be called w/o 24 | /// specifying parameters. 25 | var hasNoArgumentInitializer: Bool { 26 | guard !self.isEmpty else { return false } 27 | return self.contains(where: { $0.numberOfParametersWithoutDefaults == 0 }) 28 | } 29 | 30 | var hasDesignatedInitializers: Bool { 31 | guard !self.isEmpty else { return false } 32 | return self.contains(where: { !$0.isConvenience }) 33 | } 34 | } 35 | 36 | extension ModelInitializer: CustomStringConvertible { 37 | var description: String { 38 | var ms = " [ ModelInitializer ] 53 | { 54 | var initializers = [ ModelInitializer ]() 55 | 56 | for member : MemberBlockItemSyntax in classDecl.memberBlock.members { 57 | guard let initDecl = member.decl.as(InitializerDeclSyntax.self) else { 58 | continue 59 | } 60 | 61 | var numberOfParametersWithoutDefaults = 0 62 | var keywords = [ String ]() 63 | for parameter : FunctionParameterSyntax 64 | in initDecl.signature.parameterClause.parameters 65 | { 66 | if parameter.firstName.tokenKind == .wildcard { keywords.append("") } 67 | else { keywords.append(parameter.firstName.trimmedDescription) } 68 | 69 | if parameter.defaultValue == nil { 70 | numberOfParametersWithoutDefaults += 1 71 | } 72 | } 73 | 74 | initializers.append(ModelInitializer( 75 | isConvenience: initDecl.isConvenience, 76 | parameterKeywords: keywords, 77 | numberOfParametersWithoutDefaults: numberOfParametersWithoutDefaults 78 | )) 79 | } 80 | return initializers 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /Sources/ManagedModelMacros/Models/ModelProperty.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2023 ZeeZide GmbH. 4 | // 5 | 6 | import SwiftSyntax 7 | import SwiftSyntaxMacros 8 | 9 | private let forbiddenPropertyNames : Set = [ 10 | "context", "entity", "managedObjectContext", "objectID", 11 | "description", 12 | "inserted", "updated", "deleted", "hasChanges", "hasPersistentChangedValues", 13 | "isFault" 14 | ] 15 | 16 | /** 17 | * Information about a variable that we detected as a property. 18 | */ 19 | struct ModelProperty { 20 | 21 | /** 22 | * Whether the property is an attribute or relationship, 23 | * or whether it is unknown at macro expansion time. 24 | */ 25 | enum PropertyType { 26 | 27 | /// The property had no `@Relationship` or `@Attribute` marker macros. 28 | case plain 29 | 30 | /// The property was explicitly tagged as a `Relationship`. 31 | case relationship(AttributeSyntax) 32 | 33 | /// The property was explicitly tagged as an `Attribute`. 34 | case attribute (AttributeSyntax, isTransformable: Bool) 35 | } 36 | 37 | let binding : PatternBindingSyntax 38 | 39 | // Extracted information 40 | let type : PropertyType 41 | let name : String 42 | 43 | let declaredValueType : TypeSyntax? 44 | 45 | let isTransient : Bool 46 | let initExpression : ExprSyntax? 47 | } 48 | 49 | extension ModelProperty { 50 | 51 | var isTransformable : Bool { 52 | switch type { 53 | case .plain : return false 54 | case .relationship(_) : return false 55 | case .attribute(_, isTransformable: let value): return value 56 | } 57 | } 58 | } 59 | 60 | extension ModelProperty { 61 | 62 | /** 63 | * If the property type was not declared, attempt to derive the type from 64 | * expression. 65 | * 66 | * Example: 67 | * ``` 68 | * var lastname = "Street" 69 | * ``` 70 | * => type will be `String`, because it is a String literal initializer. 71 | */ 72 | var valueType: TypeSyntax? { 73 | if let declaredValueType { return declaredValueType } 74 | 75 | guard let initExpression else { return nil } 76 | return initExpression.detectExpressionType() 77 | } 78 | 79 | var isKnownAttributePropertyType: Bool { 80 | switch type { 81 | case .attribute(_, _) : return true 82 | case .relationship(_) : return false 83 | case .plain: return valueType?.isKnownAttributePropertyType ?? false 84 | } 85 | } 86 | var isKnownRelationshipPropertyType: Bool { 87 | switch type { 88 | case .attribute(_, _) : return false 89 | case .relationship(_) : return true 90 | case .plain: return valueType?.isKnownRelationshipPropertyType ?? false 91 | } 92 | } 93 | } 94 | 95 | extension ModelProperty: CustomStringConvertible { 96 | 97 | var description: String { 98 | var ms = " [ ModelProperty ] 131 | { 132 | var properties = [ ModelProperty ]() 133 | 134 | for member : MemberBlockItemSyntax in classDecl.memberBlock.members { 135 | // Note: One "member block" can contain multiple variable declarations. 136 | // Like in: `let a = 5, b = 6`. 137 | addModelProperties(in: member.decl, to: &properties, 138 | context: context) 139 | } 140 | 141 | return properties 142 | } 143 | 144 | /** 145 | * This creates a `ModelProperty` object/value for each property in the 146 | * given `VariableDeclSyntax`. 147 | * 148 | * A variable decl is something like this: 149 | * ```swift 150 | * @Relationship(inverse: \.blub) 151 | * var street, city, country : String 152 | * ``` 153 | * Note that attributes (e.g. `@Relationship` is one) are attached to the 154 | * whole declaration, but the declaration can have multiple "bindings", 155 | * i.e. properties. 156 | * 157 | * It uses the `propertyType` function below to look for the attributes. 158 | */ 159 | static func addModelProperties(in member: T, 160 | to properties: inout [ ModelProperty ], 161 | context: some MacroExpansionContext) 162 | where T: SyntaxProtocol 163 | { 164 | // Note: One "member block" can contain multiple variable declarations. 165 | // Like in: `let a = 5, b = 6`. 166 | guard let variables = member.as(VariableDeclSyntax.self) else { 167 | return 168 | } 169 | guard !variables.isStaticOrClass else { return } 170 | 171 | // Those apply to all variables in the declaration! 172 | let ( propertyType, isTransient ) = propertyType( 173 | for: variables.attributes, context: context 174 | ) 175 | 176 | // Each binding is a variable in a declaration list, 177 | // e.g. `let a = 5, b = 6`, the `a` and `b` would be bindings. 178 | for binding : PatternBindingSyntax in variables.bindings { 179 | guard binding.accessorBlock == nil else { 180 | // Either this is a computed property or _Swift_ property observers, 181 | // which are not allowed w/ `@NSManaged`. 182 | continue 183 | } 184 | 185 | guard let pattern = binding.pattern.as(IdentifierPatternSyntax.self) else 186 | { 187 | continue 188 | } 189 | let name = pattern.identifier.trimmed.text 190 | 191 | guard !forbiddenPropertyNames.contains(name) else { 192 | context.diagnose(.propertyNameIsForbidden, 193 | on: member) 194 | continue 195 | } 196 | 197 | properties.append(ModelProperty( 198 | binding: binding, 199 | type: propertyType, 200 | name: name, 201 | declaredValueType: binding.typeAnnotation?.type, // w/o the `:` 202 | isTransient: isTransient, 203 | initExpression: binding.initializer?.value // w/o the `=` 204 | )) 205 | } 206 | } 207 | 208 | private static func propertyType( 209 | for attributes: AttributeListSyntax, 210 | context: some MacroExpansionContext 211 | ) -> ( propertyType : ModelProperty.PropertyType, isTransient: Bool ) 212 | { 213 | // Those apply to all variables in the declaration! 214 | var propertyType = ModelProperty.PropertyType.plain 215 | var isTransient = false 216 | 217 | for attribute : AttributeListSyntax.Element in attributes { 218 | switch attribute { 219 | case .ifConfigDecl(_): 220 | continue 221 | 222 | case .attribute(let attribute): 223 | guard let name = attribute 224 | .attributeName.as(IdentifierTypeSyntax.self)? 225 | .name.trimmed.text else 226 | { 227 | continue 228 | } 229 | switch name { 230 | case "Transient": 231 | assert(!isTransient, "Transient macro applied twice!") 232 | isTransient = true 233 | 234 | case "Attribute": 235 | switch propertyType { 236 | case .plain: 237 | propertyType = .attribute( 238 | attribute, 239 | isTransformable: isTransformableAttribute(attribute) 240 | ) 241 | case .attribute(_, _): 242 | context.diagnose(.multipleAttributeMacrosAppliedOnProperty, 243 | on: attributes) 244 | case .relationship(_): 245 | context.diagnose( 246 | .attributeAndRelationshipMacrosAppliedOnProperty, 247 | on: attributes 248 | ) 249 | } 250 | 251 | case "Relationship": 252 | switch propertyType { 253 | case .plain: 254 | propertyType = .relationship(attribute) 255 | case .relationship(_): 256 | context.diagnose( 257 | .multipleRelationshipMacrosAppliedOnProperty, 258 | on: attributes 259 | ) 260 | case .attribute(_, _): 261 | context.diagnose( 262 | .attributeAndRelationshipMacrosAppliedOnProperty, 263 | on: attributes 264 | ) 265 | } 266 | 267 | default: 268 | break 269 | } 270 | 271 | } 272 | } 273 | 274 | return ( propertyType, isTransient ) 275 | } 276 | 277 | // Check whether the attribute specified a `.transformable` option. 278 | // Check for: `@Attribute(.transformable(by: xx))`, 279 | // Signature: (_ options: Option..., originalName...) 280 | private static func isTransformableAttribute(_ syntax: AttributeSyntax) 281 | -> Bool 282 | { 283 | guard let arguments = syntax.arguments, 284 | case .argumentList(let labeledExpressions) = arguments else 285 | { 286 | return false 287 | } 288 | 289 | for labeledExpression in labeledExpressions { 290 | guard labeledExpression.label == nil else { break } 291 | 292 | guard let funCall = labeledExpression.expression 293 | .as(FunctionCallExprSyntax.self), 294 | let member = funCall.calledExpression 295 | .as(MemberAccessExprSyntax.self) 296 | else { 297 | continue 298 | } 299 | guard case .identifier(let name) = 300 | member.declName.baseName.tokenKind else 301 | { 302 | continue 303 | } 304 | 305 | // Could be more advanced :-) 306 | if name == "transformable" { return true } 307 | } 308 | 309 | return false 310 | } 311 | } 312 | -------------------------------------------------------------------------------- /Sources/ManagedModelMacros/PropertyMacros/AttributeMacro.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2023 ZeeZide GmbH. 4 | // 5 | 6 | import SwiftCompilerPlugin 7 | import SwiftSyntax 8 | import SwiftSyntaxBuilder 9 | import SwiftSyntaxMacros 10 | 11 | public struct AttributeMacro: PeerMacro { // @attached(peer) macro 12 | 13 | public static func expansion( 14 | of macroNode : AttributeSyntax, 15 | providingPeersOf declaration : some DeclSyntaxProtocol, 16 | in context : some MacroExpansionContext 17 | ) throws -> [ DeclSyntax ] 18 | { 19 | [] // Annotation macro, doesn't generate anything 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Sources/ManagedModelMacros/PropertyMacros/PersistedPropertyMacro.swift: -------------------------------------------------------------------------------- 1 | import SwiftCompilerPlugin 2 | import SwiftSyntax 3 | import SwiftSyntaxBuilder 4 | import SwiftSyntaxMacros 5 | 6 | // @attached(accessor) macro 7 | public struct PersistedPropertyMacro: AccessorMacro { 8 | 9 | public static func expansion( 10 | of macroNode : AttributeSyntax, 11 | providingAccessorsOf declaration : some DeclSyntaxProtocol, 12 | in context : some MacroExpansionContext 13 | ) throws -> [ AccessorDeclSyntax ] 14 | { 15 | assert(declaration.parent == nil, "We do have access to the parent?!") 16 | 17 | var properties = [ ModelProperty ]() 18 | ModelMacro 19 | .addModelProperties(in: declaration, to: &properties, context: context) 20 | guard let property = properties.first else { return [] } 21 | if properties.count > 1 { 22 | context.diagnose(.multipleMembersInAttributesMacroCall, on: macroNode) 23 | return [] 24 | } 25 | 26 | return property.isTransformable 27 | ? [ "set { setTransformableValue(forKey: \(literal: property.name), to: newValue) }", 28 | "get { getTransformableValue(forKey: \(literal: property.name)) }" ] 29 | : [ "set { setValue(forKey: \(literal: property.name), to: newValue) }", 30 | "get { getValue(forKey: \(literal: property.name)) }" ] 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Sources/ManagedModelMacros/PropertyMacros/RelationshipMacro.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2023 ZeeZide GmbH. 4 | // 5 | 6 | import SwiftCompilerPlugin 7 | import SwiftSyntax 8 | import SwiftSyntaxBuilder 9 | import SwiftSyntaxMacros 10 | 11 | public struct RelationshipMacro: PeerMacro { // @attached(peer) macro 12 | 13 | public static func expansion( 14 | of macroNode : AttributeSyntax, 15 | providingPeersOf declaration : some DeclSyntaxProtocol, 16 | in context : some MacroExpansionContext 17 | ) throws -> [ DeclSyntax ] 18 | { 19 | [] // Annotation macro, doesn't generate anything 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Sources/ManagedModelMacros/PropertyMacros/TransientMacro.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2023 ZeeZide GmbH. 4 | // 5 | 6 | import SwiftCompilerPlugin 7 | import SwiftSyntax 8 | import SwiftSyntaxBuilder 9 | import SwiftSyntaxMacros 10 | 11 | public struct TransientMacro: PeerMacro { // @attached(peer) macro 12 | 13 | public static func expansion( 14 | of macroNode : AttributeSyntax, 15 | providingPeersOf declaration : some DeclSyntaxProtocol, 16 | in context : some MacroExpansionContext 17 | ) throws -> [ DeclSyntax ] 18 | { 19 | [] // Annotation macro, doesn't generate anything 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Sources/ManagedModelMacros/Utilities/AttributeTypes.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2023 ZeeZide GmbH. 4 | // 5 | 6 | import SwiftSyntax 7 | 8 | // This is a little fishy as the user might shadow those types, 9 | // but I suppose an acceptable tradeoff. 10 | 11 | private let swiftTypes: Set = [ 12 | // Swift 13 | "String", 14 | "Int", "Int8", "Int16", "Int32", "Int64", 15 | "UInt", "UInt8", "UInt16", "UInt32", "UInt64", 16 | "Float", "Double", 17 | "Bool", 18 | "Swift.String", 19 | "Swift.Int", "Swift.Int8", "Swift.Int16", "Swift.Int32", "Swift.Int64", 20 | "Swift.UInt", "Swift.UInt8", "Swift.UInt16", "Swift.UInt32", "Swift.UInt64", 21 | "Swift.Float", "Swift.Double", 22 | "Swift.Bool", 23 | ] 24 | private let foundationTypes: Set = [ 25 | // Foundation 26 | "Data", "Foundation.Data", 27 | "Date", "Foundation.Date", 28 | "URL", "Foundation.URL", 29 | "UUID", "Foundation.UUID", 30 | "Decimal", "Foundation.Decimal", 31 | "NSNumber", "Foundation.NSNumber", 32 | "NSString", "Foundation.NSString", 33 | "NSDecimal", "Foundation.NSDecimal", 34 | "NSURL", "Foundation.NSURL", 35 | "NSData", "Foundation.NSData" 36 | ] 37 | private let attributeTypes : Set = swiftTypes.union(foundationTypes) 38 | 39 | private let toOneRelationshipTypes : Set = [ 40 | // CoreData 41 | "NSManagedObject", "CoreData.NSManagedObject", 42 | // TBD: Those would be wrapped? 43 | "any PersistentModel", "any ManagedModels.PersistentModel" 44 | ] 45 | private let toManyRelationshipTypes : Set = [ 46 | // Foundation 47 | "Array", "Foundation.Array", 48 | "NSArray", "Foundation.NSArray", 49 | "Set", "Foundation.Set", 50 | "NSSet", "Foundation.NSSet", 51 | "NSOrderedSet", "Foundation.NSOrderedSet" 52 | ] 53 | 54 | extension TypeSyntax { 55 | 56 | /// Whether the type can be represented in Objective-C. 57 | /// A *very* basic implementation. 58 | var canBeRepresentedInObjectiveC : Bool { 59 | if let opt = self.as(OptionalTypeSyntax.self) { 60 | if let array = opt.wrappedType.as(ArrayTypeSyntax.self) { 61 | return array.element.canBeRepresentedInObjectiveC 62 | } 63 | if let id = opt.wrappedType.as(IdentifierTypeSyntax.self) { 64 | if id.isKnownRelationshipPropertyType { 65 | let element = id.genericArgumentClause?.arguments.first?.argument 66 | return element?.isKnownAttributePropertyType ?? false 67 | } 68 | return id.isKnownFoundationPropertyType 69 | } 70 | // E.g. this is not representable: `String??`, this is `String?`. 71 | // But Double? or Int? is not representable 72 | // I.e. nesting of Optional's are not representable. 73 | return false 74 | } 75 | 76 | if let array = self.as(ArrayTypeSyntax.self) { 77 | // This *is* representable: `[String]`, 78 | // even this `[ [ 10, 20 ], [ 30, 40 ] ]` 79 | return array.element.canBeRepresentedInObjectiveC 80 | } 81 | 82 | if let id = self.as(IdentifierTypeSyntax.self), 83 | id.isKnownFoundationGenericPropertyType { 84 | let arg = id.genericArgumentClause?.arguments.first?.argument 85 | return arg?.isKnownAttributePropertyType ?? false 86 | } 87 | 88 | return self.isKnownAttributePropertyType 89 | } 90 | 91 | /** 92 | * This checks for some known basetypes, like `Int`, `String` or `Data`. 93 | * It also unwraps optionals, arrays and such. 94 | */ 95 | var isKnownAttributePropertyType : Bool { 96 | if let id = self.as(IdentifierTypeSyntax.self) { 97 | return id.isKnownAttributePropertyType 98 | } 99 | 100 | // Optionals and arrays of base types are also attributes, always 101 | if let opt = self.as(OptionalTypeSyntax.self) { 102 | return opt.wrappedType.isKnownAttributePropertyType 103 | } 104 | if let array = self.as(ArrayTypeSyntax.self) { 105 | return array.element.isKnownAttributePropertyType 106 | } 107 | return false 108 | } 109 | 110 | var isKnownRelationshipPropertyType : Bool { 111 | isKnownRelationshipPropertyType(checkOptional: true) 112 | } 113 | func isKnownRelationshipPropertyType(checkOptional: Bool) -> Bool { 114 | if let id = self.as(IdentifierTypeSyntax.self) { 115 | return id.isKnownRelationshipPropertyType 116 | } 117 | 118 | // Optionals of relationship types are also relationships, but only 119 | // at one level. 120 | if checkOptional, let opt = self.as(OptionalTypeSyntax.self) { 121 | return opt.wrappedType.isKnownRelationshipPropertyType 122 | } 123 | return false 124 | } 125 | } 126 | 127 | extension IdentifierTypeSyntax { 128 | 129 | var isKnownAttributePropertyType : Bool { 130 | let name = name.trimmed.text 131 | 132 | guard let generic = genericArgumentClause else { 133 | return attributeTypes.contains(name) 134 | } 135 | guard generic.arguments.count > 1 else { // multiple arguments 136 | return false 137 | } 138 | 139 | // `GenericArgumentSyntax` 140 | guard let genericArgument = generic.arguments.first else { 141 | assertionFailure("Generic clause but no arguments?") 142 | return false 143 | } 144 | 145 | switch name { 146 | case "Array", "Optional", "Set": 147 | return genericArgument.argument.isKnownAttributePropertyType 148 | default: 149 | return false 150 | } 151 | } 152 | 153 | var isKnownFoundationPropertyType: Bool { 154 | let name = name.trimmed.text 155 | return foundationTypes.contains(name) 156 | } 157 | 158 | var isKnownFoundationGenericPropertyType: Bool { 159 | let name = name.trimmed.text 160 | guard toManyRelationshipTypes.contains(name) else { 161 | return false 162 | } 163 | if let generic = genericArgumentClause { 164 | return generic.arguments.count == 1 165 | } 166 | return false 167 | } 168 | 169 | var isKnownRelationshipPropertyType : Bool { 170 | isKnownRelationshipPropertyType(checkOptional: true) 171 | } 172 | 173 | func isKnownRelationshipPropertyType(checkOptional: Bool) -> Bool { 174 | let name = name.trimmed.text 175 | 176 | if name == "Optional" { // recurse 177 | guard let generic = genericArgumentClause, 178 | let genericArgument = generic.arguments.first, 179 | generic.arguments.count != 1 else 180 | { 181 | return false 182 | } 183 | return genericArgument.argument 184 | .isKnownRelationshipPropertyType(checkOptional: false) 185 | } 186 | 187 | if toOneRelationshipTypes.contains(name) { 188 | return true 189 | } 190 | 191 | guard toManyRelationshipTypes.contains(name) else { 192 | return false 193 | } 194 | 195 | if let generic = genericArgumentClause { 196 | return generic.arguments.count == 1 197 | } 198 | else { 199 | return true 200 | } 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /Sources/ManagedModelMacros/Utilities/ClassDeclSyntax+Extras.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2023 ZeeZide GmbH. 4 | // 5 | 6 | import SwiftSyntax 7 | 8 | extension ClassDeclSyntax { 9 | 10 | var isPublicOrOpen : Bool { modifiers.containsPublicOrOpen } 11 | 12 | var publicOrOpenModifier : DeclModifierListSyntax.Element? { 13 | modifiers.publicOrOpenModifier 14 | } 15 | } 16 | 17 | extension ClassDeclSyntax { 18 | 19 | var inheritedTypeNames : Set { 20 | // The protocol types in like (as a workaround for protocols below) 21 | // `class Model: Codable, CustomStringConvertible` 22 | var inheritedTypeNames = Set() 23 | if let inheritedTypes = inheritanceClause?.inheritedTypes { 24 | for type : InheritedTypeSyntax in inheritedTypes { 25 | let typeSyntax : TypeSyntax = type.type 26 | if let id = typeSyntax.as(IdentifierTypeSyntax.self) { 27 | inheritedTypeNames.insert(id.name.trimmed.text) 28 | } 29 | } 30 | } 31 | return inheritedTypeNames 32 | } 33 | } 34 | 35 | extension ClassDeclSyntax { 36 | 37 | func findFunctionWithName(_ name: String, isStaticOrClass: Bool, 38 | parameterCount: Int? = nil, 39 | numberOfParametersWithoutDefaults: Int? = nil) 40 | -> FunctionDeclSyntax? 41 | { 42 | for member : MemberBlockItemSyntax in memberBlock.members { 43 | guard let funcDecl = member.decl.as(FunctionDeclSyntax.self) else { 44 | continue 45 | } 46 | let hasStatic = funcDecl.modifiers.containsStaticOrClass 47 | guard hasStatic == isStaticOrClass else { continue } 48 | 49 | if let parameterCount { 50 | guard parameterCount == funcDecl.parameterCount else { continue } 51 | } 52 | if let numberOfParametersWithoutDefaults { 53 | guard numberOfParametersWithoutDefaults == 54 | funcDecl.numberOfParametersWithoutDefaults else { continue } 55 | } 56 | 57 | // filter out operators and different names 58 | guard case .identifier(let idName) = funcDecl.name.tokenKind, 59 | idName == name else 60 | { 61 | continue 62 | } 63 | 64 | // Found it 65 | return funcDecl 66 | } 67 | return nil 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Sources/ManagedModelMacros/Utilities/ContextHelpers.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2023 ZeeZide GmbH. 4 | // 5 | 6 | import SwiftSyntax 7 | import SwiftSyntaxMacros 8 | import SwiftDiagnostics 9 | 10 | extension MacroExpansionContext { 11 | 12 | // TBD: rather put this on the "error node"? (the macro) 13 | func diagnose(_ message: MacroDiagnostic, on errorNode: N) 14 | { 15 | diagnose(Diagnostic(node: Syntax(errorNode), message: message)) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Sources/ManagedModelMacros/Utilities/DeclHelpers.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2023 ZeeZide GmbH. 4 | // 5 | 6 | import SwiftSyntax 7 | 8 | extension VariableDeclSyntax { 9 | 10 | var isStaticOrClass : Bool { modifiers.containsStaticOrClass } 11 | var isPublicOrOpen : Bool { modifiers.containsPublicOrOpen } 12 | } 13 | 14 | extension InitializerDeclSyntax { 15 | 16 | var isConvenience : Bool { 17 | modifiers.contains { $0.name.tokenKind == .keyword(.convenience) } 18 | } 19 | 20 | var parameterCount : Int { signature.parameterClause.parameters.count } 21 | 22 | var numberOfParametersWithoutDefaults : Int { 23 | signature.parameterClause.parameters.numberOfParametersWithoutDefaults 24 | } 25 | } 26 | 27 | extension FunctionDeclSyntax { 28 | 29 | var parameterCount : Int { signature.parameterClause.parameters.count } 30 | 31 | var numberOfParametersWithoutDefaults : Int { 32 | signature.parameterClause.parameters.numberOfParametersWithoutDefaults 33 | } 34 | } 35 | 36 | extension FunctionParameterListSyntax { 37 | 38 | var numberOfParametersWithoutDefaults : Int { 39 | var count = 0 40 | for parameter : FunctionParameterSyntax in self { 41 | guard parameter.defaultValue == nil else { continue } 42 | count += 1 43 | } 44 | return count 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Sources/ManagedModelMacros/Utilities/DeclModifierListSyntax+Extras.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2023 ZeeZide GmbH. 4 | // 5 | 6 | import SwiftSyntax 7 | 8 | extension DeclModifierListSyntax { 9 | 10 | var modifiers : Set { 11 | var modifiers = Set() 12 | for modifier in self { 13 | guard case .keyword(let keyword) = modifier.name.tokenKind else { 14 | continue 15 | } 16 | modifiers.insert(keyword) 17 | } 18 | return modifiers 19 | } 20 | 21 | func contains(_ keyword: Keyword) -> Bool { 22 | for modifier in self { 23 | guard case .keyword(let keywordMember) = modifier.name.tokenKind else { 24 | continue 25 | } 26 | if keyword == keywordMember { return true } 27 | } 28 | return false 29 | } 30 | 31 | var publicOrOpenModifier : Element? { 32 | self.first { 33 | switch $0.name.tokenKind { 34 | case .keyword(.public) : return true 35 | case .keyword(.open) : return true 36 | default: return false 37 | } 38 | } 39 | } 40 | 41 | var containsPublicOrOpen : Bool { publicOrOpenModifier != nil } 42 | 43 | var containsStaticOrClass : Bool { 44 | contains { 45 | switch $0.name.tokenKind { 46 | case .keyword(.static) : return true 47 | case .keyword(.class) : return true 48 | default: return false 49 | } 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Sources/ManagedModelMacros/Utilities/ExpressionType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2023 ZeeZide GmbH. 4 | // 5 | 6 | import SwiftSyntax 7 | 8 | extension ExprSyntax { 9 | 10 | func detectExpressionType() -> TypeSyntax? { 11 | guard let idType = detectExpressionTypeName() else { return nil } 12 | return TypeSyntax(IdentifierTypeSyntax(name: .identifier(idType))) 13 | } 14 | func detectExpressionTypeName() -> String? { 15 | // TODO: detect base types and such 16 | // - check function expressions, like: 17 | // - `Data()`? 18 | // - `Date()`, `Date.now`, etc 19 | 20 | switch kind { 21 | case .stringLiteralExpr : return "Swift.String" 22 | case .integerLiteralExpr : return "Swift.Int" 23 | case .floatLiteralExpr : return "Swift.Double" 24 | case .booleanLiteralExpr : return "Swift.Bool" 25 | default: 26 | break 27 | } 28 | 29 | return nil 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Sources/ManagedModelMacros/Utilities/TypeSyntax+Extras.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2023 ZeeZide GmbH. 4 | // 5 | 6 | import SwiftSyntax 7 | 8 | extension TypeSyntax { 9 | 10 | /** 11 | * Rewrite `Product!` to `Product?`, since the former is not allowed in 12 | * type references, like `Product!.self` (is forbidden). 13 | */ 14 | func replacingImplicitlyUnwrappedOptionalTypes() -> TypeSyntax { 15 | guard let force = self.as(ImplicitlyUnwrappedOptionalTypeSyntax.self) else { 16 | return self 17 | } 18 | let regularOptional = OptionalTypeSyntax(wrappedType: force.wrappedType) 19 | return TypeSyntax(regularOptional) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Sources/ManagedModels/Container/ModelConfiguration.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2023-2024 ZeeZide GmbH. 4 | // 5 | 6 | @preconcurrency import CoreData 7 | 8 | public struct ModelConfiguration: Hashable, Sendable { 9 | // TBD: Some of those are `let` in SwiftData 10 | 11 | public var path : String 12 | public var name : String 13 | public var groupAppContainerIdentifier : String? = nil 14 | public var cloudKitContainerIdentifier : String? = nil 15 | public var groupContainer = GroupContainer.none 16 | public var cloudKitDatabase = CloudKitDatabase.none 17 | public var schema : NSManagedObjectModel? 18 | public var allowsSave = true 19 | 20 | public var isStoredInMemoryOnly : Bool { 21 | set { 22 | if newValue { 23 | path = "/dev/null" 24 | } 25 | else if path == "/dev/null" { 26 | do { path = try lookupDefaultPath(for: name) } 27 | catch { fatalError("Could not lookup path for: \(name) \(error)") } 28 | } 29 | // else: preserve existing path 30 | } 31 | get { path == "/dev/null" } 32 | } 33 | 34 | public init(path: String? = nil, name: String? = nil, 35 | schema : NSManagedObjectModel? = nil, 36 | isStoredInMemoryOnly : Bool = false, 37 | allowsSave : Bool = true, 38 | groupAppContainerIdentifier : String? = nil, 39 | cloudKitContainerIdentifier : String? = nil, 40 | groupContainer : GroupContainer = .none, 41 | cloudKitDatabase : CloudKitDatabase = .none) 42 | { 43 | let actualPath : String 44 | 45 | if let path, !path.isEmpty { 46 | actualPath = path 47 | } 48 | else if isStoredInMemoryOnly { 49 | actualPath = "/dev/null" 50 | } 51 | else { 52 | do { 53 | actualPath = try lookupDefaultPath(for: name) 54 | } 55 | catch { 56 | fatalError("Could not lookup path for model \(name ?? "?"): \(error)") 57 | } 58 | } 59 | var defaultName : String { 60 | if isStoredInMemoryOnly || actualPath == "/dev/null" { return "InMemory" } 61 | if let idx = actualPath.lastIndex(of: "/") { 62 | return String(actualPath[idx...].dropFirst()) 63 | } 64 | return actualPath 65 | } 66 | 67 | self.path = actualPath 68 | self.name = name ?? defaultName 69 | self.groupAppContainerIdentifier = groupAppContainerIdentifier 70 | self.cloudKitContainerIdentifier = cloudKitContainerIdentifier 71 | self.groupContainer = groupContainer 72 | self.cloudKitDatabase = cloudKitDatabase 73 | self.schema = schema 74 | self.allowsSave = allowsSave 75 | } 76 | } 77 | 78 | extension ModelConfiguration: Identifiable { 79 | 80 | @inlinable 81 | public var id : String { path } 82 | } 83 | 84 | public extension ModelConfiguration { 85 | 86 | struct GroupContainer: Hashable, Sendable { 87 | enum Value: Hashable { 88 | case automatic, none 89 | case identifier(String) 90 | } 91 | let value : Value 92 | 93 | public static let automatic = Self(value: .automatic) 94 | public static let none = Self(value: .none) 95 | public static func identifier(_ groupName: String) -> Self { 96 | .init(value: .identifier(groupName)) 97 | } 98 | } 99 | } 100 | 101 | public extension ModelConfiguration { 102 | 103 | struct CloudKitDatabase: Hashable, Sendable { 104 | enum Value: Hashable { 105 | case automatic, none 106 | case `private`(String) 107 | } 108 | let value : Value 109 | 110 | public static let automatic = Self(value: .automatic) 111 | public static let none = Self(value: .none) 112 | public static func `private`(_ dbName: String) -> Self { 113 | .init(value: .private(dbName)) 114 | } 115 | } 116 | } 117 | 118 | public extension ModelConfiguration { 119 | 120 | @inlinable 121 | var url : URL { 122 | set { path = newValue.path } 123 | get { URL(fileURLWithPath: path) } 124 | } 125 | 126 | @inlinable 127 | init(_ name: String? = nil, schema: Schema? = nil, url: URL, 128 | isStoredInMemoryOnly: Bool = false, allowsSave: Bool = true, 129 | groupAppContainerIdentifier: String? = nil, 130 | cloudKitContainerIdentifier: String? = nil, 131 | groupContainer: GroupContainer = .none, 132 | cloudKitDatabase: CloudKitDatabase = .none) 133 | { 134 | self.init(path: url.path, name: name ?? url.lastPathComponent, 135 | schema: schema, 136 | isStoredInMemoryOnly: isStoredInMemoryOnly, 137 | allowsSave: allowsSave, 138 | groupAppContainerIdentifier: groupAppContainerIdentifier, 139 | cloudKitContainerIdentifier: cloudKitContainerIdentifier, 140 | groupContainer: groupContainer, 141 | cloudKitDatabase: cloudKitDatabase) 142 | } 143 | 144 | @inlinable 145 | init(isStoredInMemoryOnly: Bool) { 146 | self.init(schema: nil, isStoredInMemoryOnly: isStoredInMemoryOnly) 147 | } 148 | @inlinable 149 | init(for forTypes: any PersistentModel.Type..., 150 | isStoredInMemoryOnly: Bool = false) 151 | { 152 | let model = NSManagedObjectModel(forTypes) 153 | self.init(schema: model, isStoredInMemoryOnly: isStoredInMemoryOnly) 154 | } 155 | } 156 | 157 | private func lookupDefaultPath(for name: String?) throws -> String { 158 | // Synchronous I/O, hm. 159 | let filename = (name?.isEmpty ?? true) ? "default.sqlite" : (name ?? "") 160 | 161 | let fm = FileManager.default 162 | guard let appSupportURL = 163 | fm.urls(for: .applicationSupportDirectory, in: .userDomainMask).first 164 | else { 165 | struct MissingAppSupport: Swift.Error {} 166 | throw MissingAppSupport() 167 | } 168 | // Make sure it exists 169 | try fm.createDirectory(at: appSupportURL, 170 | withIntermediateDirectories: true) 171 | let url = appSupportURL.appendingPathComponent(filename) 172 | return url.path 173 | } 174 | -------------------------------------------------------------------------------- /Sources/ManagedModels/Container/ModelContainer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2023 ZeeZide GmbH. 4 | // 5 | 6 | import CoreData 7 | 8 | public typealias ModelContainer = NSPersistentContainer 9 | -------------------------------------------------------------------------------- /Sources/ManagedModels/Container/NSPersistentContainer+Data.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2023-2024 ZeeZide GmbH. 4 | // 5 | 6 | import CoreData 7 | 8 | extension NSPersistentContainer { 9 | 10 | @inlinable 11 | public var schema : NSManagedObjectModel { managedObjectModel } 12 | 13 | //@MainActor - TBD :-) 14 | @inlinable 15 | public var mainContext : NSManagedObjectContext { viewContext } 16 | 17 | @inlinable 18 | public var configurations : [ NSPersistentStoreDescription ] { 19 | persistentStoreDescriptions 20 | } 21 | 22 | convenience 23 | public init(for model : NSManagedObjectModel, 24 | migrationPlan : SchemaMigrationPlan.Type? = nil, 25 | configurations : [ ModelConfiguration ]) throws 26 | { 27 | precondition(migrationPlan == nil, "Migration plans not yet supported") 28 | 29 | let combinedModel : NSManagedObjectModel = { 30 | guard let firstConfig = configurations.first else { return model } 31 | if configurations.count == 1, 32 | firstConfig.schema == nil || firstConfig.schema == model 33 | { 34 | return model 35 | } 36 | 37 | var allModels = [ ObjectIdentifier : NSManagedObjectModel ]() 38 | allModels[ObjectIdentifier(model)] = model 39 | for config in configurations { 40 | guard let model = config.schema else { continue } 41 | allModels[ObjectIdentifier(model)] = model 42 | } 43 | guard allModels.count > 1 else { return model } 44 | let merged = NSManagedObjectModel(byMerging: Array(allModels.values)) 45 | assert(merged != nil, "Could not combine object models: \(allModels)") 46 | return merged ?? model 47 | }() 48 | 49 | var configurations = configurations 50 | if configurations.isEmpty { 51 | configurations.append(Self.defaultConfiguration) 52 | } 53 | 54 | // TBD: Is this correct? It is the container name, not the configuration 55 | // name? 56 | let firstName = configurations.first(where: { !$0.name.isEmpty })?.name 57 | ?? "ManagedModels" 58 | 59 | assert(!configurations.isEmpty) 60 | self.init( 61 | name: firstName, 62 | managedObjectModel: combinedModel 63 | ) 64 | persistentStoreDescriptions = configurations.map { .init($0) } 65 | 66 | // This seems to run synchronously unless the storeDescription has 67 | // `shouldAddStoreAsynchronously`. 68 | var errors = [ Swift.Error ]() 69 | loadPersistentStores { (storeDescription, error) in 70 | if let error { 71 | if !storeDescription.shouldAddStoreAsynchronously { 72 | errors.append(error) 73 | } 74 | else { // TBD: how to report those errors? Delegate? 75 | // Well, the modifiers take a closure for that, use it?! 76 | fatalError("Failed to add store: \(error), \(storeDescription)") 77 | } 78 | } 79 | } 80 | if let error = errors.first { // TODO: Combine multiple errors :-) 81 | throw error 82 | } 83 | 84 | viewContext.automaticallyMergesChangesFromParent = true 85 | } 86 | 87 | private static var defaultConfiguration : ModelConfiguration { 88 | .init( 89 | path: nil, name: nil, schema: nil, 90 | isStoredInMemoryOnly: false, allowsSave: true, 91 | groupAppContainerIdentifier: nil, cloudKitContainerIdentifier: nil, 92 | groupContainer: .none, cloudKitDatabase: .none 93 | ) 94 | } 95 | } 96 | 97 | public extension NSPersistentContainer { 98 | 99 | @inlinable 100 | convenience init(for model : NSManagedObjectModel, 101 | migrationPlan : SchemaMigrationPlan.Type? = nil, 102 | configurations : ModelConfiguration...) throws 103 | { 104 | try self.init(for: model, migrationPlan: migrationPlan, 105 | configurations: configurations) 106 | } 107 | 108 | @inlinable 109 | convenience init(for types : any PersistentModel.Type..., 110 | migrationPlan : SchemaMigrationPlan.Type? = nil, 111 | configurations : ModelConfiguration...) throws 112 | { 113 | let model = NSManagedObjectModel.model(for: types) // this caches! 114 | try self.init(for: model, migrationPlan: migrationPlan, 115 | configurations: configurations) 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /Sources/ManagedModels/Container/NSPersistentStoreDescription+Data.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2023 ZeeZide GmbH. 4 | // 5 | 6 | import CoreData 7 | 8 | extension NSPersistentStoreDescription { 9 | 10 | convenience init(_ modelConfiguration: ModelConfiguration) { 11 | self.init(url: modelConfiguration.url) 12 | 13 | type = NSSQLiteStoreType 14 | isReadOnly = !modelConfiguration.allowsSave 15 | 16 | // Setting a name produces issues, I think because an NSEntityDescription 17 | // object is actually bound to a specific configuration. 18 | // This conflicts w/ our setup? 19 | #if false 20 | if !modelConfiguration.name.isEmpty { 21 | configuration = modelConfiguration.name 22 | } 23 | #endif 24 | 25 | // TBD: options, timeout, sqlitePragmas 26 | 27 | if !modelConfiguration.allowsSave { 28 | shouldMigrateStoreAutomatically = false 29 | } 30 | 31 | shouldAddStoreAsynchronously = false 32 | // shouldMigrateStoreAutomatically 33 | // shouldInferMappingModelAutomatically 34 | 35 | /* TBD. Maybe those are options? 36 | CoreData (NSPersistentCloudKitContainerOptions/.cloudKitContainerOptions): 37 | - containerIdentifier: String 38 | 39 | SwiftData 40 | groupAppContainerIdentifier : String? = nil 41 | cloudKitContainerIdentifier : String? = nil 42 | groupContainer = GroupContainer.none 43 | .automatic 44 | .none 45 | .identifier(_ groupName: String) -> Self { 46 | cloudKitDatabase = CloudKitDatabase.none 47 | .automatic = Self(value: .automatic) 48 | .none = Self(value: .none) 49 | .`private`(_ dbName: String) -> Self { 50 | */ 51 | precondition(modelConfiguration.groupAppContainerIdentifier == .none, 52 | "groupAppContainerIdentifier config is not yet supported") 53 | precondition(modelConfiguration.groupContainer == .none, 54 | "groupContainer config is not yet supported") 55 | precondition(modelConfiguration.cloudKitDatabase == .none, 56 | "cloudKitDatabase config is not yet supported") 57 | if let id = modelConfiguration.cloudKitContainerIdentifier { 58 | self.cloudKitContainerOptions = 59 | NSPersistentCloudKitContainerOptions(containerIdentifier: id) 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Sources/ManagedModels/Context/ModelContext.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2023 ZeeZide GmbH. 4 | // 5 | 6 | import CoreData 7 | 8 | public typealias ModelContext = NSManagedObjectContext 9 | -------------------------------------------------------------------------------- /Sources/ManagedModels/Context/NSManagedObjectContext+Data.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2023 ZeeZide GmbH. 4 | // 5 | 6 | import CoreData 7 | 8 | // TODO: autosave 9 | 10 | public extension NSManagedObjectContext { 11 | 12 | @inlinable 13 | var autosaveEnabled : Bool { false } // TODO 14 | 15 | @inlinable 16 | var insertedModels : Set { insertedObjects } 17 | @inlinable 18 | var changedModels : Set { updatedObjects } 19 | @inlinable 20 | var deletedModels : Set { deletedObjects } 21 | @inlinable 22 | var registeredModels : Set { registeredObjects } 23 | 24 | @inlinable 25 | var insertedModelsArray : [NSManagedObject] { Array(insertedModels) } 26 | @inlinable 27 | var changedModelsArray : [NSManagedObject] { Array(changedModels) } 28 | @inlinable 29 | var deletedModelsArray : [NSManagedObject] { Array(deletedModels) } 30 | 31 | /** 32 | * Check whether a model with the given ``PersistentIdentifier`` is known to 33 | * the storage. 34 | */ 35 | @inlinable 36 | func registeredModel(for id: NSManagedObjectID) -> T? 37 | where T: NSManagedObject 38 | { 39 | // Note: This needs the type for a call, e.g. 40 | // `let x : Address = ctx.registeredModel(for: id)` 41 | for object in registeredObjects { // Ugh, scan 42 | if object.objectID == id { return object as? T } 43 | } 44 | return nil 45 | } 46 | } 47 | 48 | public extension NSManagedObjectContext { 49 | 50 | @inlinable 51 | func fetchCount(_ request: NSFetchRequest) throws -> Int 52 | where T: NSFetchRequestResult 53 | { 54 | try count(for: request) 55 | } 56 | } 57 | 58 | public extension NSManagedObjectContext { 59 | 60 | static let willSave = willSaveObjectsNotification 61 | static let didSave = didSaveObjectsNotification 62 | } 63 | 64 | public extension NSManagedObjectContext { 65 | 66 | @inlinable 67 | convenience init(_ container: ModelContainer) { 68 | self.init(concurrencyType: .mainQueueConcurrencyType) // TBD 69 | persistentStoreCoordinator = container.persistentStoreCoordinator 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /Sources/ManagedModels/Documentation.docc/DifferencesToSwiftData.md: -------------------------------------------------------------------------------- 1 | # Differences to SwiftData 2 | 3 | ManagedObjects tries to provide an API that is very close to SwiftData, 4 | but it is not exactly the same API. 5 | 6 | ## Differences 7 | 8 | The key difference when converting SwiftData projects: 9 | - Import `ManagedModels` instead of `SwiftData`. 10 | - Let the models inherit from the CoreData 11 | [`NSManagedObject`](https://developer.apple.com/documentation/coredata/nsmanagedobject). 12 | - Use the CoreData 13 | [`@FetchRequest`](https://developer.apple.com/documentation/swiftui/fetchrequest) 14 | instead of SwiftData's 15 | [`@Query`](https://developer.apple.com/documentation/swiftdata/query). 16 | 17 | 18 | ### Explicit Superclass 19 | 20 | ManagedModels classes must explicitly inherit from the CoreData 21 | [`NSManagedObject`](https://developer.apple.com/documentation/coredata/nsmanagedobject). 22 | Instead of just this in SwiftData: 23 | ```swift 24 | @Model class Contact {} 25 | ``` 26 | the superclass has to be specified w/ ManagedModels: 27 | ```swift 28 | @Model class Contact: NSManagedObject {} 29 | ``` 30 | 31 | > That is due to a limitation of 32 | > [Swift Macros](https://docs.swift.org/swift-book/documentation/the-swift-programming-language/macros/). 33 | > A macro can add protocol conformances, but it cannot add a superclass to a 34 | > type. 35 | 36 | 37 | ### CoreData @FetchRequest instead of SwiftData @Query 38 | 39 | Instead of using the new SwiftUI 40 | [`@Query`](https://developer.apple.com/documentation/swiftdata/query) 41 | wrapper, the already available 42 | [`@FetchRequest`](https://developer.apple.com/documentation/swiftui/fetchrequest) 43 | property wrapper is used. 44 | 45 | SwiftData: 46 | ```swift 47 | @Query var contacts : [ Contact ] 48 | ``` 49 | ManagedModels: 50 | ```swift 51 | @FetchRequest var contacts: FetchedResults 52 | ``` 53 | 54 | ### Properties 55 | 56 | The properties work quite similar. 57 | 58 | Like SwiftData, ManagedModels provides implementations of the 59 | [`@Attribute`](https://developer.apple.com/documentation/swiftdata/attribute(_:originalname:hashmodifier:)), 60 | `@Relationship` and 61 | [`@Transient`](https://developer.apple.com/documentation/swiftdata/transient()) 62 | macros. 63 | 64 | #### Compound Properties 65 | 66 | More complex Swift types are always stored as JSON by ManagedModels, e.g. 67 | ```swift 68 | @Model class Person: NSManagedObject { 69 | 70 | struct Address: Codable { 71 | var street : String? 72 | var city : String? 73 | } 74 | 75 | var privateAddress : Address 76 | var businessAddress : Address 77 | } 78 | ``` 79 | 80 | SwiftData decomposes those structures in the database. 81 | 82 | 83 | #### RawRepresentable Properties 84 | 85 | Those end up working the same like in SwiftData, but are implemented 86 | differently. 87 | If a type is RawRepresentable by a CoreData base type (like `Int` or `String`), 88 | they get mapped to the same base type in the model. 89 | 90 | Example: 91 | ```swift 92 | enum Color: String { 93 | case red, green, blue 94 | } 95 | 96 | enum Priority: Int { 97 | case high = 5, medium = 3, low = 1 98 | } 99 | ``` 100 | 101 | 102 | ### Initializers 103 | 104 | A CoreData object has to be initialized through some 105 | [very specific initializer](https://developer.apple.com/documentation/coredata/nsmanagedobject/1506357-init), 106 | while a SwiftData model class _must have_ an explicit `init`, 107 | but is otherwise pretty regular. 108 | 109 | The ManagedModels `@Model` macro generates a set of helper inits to deal with 110 | that. 111 | But the general recommendation is to use a `convenience init` like so: 112 | ```swift 113 | convenience init(title: String, age: Int = 50) { 114 | self.init() 115 | title = title 116 | age = age 117 | } 118 | ``` 119 | If the own init prefilles _all_ properties (i.e. can be called w/o arguments), 120 | the default `init` helper is not generated anymore, another one has to be used: 121 | ```swift 122 | convenience init(title: String = "", age: Int = 50) { 123 | self.init(context: nil) 124 | title = title 125 | age = age 126 | } 127 | ``` 128 | The same `init(context:)` can be used to insert into a specific context. 129 | Often necessary when setting up relationships (to make sure that they 130 | live in the same 131 | [`NSManagedObjectContext`](https://developer.apple.com/documentation/coredata/nsmanagedobjectcontext)). 132 | 133 | 134 | ### Migration 135 | 136 | Regular CoreData migration mechanisms have to be used. 137 | SwiftData specific migration APIs might be 138 | [added later](https://github.com/Data-swift/ManagedModels/issues/6). 139 | 140 | 141 | ## Implementation Differences 142 | 143 | SwiftData completely wraps CoreData and doesn't expose the CoreData APIs. 144 | 145 | SwiftData relies on the 146 | [Observation](https://developer.apple.com/documentation/observation) 147 | framework (which requires iOS 17+). 148 | ManagedModels uses CoreData, which makes models conform to 149 | [ObservableObject](https://developer.apple.com/documentation/combine/observableobject) 150 | to integrate w/ SwiftUI. 151 | -------------------------------------------------------------------------------- /Sources/ManagedModels/Documentation.docc/Documentation.md: -------------------------------------------------------------------------------- 1 | # ``ManagedModels`` 2 | 3 | SwiftData like declarative schemas for CoreData. 4 | 5 | @Metadata { 6 | @DisplayName("ManagedModels for CoreData") 7 | } 8 | 9 | ## Overview 10 | 11 | [ManagedModels](https://github.com/Data-swift/ManagedModels/) is a package 12 | that provides a 13 | [Swift 5.9](https://www.swift.org/blog/swift-5.9-released/) 14 | macro similar to the SwiftData 15 | [@Model](https://developer.apple.com/documentation/SwiftData/Model()). 16 | It can generate CoreData 17 | [ManagedObjectModel](https://developer.apple.com/library/archive/documentation/DataManagement/Devpedia-CoreData/managedObjectModel.html)'s 18 | declaratively from Swift classes, 19 | w/o having to use the Xcode "CoreData Modeler". 20 | 21 | Unlike SwiftData it doesn't require iOS 17+ and works directly w/ 22 | [CoreData](https://developer.apple.com/documentation/coredata). 23 | It is **not** a direct API replacement, but a look-a-like. 24 | Example model class: 25 | ```swift 26 | @Model 27 | class ToDo: NSManagedObject { 28 | var title: String 29 | var isDone: Bool 30 | var attachments: [ Attachment ] 31 | } 32 | ``` 33 | Setting up a store in SwiftUI: 34 | ```swift 35 | ContentView() 36 | .modelContainer(for: ToDo.self) 37 | ``` 38 | Performing a query: 39 | ```swift 40 | struct ToDoListView: View { 41 | @FetchRequest(sort: \.isDone) 42 | private var toDos: FetchedResults 43 | 44 | var body: some View { 45 | ForEach(toDos) { todo in 46 | Text("\(todo.title)") 47 | .foregroundColor(todo.isDone ? .green : .red) 48 | } 49 | } 50 | } 51 | ``` 52 | 53 | - Swift package: [https://github.com/Data-swift/ManagedModels.git](https://github.com/Data-swift/ManagedModels/) 54 | - Example ToDo list app: [https://github.com/Data-swift/ManagedToDosApp.git](https://github.com/Data-swift/ManagedToDosApp/) 55 | 56 | 57 | ## Northwind 58 | 59 | A little bigger example, 60 | a port of a demo database for SwiftData to ManagedModels: 61 | [Northwind for ManagedModels](https://github.com/Northwind-swift/NorthwindManagedModels) 62 | (SwiftData original: 63 | [NorthwindSwiftData](https://github.com/Northwind-swift/NorthwindSwiftData)). 64 | 65 | This is the old [Northwind database](https://github.com/jpwhite3/northwind-SQLite3) 66 | packaged up as a Swift package that works with ManagedModels. 67 | It contains a set of model classes and a prefilled database which makes it ideal 68 | for testing, to get started quickly. 69 | 70 | Sample usage 71 | (import `https://github.com/Northwind-swift/NorthwindSwiftData.git`): 72 | ```swift 73 | import SwiftUI 74 | import NorthwindSwiftData // @Northwind-swift/NorthwindManagedModels 75 | 76 | @main 77 | struct NorthwindApp: App { 78 | 79 | var body: some Scene { 80 | WindowGroup { 81 | ContentView() 82 | } 83 | .modelContainer(try! NorthwindStore.modelContainer()) 84 | } 85 | } 86 | 87 | struct ContentView: View { 88 | 89 | @FetchRequest(sort: \.name) 90 | private var products: FetchedResults 91 | 92 | var body: some View { 93 | List { 94 | ForEach(products) { product in 95 | Text(verbatim: product.name) 96 | } 97 | } 98 | } 99 | } 100 | ``` 101 | 102 | - [Northwind for ManagedModels Documentation](https://swiftpackageindex.com/Northwind-swift/NorthwindManagedModels/documentation/northwindswiftdata) 103 | 104 | 105 | ## Topics 106 | 107 | ### Getting Started 108 | 109 | - 110 | - 111 | 112 | ### Support 113 | 114 | - 115 | - 116 | - 117 | -------------------------------------------------------------------------------- /Sources/ManagedModels/Documentation.docc/FAQ.md: -------------------------------------------------------------------------------- 1 | # Frequently Asked Questions 2 | 3 | A collection of questions and possible answers. 4 | 5 | ## Overview 6 | 7 | Any question we should add: [info@zeezide.de](mailto:info@zeezide.de), 8 | file a GitHub [Issue](https://github.com/Data-swift/ManagedModels/issues). 9 | or submit a GitHub PR w/ the answer. Thank you! 10 | 11 | ## General 12 | 13 | ### Is the API the same like in SwiftData? 14 | 15 | The API is very similar, but there are some significant differences: 16 | . 17 | 18 | ### Is ManagedObjects a replacement for SwiftData? 19 | 20 | It is not exactly the same but can be used as one, yes. 21 | ManagedObjects allows deployment on earlier versions than iOS 17 or macOS 14 22 | while still providing many of the SwiftData benefits. 23 | 24 | It might be a sensible migration path towards using SwiftData directly in the 25 | future. 26 | 27 | ### Which deployment versions does ManagedObjects support? 28 | 29 | While it might be possible to backport further, ManagedObjects currently 30 | supports: 31 | - iOS 13+ 32 | - macOS 11+ 33 | - tvOS 13+ 34 | - watchOS 6+ 35 | 36 | ### Does this require SwiftUI or can I use it in UIKit as well? 37 | 38 | ManagedObjects works with both, SwiftUI and UIKit. 39 | 40 | In a UIKit environment the ``ModelContainer`` (aka ``NSPersistentContainer``) 41 | needs to be setup manually, e.g. in the `ApplicationDelegate`. 42 | 43 | Example: 44 | ```swift 45 | import ManagedModels 46 | 47 | let schema = Schema([ Item.self ]) 48 | let container = try ModelContainer(for: schema, configurations: []) 49 | ``` 50 | 51 | ### Are the `ModelContainer`, `Schema` classes subclasses of CoreData classes? 52 | 53 | No, most of the SwiftData-like types provided by ManagedObjects are just 54 | "typealiases" to the corresponding CoreData types, e.g.: 55 | - ``ModelContainer`` == ``NSPersistentContainer`` 56 | - ``ModelContext`` == ``NSManagedObjectContext`` 57 | - ``Schema`` == ``NSManagedObjectModel`` 58 | - `Schema/Entity` == ``NSEntityDescription`` 59 | 60 | And so on. The CoreData type names can be used instead, but make a future 61 | migration to SwiftData harder. 62 | 63 | ### Is it possible to use ManagedModels in SwiftUI Previews? 64 | 65 | Yes! Attach an in-memory store to the preview-View, like so: 66 | ```swift 67 | #Preview { 68 | ContentView() 69 | .modelContainer(for: Item.self, inMemory: true) 70 | } 71 | ``` 72 | 73 | ### Something isn't working right, how do I file a Radar? 74 | 75 | Please file a GitHub 76 | [Issue](https://github.com/Data-swift/ManagedModels/issues). 77 | Thank you very much. 78 | -------------------------------------------------------------------------------- /Sources/ManagedModels/Documentation.docc/GettingStarted.md: -------------------------------------------------------------------------------- 1 | # Getting Started 2 | 3 | Setting up ManagedModels. 4 | 5 | ## Introduction 6 | 7 | This article shows how to setup a small SwiftUI Xcode project to work with 8 | ManagedModels. 9 | It is a conversion of the Xcode template project for CoreData. 10 | 11 | 12 | ## Creating the Xcode Project and Adding ManagedModels 13 | 14 | 1. Create a new SwiftUI project in Xcode, e.g. a Multiplatform/App project or an 15 | iOS/App project. (it does work for UIKit as well!) 16 | 2. Choose "None" as the "Storage" (instead of "SwiftData" or "Core Data") 17 | 3. Select "Add Package Dependencies" in Xcode's "File" menu to add the 18 | ManagedModels macro. 19 | 4. In the **search field** (yes!) of the packages panel, 20 | paste in the URL of the ManagedModels package: 21 | `https://github.com/Data-swift/ManagedModels.git`, 22 | and press "Add Package" twice. 23 | 24 | > At some point Xcode will stop compilation and ask you to confirm that you 25 | > want to use the `@Model` macro provided by ManagedModels 26 | > ("Target 'ManagedModelMacros' must be enabled before it can be used."). 27 | > Confirm to continue, or review the source code of the macro first. 28 | 29 | 30 | ## Create a Model 31 | 32 | Create a new file for the model, say `Item.swift` (or just paste the code in 33 | any other Swift file of the project). 34 | It could look like this: 35 | ```swift 36 | import ManagedModels 37 | 38 | @Model class Item: NSManagedObject { 39 | var timestamp : Date 40 | } 41 | ``` 42 | 43 | ## Configure the App to use the Container 44 | 45 | Use the `View/modelContainer` modifier provided by ManagedModels to setup 46 | the whole CoreData stack for the `Item` model: 47 | ```swift 48 | import SwiftUI 49 | import ManagedModels 50 | 51 | @main 52 | struct MyApp: App { 53 | var body: some Scene { 54 | WindowGroup { 55 | ContentView() 56 | } 57 | .modelContainer(for: Item.self, inMemory: true) 58 | } 59 | } 60 | ``` 61 | 62 | This is using `inMemory: true` during testing, i.e. a new in-memory database 63 | will be create on each restart of the app. 64 | Once the app is in better shape, this can be set to `false` (or the whole 65 | argument removed). 66 | 67 | 68 | ## Write a SwiftUI View that works w/ the Model 69 | 70 | Replace the default SwiftUI ContentView with this setup. 71 | The details are addressed below. 72 | 73 | ```swift 74 | import SwiftUI 75 | import ManagedModels 76 | 77 | struct ContentView: View { 78 | 79 | @Environment(\.modelContext) 80 | private var viewContext 81 | 82 | @FetchRequest(sort: \.timestamp, animation: .default) 83 | private var items: FetchedResults 84 | 85 | private func addItem() { 86 | withAnimation { 87 | let newItem = Item() 88 | newItem.timestamp = Date() 89 | viewContext.insert(newItem) 90 | } 91 | } 92 | 93 | var body: some View { 94 | NavigationStack { 95 | List { 96 | ForEach(items) { item in 97 | Text("\(item.timestamp, format: .dateTime)") 98 | } 99 | } 100 | .toolbar { 101 | Button(action: addItem) { Label("Add Item", systemImage: "plus") } 102 | } 103 | } 104 | } 105 | } 106 | 107 | #Preview { 108 | ContentView() 109 | .modelContainer(for: Item.self, inMemory: true) 110 | } 111 | ``` 112 | 113 | #### Access the ModelContext aka NSManagedObjectContext 114 | 115 | A ``ModelContext`` (``NSManagedObjectContext``) maintains the current state of 116 | the models. It is used to insert and delete models. 117 | A model object is always assigned to just one context. 118 | 119 | The context can be retrieved using the `@Environment` property wrapper: 120 | ```swift 121 | @Environment(\.modelContext) private var viewContext 122 | ``` 123 | And is then used in e.g. the `addItem` function to create a new model object: 124 | ```swift 125 | let newItem = Item() 126 | newItem.timestamp = Date() 127 | viewContext.insert(newItem) 128 | ``` 129 | One could also write a convenience initializer to streamline the process. 130 | 131 | #### Use the SwiftUI @FetchRequest to Fetch Models 132 | 133 | The 134 | [`@FetchRequest`](https://developer.apple.com/documentation/swiftui/fetchrequest) 135 | property wrapper is used to load models from CoreData: 136 | ```swift 137 | @FetchRequest(sort: \.timestamp, animation: .default) 138 | private var items: FetchedResults 139 | ``` 140 | It can sort and filter and do various other things as documented in the 141 | - [CoreData documentation](https://developer.apple.com/documentation/coredata). 142 | 143 | The value of the `items` can be directly used in SwiftUI 144 | [List](https://developer.apple.com/documentation/swiftui/list)'s: 145 | ```swift 146 | List { 147 | ForEach(items) { 148 | item in Text("\(item.timestamp)") 149 | } 150 | } 151 | ``` 152 | 153 | #### Test the View using SwiftUI Previews 154 | 155 | Using the `#Preview` macro the view can be directly tested in Xcode: 156 | ```swift 157 | #Preview { 158 | ContentView() 159 | .modelContainer(for: Item.self, inMemory: true) 160 | } 161 | ``` 162 | Note how the in-memory store is used again. In a real setup, one might want to 163 | pre-populate an in-memory store for testing. 164 | 165 | 166 | ## Full Example 167 | 168 | There is a small SwiftUI todo list example app, 169 | demonstrating the use of 170 | [ManagedModels](https://github.com/Data-swift/ManagedModels/). 171 | It has two connected entities and shows the general setup: 172 | [Managed ToDos](https://github.com/Data-swift/ManagedToDosApp/). 173 | 174 | It should be self-explanatory. Works on macOS 13+ and iOS 16+, due to the use 175 | of the new SwiftUI navigation views. 176 | Could be backported to earlier versions. 177 | -------------------------------------------------------------------------------- /Sources/ManagedModels/Documentation.docc/Links.md: -------------------------------------------------------------------------------- 1 | # Links related to CoreData & SwiftData 2 | 3 | A collection of links related to CoreData/SwiftData etc. 4 | 5 | 6 | ## Project Links 7 | 8 | Swift Package URL: `https://github.com/Data-swift/ManagedModels.git` 9 | 10 | - [ManagedModels](https://github.com/Data-swift/ManagedModels/) 11 | - filing [GitHub Issues](https://github.com/Data-swift/ManagedModels/issues) 12 | - [Managed ToDos](https://github.com/Data-swift/ManagedToDosApp/) example app 13 | - [Northwind for ManagedModels](https://github.com/Northwind-swift/NorthwindManagedModels) 14 | (more complex example, schema with many entities and a prefilled DB for 15 | testing) 16 | 17 | 18 | ## Apple 19 | 20 | - [CoreData](https://developer.apple.com/documentation/coredata) documentation 21 | - [SwiftData](https://developer.apple.com/documentation/swiftdata) documentation 22 | - [@Model](https://developer.apple.com/documentation/SwiftData/Model()) macro 23 | 24 | ### WWDC Sessions 25 | 26 | - WWDC 2023 27 | - [Meet SwiftData](https://developer.apple.com/videos/play/wwdc2023/10187), 28 | Session 10187 29 | - [Build an App with SwiftData](https://developer.apple.com/videos/play/wwdc2023/10154), 30 | Session 10154 31 | - [Model your Schema with SwiftData](https://developer.apple.com/videos/play/wwdc2023/10195), 32 | Session 10195 33 | 34 | ### Misc 35 | 36 | - [Enterprise Objects Framework](https://en.wikipedia.org/wiki/Enterprise_Objects_Framework) / 37 | aka EOF, NeXT's/Apple's old ORM CoreData is based on. 38 | - EOF [Developer Guide](https://developer.apple.com/library/archive/documentation/LegacyTechnologies/WebObjects/WebObjects_4.5/System/Documentation/Developer/EnterpriseObjects/DevGuide/EOFDevGuide.pdf) 39 | - Swift 40 | [Macros](https://docs.swift.org/swift-book/documentation/the-swift-programming-language/macros/) 41 | 42 | 43 | ## Related Technology 44 | 45 | - [Lighter.swift](https://github.com/Lighter-swift), typesafe and superfast 46 | [SQLite](https://www.sqlite.org) Swift tooling 47 | - [ZeeQL](http://zeeql.io). Prototype of an 48 | [EOF](https://en.wikipedia.org/wiki/Enterprise_Objects_Framework) for Swift, 49 | with many database backends 50 | -------------------------------------------------------------------------------- /Sources/ManagedModels/Documentation.docc/Who.md: -------------------------------------------------------------------------------- 1 | # Support 2 | 3 | Built by @helje5. 4 | 5 | ## ZeeZide 6 | 7 | ManagedModels is brought to you by 8 | [Helge Heß](https://github.com/helje5/) via [ZeeZide](https://zeezide.de). 9 | We like feedback, GitHub stars, cool contract work, 10 | presumably any form of praise you can think of. 11 | 12 | ## Supporting the Project 13 | 14 | If you want to support the project, consider buying a copy of the 15 | [“Code for SQLite3” application](https://apps.apple.com/us/app/code-for-sqlite3/id1638111010). 16 | Or any other, you don't have to use them! 😀 17 | 18 | Thank you! 19 | 20 | ## Support for ManagedModels 21 | 22 | Commercial support for ManagedModels is available from [ZeeZide](https://zeezide.de). 23 | Need help to apply that to your codebase, prepare the CoreData codebase for 24 | SwiftData. 25 | ZeeZide can help with that 26 | [info@zeezide.de](mailto:info@zeezide.de). 27 | 28 | Issues with the software can be filed against the public GitHub repository: 29 | [ManagedModels Issues](https://github.com/Data-swift/ManagedModels/issues). 30 | -------------------------------------------------------------------------------- /Sources/ManagedModels/Migration/MigrationStage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2023 ZeeZide GmbH. 4 | // 5 | 6 | import CoreData 7 | 8 | public enum MigrationStage { 9 | 10 | @available(*, unavailable, message: "Not yet implemented") 11 | case lightweight(fromVersion : VersionedSchema.Type, 12 | toVersion : VersionedSchema.Type) 13 | 14 | // TODO: This takes a context, check how this is supposed to work. 15 | @available(*, unavailable, message: "Not yet implemented") 16 | case custom(fromVersion : VersionedSchema.Type, 17 | toVersion : VersionedSchema.Type, 18 | willMigrate : (( NSManagedObjectContext ) throws -> Void)?, 19 | didMigrate : (( NSManagedObjectContext ) throws -> Void)?) 20 | } 21 | -------------------------------------------------------------------------------- /Sources/ManagedModels/Migration/SchemaMigrationPlan.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2023 ZeeZide GmbH. 4 | // 5 | 6 | public protocol SchemaMigrationPlan { 7 | 8 | static var schemas : [ VersionedSchema.Type ] { get } 9 | static var stages : [ MigrationStage ] { get } 10 | } 11 | -------------------------------------------------------------------------------- /Sources/ManagedModels/ModelMacroDefinition.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2023 ZeeZide GmbH. 4 | // 5 | 6 | import CoreData 7 | 8 | /** 9 | * Tag an `NSManagedObject` class property as an "Attribute" 10 | * (`NSAttributeDescription`, vs a `NSRelationshipDescription`). 11 | * 12 | * - Parameters: 13 | * - options: A set of attribute `Option`s, e.g. `.unique`, or none. 14 | * - originalName: The peer to CoreData's `renamingIdentifier`. 15 | * - hashModifier: The peer to CoreData's `versionHashModifier`. 16 | * - defaultValue: The default value for the property. 17 | */ 18 | @available(swift 5.9) 19 | @attached(peer) 20 | public macro Attribute( 21 | _ options: CoreData.NSAttributeDescription.Option..., 22 | originalName: String? = nil, 23 | hashModifier: String? = nil, 24 | defaultValue: Any? = nil 25 | ) = #externalMacro(module: "ManagedModelMacros", type: "AttributeMacro") 26 | 27 | 28 | /** 29 | * Tag an `NSManagedObject` class property as a "Relationship" 30 | * (`NSRelationshipDescription` vs a `NSAttributeDescription`). 31 | * 32 | * - Parameters: 33 | * - options: A set of relationship `Option`s. 34 | * - originalName: The peer to CoreData's `renamingIdentifier`. 35 | * - hashModifier: The peer to CoreData's `versionHashModifier`. 36 | */ 37 | @available(swift 5.9) 38 | @attached(peer) 39 | public macro Relationship( 40 | _ options: CoreData.NSRelationshipDescription.Option..., 41 | deleteRule: CoreData.NSRelationshipDescription.DeleteRule = .nullify, 42 | minimumModelCount: Int? = 0, maximumModelCount: Int? = 0, 43 | originalName: String? = nil, 44 | inverse: AnyKeyPath? = nil, 45 | hashModifier: String? = nil 46 | ) = #externalMacro(module: "ManagedModelMacros", type: "RelationshipMacro") 47 | 48 | 49 | /** 50 | * Tag an `NSManagedObject` class property as "transient". 51 | * 52 | * Transient properties are ignored for any persistence or other 53 | * `NSManagedObject` operations (it does *not* map to non-stored CoreData 54 | * properties!). 55 | * They just live as regular instance variables in the class. 56 | */ 57 | @available(swift 5.9) 58 | @attached(peer) 59 | public macro Transient() = 60 | #externalMacro(module: "ManagedModelMacros", type: "TransientMacro") 61 | 62 | /** 63 | * An internal helper macro. Don't use this. 64 | */ 65 | @available(swift 5.9) 66 | @attached(accessor) 67 | public macro _PersistedProperty() = 68 | #externalMacro(module: "ManagedModelMacros", type: "PersistedPropertyMacro") 69 | 70 | 71 | // MARK: - Model Macro 72 | 73 | // TBD: This needs an API `originalName` for migrations? 74 | /** 75 | * Tags a class as a ``PersistentModel``, i.e. an `NSManagedObject` that will 76 | * have an `NSEntityDescriptor` that is generated from the Swift code. 77 | * 78 | * This generates all the necessary helper structures to allow the class being 79 | * used in CoreData. 80 | */ 81 | @available(swift 5.9) 82 | @attached(member, names: // Those are the names we add 83 | named(init), // Initializers. 84 | named(schemaMetadata), // The metadata. 85 | named(_$originalName), 86 | named(_$hashModifier) 87 | ) 88 | @attached(memberAttribute) // attaches attributes (@NSManaged) to members 89 | @attached(extension, conformances: // the protocols we add automagically 90 | PersistentModel 91 | ) 92 | public macro Model( 93 | originalName: String? = nil, 94 | hashModifier: String? = nil 95 | ) = #externalMacro(module: "ManagedModelMacros", type: "ModelMacro") 96 | -------------------------------------------------------------------------------- /Sources/ManagedModels/PersistentModel/PersistentIdentifier.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2023-2024 ZeeZide GmbH. 4 | // 5 | 6 | import CoreData 7 | 8 | public typealias PersistentIdentifier = NSManagedObjectID 9 | 10 | #if compiler(>=6) 11 | extension NSManagedObjectID: @retroactive Identifiable, @retroactive Encodable { 12 | } 13 | #else 14 | extension NSManagedObjectID: Identifiable, Encodable {} 15 | #endif 16 | 17 | extension NSManagedObjectID { 18 | public typealias ID = NSManagedObjectID 19 | 20 | @inlinable 21 | public var id: Self { self } 22 | } 23 | 24 | extension NSManagedObjectID { 25 | 26 | @inlinable 27 | public func encode(to encoder: Encoder) throws { 28 | var container = encoder.singleValueContainer() 29 | try container.encode(self.uriRepresentation()) 30 | } 31 | } 32 | 33 | 34 | public extension NSManagedObjectID { 35 | 36 | @inlinable 37 | var entityName : String { 38 | entity.name ?? { 39 | assertionFailure( 40 | """ 41 | Entity has no name: \(entity), called proper designated initializer? 42 | 43 | If an own designated initializer is used, it still has to call into 44 | 45 | super.init(entity: Self.entity(), insertInto: nil) 46 | 47 | Otherwise CoreData won't be able to generate a proper key. 48 | """ 49 | ) 50 | let oid = ObjectIdentifier(entity) 51 | return "Entity<\(String(UInt(bitPattern: oid), radix: 16))>" 52 | }() 53 | } 54 | 55 | @inlinable 56 | var storeIdentifier : String? { 57 | isTemporaryID ? nil : uriRepresentation().absoluteString 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Sources/ManagedModels/PersistentModel/PersistentModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2023 ZeeZide GmbH. 4 | // 5 | 6 | import CoreData 7 | 8 | /** 9 | * An `NSManagedObject` that can construct its `NSEntityDescription` using the 10 | * `@Model` macro. 11 | */ 12 | public protocol PersistentModel: NSManagedObject, Hashable, Identifiable { 13 | 14 | /// The `NSManagedObjectContext` the model is inserted into. 15 | var modelContext : NSManagedObjectContext? { get } 16 | 17 | /** 18 | * Reflection data for the model. 19 | */ 20 | static var schemaMetadata : [ NSManagedObjectModel.PropertyMetadata ] { get } 21 | 22 | 23 | /// The `renamingIdentifier` of the model. 24 | static var _$originalName : String? { get } 25 | /// The `versionHashModifier` of the model. 26 | static var _$hashModifier : String? { get } 27 | } 28 | 29 | public extension PersistentModel { 30 | 31 | @inlinable 32 | var modelContext : NSManagedObjectContext? { managedObjectContext } 33 | 34 | /// The `NSManagedObjectID` of the model. 35 | @inlinable 36 | var persistentModelID : NSManagedObjectID { objectID } 37 | 38 | @inlinable 39 | var id : NSManagedObjectID { persistentModelID } 40 | } 41 | 42 | extension PersistentModel { 43 | 44 | @inlinable 45 | public static var schemaMetadata : [ NSManagedObjectModel.PropertyMetadata ] { 46 | fatalError("Subclass needs to implement `schemaMetadata`") 47 | } 48 | } 49 | 50 | public extension PersistentModel { 51 | 52 | @inlinable 53 | static func fetchRequest() -> NSFetchRequest { 54 | NSFetchRequest(entityName: _typeName(Self.self, qualified: false)) 55 | } 56 | 57 | @inlinable 58 | static func fetchRequest(filter : NSPredicate? = nil, 59 | sortBy keyPath : KeyPath, 60 | order: NSSortDescriptor.SortOrder = .forward, 61 | fetchOffset : Int? = nil, 62 | fetchLimit : Int? = nil) 63 | -> NSFetchRequest 64 | { 65 | let fetchRequest = Self.fetchRequest() 66 | fetchRequest.predicate = filter 67 | if let meta = Self.schemaMetadata.first(where: { $0.keypath == keyPath }) { 68 | fetchRequest.sortDescriptors = [ 69 | NSSortDescriptor(key: meta.name, ascending: order == .forward) 70 | ] 71 | } 72 | else { 73 | fetchRequest.sortDescriptors = [ 74 | NSSortDescriptor(keyPath: keyPath, ascending: order == .forward) 75 | ] 76 | } 77 | if let fetchOffset { fetchRequest.fetchOffset = fetchOffset } 78 | if let fetchLimit { fetchRequest.fetchLimit = fetchLimit } 79 | return fetchRequest 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /Sources/ManagedModels/PersistentModel/PropertyMetadata.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2023 ZeeZide GmbH. 4 | // 5 | 6 | import CoreData 7 | 8 | public extension NSManagedObjectModel { 9 | 10 | /** 11 | * Metadata for a property of a ``NSManagedObjectModel`` object. 12 | * 13 | * All (code defined) properties of the ``NSManagedObjectModel`` are stored 14 | * in the `schemaMetadata` static property. 15 | */ 16 | struct PropertyMetadata: @unchecked Sendable { 17 | 18 | /// The name of the property instance variable, e.g. `street`. 19 | public let name : String 20 | 21 | /// A Swift keypath that can be used to access the instance variable, 22 | /// e.g. `\Address.street`. 23 | public let keypath : AnyKeyPath 24 | 25 | /// The default value associated with the property, e.g. `""`. 26 | public let defaultValue : Any? 27 | 28 | /** 29 | * Either a ``NSAttributeDescription`` or a ``NSRelationshipDescription`` 30 | * object (or nil if the user didn't specify an `@Attribute` or 31 | * `@Relationship` macro). 32 | * Note: This is never modified, it is treated as a template and gets 33 | * copied when the `NSEntityDescription` is built. 34 | */ 35 | public let metadata : NSPropertyDescription? 36 | 37 | /** 38 | * Create a new ``PropertyMetadata`` value. 39 | * 40 | * - Parameters: 41 | * - name: name of the property instance variable, e.g. `street`. 42 | * - keypath: KeyPath to access the related instance variable. 43 | * - defaultValue: The properties default value, if available. 44 | * - metadata: Either nothing, or a template Attribute/Relationship. 45 | */ 46 | public init(name: String, keypath: AnyKeyPath, 47 | defaultValue: Any? = nil, 48 | metadata: NSPropertyDescription? = nil) 49 | { 50 | self.name = name 51 | self.keypath = keypath 52 | self.defaultValue = defaultValue 53 | self.metadata = metadata 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Sources/ManagedModels/PersistentModel/RelationshipCollection.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2023 ZeeZide GmbH. 4 | // 5 | 6 | import Foundation 7 | 8 | /** 9 | * An optional or non-optional relationship collection. 10 | */ 11 | public protocol RelationshipCollection { 12 | 13 | associatedtype PersistentElement: PersistentModel 14 | 15 | init(coreDataAnySet: Set?) 16 | var coreDataAnySet : Set? { get } 17 | } 18 | 19 | #if false // see Utilities, doesn't fly yet. 20 | extension OrderedSet: RelationshipCollection where Element: PersistentModel { 21 | public typealias PersistentElement = Element 22 | } 23 | #endif 24 | 25 | extension Set: RelationshipCollection where Element: PersistentModel { 26 | public typealias PersistentElement = Element 27 | 28 | @inlinable 29 | public init(coreDataAnySet: Set?) { 30 | assert(coreDataAnySet == nil || coreDataAnySet is Self, "Type mismatch.") 31 | self = coreDataAnySet as? Self ?? Set() 32 | } 33 | @inlinable 34 | public var coreDataAnySet : Set? { self } 35 | } 36 | 37 | extension Array: RelationshipCollection where Element: PersistentModel { 38 | public typealias PersistentElement = Element 39 | 40 | @inlinable 41 | public init(coreDataAnySet: Set?) { 42 | assert(coreDataAnySet == nil || coreDataAnySet is Set, 43 | "Type mismatch.") 44 | self.init(coreDataAnySet as! Set) 45 | } 46 | @inlinable 47 | public var coreDataAnySet : Set? { Set(self) } 48 | } 49 | 50 | // Note: This is not any optional, it is an optional collection! (toMany) 51 | extension Optional: RelationshipCollection 52 | where Wrapped: Sequence & RelationshipCollection, Wrapped.Element: PersistentModel 53 | { 54 | public typealias PersistentElement = Wrapped.Element 55 | 56 | @inlinable 57 | public init(coreDataAnySet: Set?) { 58 | if let coreDataAnySet { 59 | self = .some(.init(coreDataAnySet: coreDataAnySet)) 60 | } 61 | else { 62 | self = .none 63 | } 64 | } 65 | @inlinable 66 | public var coreDataAnySet : Set? { 67 | switch self { 68 | case .none: return nil 69 | case .some(let value): return value.coreDataAnySet 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Sources/ManagedModels/ReExports.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2023 ZeeZide GmbH. 4 | // 5 | 6 | @_exported import CoreData 7 | -------------------------------------------------------------------------------- /Sources/ManagedModels/Schema/Attribute.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2023 ZeeZide GmbH. 4 | // 5 | 6 | import CoreData 7 | 8 | public extension Schema { 9 | 10 | typealias Attribute = CoreData.NSAttributeDescription 11 | } 12 | -------------------------------------------------------------------------------- /Sources/ManagedModels/Schema/AttributeOption.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2023 ZeeZide GmbH. 4 | // 5 | 6 | import class Foundation.ValueTransformer 7 | 8 | extension NSAttributeDescription { 9 | 10 | public struct Option: Equatable, Sendable { 11 | 12 | let value : Value 13 | 14 | public static let unique = Self(value: .unique) 15 | public static let ephemeral = Self(value: .ephemeral) 16 | public static let spotlight = Self(value: .spotlight) 17 | 18 | /// Use a Foundation `ValueTransformer`. 19 | public static func transformable(by transformerType: ValueTransformer.Type) 20 | -> Self 21 | { 22 | Self(value: .transformableByType(transformerType)) 23 | } 24 | public static func transformable(by transformerName: String) -> Self { 25 | Self(value: .transformableByName(transformerName)) 26 | } 27 | 28 | public static let externalStorage = Self(value: .externalStorage) 29 | 30 | @available(iOS 15.0, macOS 12.0, *) 31 | public static let allowsCloudEncryption = 32 | Self(value: .allowsCloudEncryption) 33 | 34 | public static let preserveValueOnDeletion = 35 | Self(value: .preserveValueOnDeletion) 36 | } 37 | 38 | } 39 | 40 | extension NSAttributeDescription.Option { 41 | 42 | enum Value: Sendable { 43 | case unique, externalStorage, preserveValueOnDeletion, ephemeral, spotlight 44 | case transformableByType(ValueTransformer.Type) 45 | case transformableByName(String) 46 | 47 | @available(iOS 15.0, macOS 12.0, *) 48 | case allowsCloudEncryption 49 | } 50 | } 51 | 52 | extension NSAttributeDescription.Option.Value: Equatable { 53 | 54 | public static func == (lhs: Self, rhs: Self) -> Bool { 55 | switch ( lhs, rhs ) { 56 | case ( .unique , .unique ): return true 57 | case ( .externalStorage , .externalStorage ): return true 58 | case ( .ephemeral , .ephemeral ): return true 59 | case ( .spotlight , .spotlight ): return true 60 | case ( .transformableByType(let lhs), .transformableByType(let rhs) ): 61 | return lhs == rhs 62 | case ( .transformableByName(let lhs), .transformableByName(let rhs) ): 63 | return lhs == rhs 64 | case ( .allowsCloudEncryption , .allowsCloudEncryption ): 65 | return true 66 | case ( .preserveValueOnDeletion, .preserveValueOnDeletion ): 67 | return true 68 | default: return false 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /Sources/ManagedModels/Schema/Entity.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2023 ZeeZide GmbH. 4 | // 5 | 6 | import CoreData 7 | 8 | public extension Schema { 9 | typealias Entity = CoreData.NSEntityDescription 10 | } 11 | -------------------------------------------------------------------------------- /Sources/ManagedModels/Schema/Relationship.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2023 ZeeZide GmbH. 4 | // 5 | 6 | import CoreData 7 | 8 | public extension Schema { 9 | 10 | typealias Relationship = CoreData.NSRelationshipDescription 11 | } 12 | -------------------------------------------------------------------------------- /Sources/ManagedModels/Schema/RelationshipOption.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2023 ZeeZide GmbH. 4 | // 5 | 6 | import CoreData 7 | 8 | public extension NSRelationshipDescription { 9 | 10 | struct Option: Equatable, Sendable { 11 | 12 | enum Value: Sendable { 13 | case unique 14 | } 15 | let value : Value 16 | 17 | /// Only one record may point to the target of this relationship. 18 | public static let unique = Self(value: .unique) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Sources/ManagedModels/Schema/RelationshipTargetType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2023 ZeeZide GmbH. 4 | // 5 | 6 | /** 7 | * Reflects and categorizes the various property types, given a target type. 8 | */ 9 | enum RelationshipTargetType { 10 | 11 | case attribute(Any.Type) 12 | 13 | case toOne (modelType: any PersistentModel.Type, optional: Bool) 14 | 15 | case toMany(collectionType: any RelationshipCollection.Type, 16 | modelType: any PersistentModel.Type) 17 | 18 | case toOrderedSet(optional: Bool) 19 | 20 | init(_ type: Any.Type) { 21 | if let relType = type as? any RelationshipCollection.Type { 22 | func modelType(in collection: P.Type) 23 | -> any PersistentModel.Type 24 | { 25 | return collection.PersistentElement.self 26 | } 27 | 28 | self = .toMany(collectionType: relType, 29 | modelType: modelType(in: relType)) 30 | } 31 | else if let modelType = type as? any PersistentModel.Type { 32 | self = .toOne(modelType: modelType, optional: false) 33 | } 34 | else if let anyType = type as? any AnyOptional.Type, 35 | let modelType = anyType.wrappedType as? any PersistentModel.Type 36 | { 37 | self = .toOne(modelType: modelType, optional: true) 38 | } 39 | else if type is NSOrderedSet.Type { 40 | self = .toOrderedSet(optional: false) 41 | } 42 | else if type is Optional.Type { 43 | self = .toOrderedSet(optional: true) 44 | } 45 | else { 46 | self = .attribute(type) 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Sources/ManagedModels/Schema/Schema.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2023 ZeeZide GmbH. 4 | // 5 | 6 | import CoreData 7 | 8 | public typealias Schema = NSManagedObjectModel 9 | -------------------------------------------------------------------------------- /Sources/ManagedModels/Schema/SchemaProperty.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2023 ZeeZide GmbH. 4 | // 5 | 6 | public protocol SchemaProperty : NSPropertyDescription, Hashable { 7 | 8 | var name : String { get set } 9 | var originalName : String { get set } 10 | 11 | var valueType : Any.Type { get set } 12 | 13 | var isAttribute : Bool { get } 14 | var isRelationship : Bool { get } 15 | var isTransient : Bool { get } 16 | var isOptional : Bool { get } 17 | 18 | var isUnique : Bool { get } 19 | } 20 | 21 | public extension SchemaProperty { 22 | 23 | @inlinable 24 | var originalName: String { 25 | get { renamingIdentifier ?? "" } 26 | set { renamingIdentifier = newValue } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Sources/ManagedModels/Schema/SchemaVersion.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2023 ZeeZide GmbH. 4 | // 5 | 6 | public extension Schema { 7 | 8 | struct Version: Codable, Hashable { 9 | 10 | public let major: Int 11 | public let minor: Int 12 | public let patch: Int 13 | 14 | public init(_ major: Int, _ minor: Int, _ patch: Int) { 15 | self.major = major 16 | self.minor = minor 17 | self.patch = patch 18 | } 19 | } 20 | } 21 | 22 | extension Schema.Version: CustomStringConvertible { 23 | 24 | @inlinable 25 | public var description: String { "\(major).\(minor).\(patch)" } 26 | } 27 | 28 | extension Schema.Version: Comparable { 29 | 30 | @inlinable 31 | public static func < (lhs: Self, rhs: Self) -> Bool { 32 | if lhs.major != rhs.major { return lhs.major < rhs.major } 33 | if lhs.minor != rhs.minor { return lhs.minor < rhs.minor } 34 | if lhs.patch != rhs.patch { return lhs.patch < rhs.patch } 35 | return true // TBD 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Sources/ManagedModels/Schema/VersionedSchema.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2023 ZeeZide GmbH. 4 | // 5 | 6 | import CoreData 7 | 8 | public protocol VersionedSchema { 9 | 10 | static var models : [ any PersistentModel.Type ] { get } 11 | static var versionIdentifier : NSManagedObjectModel.Version { get } 12 | } 13 | 14 | public extension VersionedSchema { 15 | 16 | /** 17 | * Returns a cached managed object model for the given schema. 18 | */ 19 | static var managedObjectModel : NSManagedObjectModel { .model(for: models) } 20 | } 21 | -------------------------------------------------------------------------------- /Sources/ManagedModels/SchemaCompatibility/CodableTransformer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2023 ZeeZide GmbH. 4 | // 5 | 6 | import Foundation 7 | import CoreData 8 | 9 | final class CodableTransformer: ValueTransformer { 10 | 11 | #if false 12 | override class func transformedValueClass() -> AnyClass { 13 | T.self // doesn't work 14 | } 15 | #endif 16 | override class func allowsReverseTransformation() -> Bool { true } 17 | 18 | override func transformedValue(_ value: Any?) -> Any? { 19 | // value is the box 20 | guard let value else { return nil } 21 | guard let typed = value as? T else { 22 | assertionFailure("Value to be transformed is not the right type? \(value)") 23 | return nil 24 | } 25 | do { 26 | return try JSONEncoder().encode(typed) 27 | } 28 | catch { 29 | assertionFailure("Could not encode JSON value of property? \(error)") 30 | return nil 31 | } 32 | } 33 | 34 | override func reverseTransformedValue(_ value: Any?) -> Any? { 35 | guard let value else { return nil } 36 | guard let data = value as? Data else { 37 | assert(value is Data, "Reverse value is not `Data`?") 38 | return nil 39 | } 40 | do { 41 | return try JSONDecoder().decode(T.self, from: data) 42 | } 43 | catch { 44 | assertionFailure("Could not decode JSON value of property? \(error)") 45 | return nil 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Sources/ManagedModels/SchemaCompatibility/CoreDataPrimitiveValue.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2023 ZeeZide GmbH. 4 | // 5 | 6 | import CoreData 7 | 8 | public extension CoreData.NSAttributeDescription { 9 | struct TypeConfiguration: Sendable { 10 | let attributeType : NSAttributeType 11 | let isOptional : Bool 12 | let attributeValueClassName : String? 13 | } 14 | } 15 | 16 | /** 17 | * Implemented by types that can be directly converted to the primitive values 18 | * CoreData supports. 19 | */ 20 | public protocol CoreDataPrimitiveValue { 21 | 22 | static var coreDataValue : NSAttributeDescription.TypeConfiguration { get } 23 | } 24 | 25 | extension Optional: CoreDataPrimitiveValue 26 | where Wrapped: CoreDataPrimitiveValue 27 | { 28 | public static var coreDataValue : NSAttributeDescription.TypeConfiguration { 29 | .init(attributeType: Wrapped.coreDataValue.attributeType, 30 | isOptional: true, 31 | attributeValueClassName: Wrapped.coreDataValue.attributeValueClassName 32 | ) 33 | } 34 | } 35 | 36 | extension Int: CoreDataPrimitiveValue { 37 | public static let coreDataValue = NSAttributeDescription.TypeConfiguration( 38 | attributeType : .integer64AttributeType, 39 | isOptional : false, 40 | attributeValueClassName : nil 41 | ) 42 | } 43 | extension Int16: CoreDataPrimitiveValue { 44 | public static let coreDataValue = NSAttributeDescription.TypeConfiguration( 45 | attributeType : .integer16AttributeType, 46 | isOptional : false, 47 | attributeValueClassName : nil 48 | ) 49 | } 50 | extension Int32: CoreDataPrimitiveValue { 51 | public static let coreDataValue = NSAttributeDescription.TypeConfiguration( 52 | attributeType : .integer32AttributeType, 53 | isOptional : false, 54 | attributeValueClassName : nil 55 | ) 56 | } 57 | extension Int64: CoreDataPrimitiveValue { 58 | public static let coreDataValue = NSAttributeDescription.TypeConfiguration( 59 | attributeType : .integer64AttributeType, 60 | isOptional : false, 61 | attributeValueClassName : nil 62 | ) 63 | } 64 | extension Int8: CoreDataPrimitiveValue { 65 | public static let coreDataValue = NSAttributeDescription.TypeConfiguration( 66 | attributeType : .integer16AttributeType, 67 | isOptional : false, 68 | attributeValueClassName : nil 69 | ) 70 | } 71 | 72 | extension UInt: CoreDataPrimitiveValue { // edgy 73 | public static let coreDataValue = Int64.coreDataValue 74 | } 75 | extension UInt64: CoreDataPrimitiveValue { // edgy 76 | public static let coreDataValue = Int64.coreDataValue 77 | } 78 | 79 | extension UInt32: CoreDataPrimitiveValue { 80 | public static let coreDataValue = Int64.coreDataValue 81 | } 82 | extension UInt16: CoreDataPrimitiveValue { 83 | public static let coreDataValue = Int32.coreDataValue 84 | } 85 | extension UInt8: CoreDataPrimitiveValue { 86 | public static let coreDataValue = Int16.coreDataValue 87 | } 88 | 89 | 90 | extension String: CoreDataPrimitiveValue { 91 | public static let coreDataValue = NSAttributeDescription.TypeConfiguration( 92 | attributeType : .stringAttributeType, 93 | isOptional : false, 94 | attributeValueClassName : nil 95 | ) 96 | } 97 | 98 | extension Bool: CoreDataPrimitiveValue { 99 | public static let coreDataValue = NSAttributeDescription.TypeConfiguration( 100 | attributeType : .booleanAttributeType, 101 | isOptional : false, 102 | attributeValueClassName : nil 103 | ) 104 | } 105 | 106 | extension Double: CoreDataPrimitiveValue { 107 | public static let coreDataValue = NSAttributeDescription.TypeConfiguration( 108 | attributeType : .doubleAttributeType, 109 | isOptional : false, 110 | attributeValueClassName : nil 111 | ) 112 | } 113 | extension Float: CoreDataPrimitiveValue { 114 | public static let coreDataValue = NSAttributeDescription.TypeConfiguration( 115 | attributeType : .floatAttributeType, 116 | isOptional : false, 117 | attributeValueClassName : nil 118 | ) 119 | } 120 | 121 | // MARK: - Foundation 122 | 123 | extension Date: CoreDataPrimitiveValue { 124 | public static let coreDataValue = NSAttributeDescription.TypeConfiguration( 125 | attributeType : .dateAttributeType, 126 | isOptional : false, 127 | attributeValueClassName : nil 128 | ) 129 | } 130 | 131 | extension Data: CoreDataPrimitiveValue { 132 | public static let coreDataValue = NSAttributeDescription.TypeConfiguration( 133 | attributeType : .binaryDataAttributeType, 134 | isOptional : false, 135 | attributeValueClassName : nil 136 | ) 137 | } 138 | 139 | extension Decimal: CoreDataPrimitiveValue { 140 | public static let coreDataValue = NSAttributeDescription.TypeConfiguration( 141 | attributeType : .decimalAttributeType, 142 | isOptional : false, 143 | attributeValueClassName : nil 144 | ) 145 | } 146 | 147 | extension UUID: CoreDataPrimitiveValue { 148 | public static let coreDataValue = NSAttributeDescription.TypeConfiguration( 149 | attributeType : .UUIDAttributeType, 150 | isOptional : false, 151 | attributeValueClassName : nil 152 | ) 153 | } 154 | 155 | extension URL: CoreDataPrimitiveValue { 156 | public static let coreDataValue = NSAttributeDescription.TypeConfiguration( 157 | attributeType : .URIAttributeType, 158 | isOptional : false, 159 | attributeValueClassName : nil 160 | ) 161 | } 162 | -------------------------------------------------------------------------------- /Sources/ManagedModels/SchemaCompatibility/NSAttributeDescription+Data.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2023 ZeeZide GmbH. 4 | // 5 | 6 | import CoreData 7 | 8 | extension CoreData.NSAttributeDescription { 9 | 10 | func internalCopy() -> Self { 11 | guard let copy = self.copy() as? Self else { 12 | fatalError("Could not copy attribute \(self)") 13 | } 14 | assert(copy !== self, "Copy didn't produce a copy?") 15 | 16 | // Ensure copy of unique marker 17 | if isUnique { copy.isUnique = true } 18 | return copy 19 | } 20 | } 21 | 22 | @available(iOS 11.0, *) // could backport further 23 | extension CoreData.NSAttributeDescription: SchemaProperty { 24 | 25 | public var valueType: Any.Type { 26 | // TBD: we might actually want to hold on to the type in an assoc prop! 27 | // Though I'm not sure we actually need it. Maybe we should always convert 28 | // down to the CoreData base type for _this_ particular property. 29 | // Its primary use is when the entity builder sets the type from the macro 30 | // during construction. 31 | get { 32 | if let baseType = attributeType.swiftBaseType(isOptional: isOptional) { 33 | return baseType 34 | } 35 | guard let attributeValueClassName else { return isOptional ? Any?.self : Any.self } 36 | return NSClassFromString(attributeValueClassName) ?? (isOptional ? Any?.self : Any.self) 37 | } 38 | set { 39 | // Note: This needs to match up w/ PersistentModel+KVC. 40 | 41 | if let primitiveType = newValue as? CoreDataPrimitiveValue.Type { 42 | let config = primitiveType.coreDataValue 43 | self.attributeType = config.attributeType 44 | self.isOptional = config.isOptional 45 | if let newClassName = config.attributeValueClassName { 46 | self.attributeValueClassName = newClassName 47 | } 48 | return 49 | } 50 | 51 | // This requires iOS 16: 52 | // RawRepresentable 53 | if let rawType = newValue as? any RawRepresentable.Type { 54 | func setIt(for type: T.Type) -> Bool { 55 | let rawType = type.RawValue.self 56 | if let primitiveType = rawType as? CoreDataPrimitiveValue.Type { 57 | let config = primitiveType.coreDataValue 58 | self.attributeType = config.attributeType 59 | self.isOptional = config.isOptional 60 | if let newClassName = config.attributeValueClassName { 61 | self.attributeValueClassName = newClassName 62 | } 63 | return true 64 | } 65 | else { 66 | return false 67 | } 68 | } 69 | if setIt(for: rawType) { return } 70 | } 71 | 72 | if let codableType = newValue as? any Codable.Type { 73 | // TBD: Someone tell me whether this is sensible. 74 | self.attributeType = .transformableAttributeType 75 | self.isOptional = newValue is any AnyOptional.Type 76 | 77 | func setValueClassName(for type: T.Type) { 78 | #if false // doesn't work 79 | self.attributeValueClassName = NSStringFromClass(T.self) 80 | #endif 81 | 82 | let name = NSStringFromClass(CodableTransformer.self) 83 | if !ValueTransformer.valueTransformerNames().contains(.init(name)) { 84 | // no access to valueTransformerForName? 85 | let transformer = CodableTransformer() 86 | ValueTransformer 87 | .setValueTransformer(transformer, forName: .init(name)) 88 | } 89 | assert(ValueTransformer.valueTransformerNames().contains(.init(name))) 90 | valueTransformerName = name 91 | assert(valueTransformerName != nil) 92 | } 93 | setValueClassName(for: codableType) 94 | return 95 | } 96 | 97 | if let valueTransformerName = valueTransformerName { 98 | self.attributeType = .transformableAttributeType 99 | self.isOptional = newValue is any AnyOptional.Type 100 | 101 | self.attributeValueClassName = NSStringFromClass(NSObject.self) 102 | assert(ValueTransformer.valueTransformerNames().contains(.init(valueTransformerName))) 103 | return 104 | } 105 | 106 | // TBD: 107 | // undefinedAttributeType = 0 108 | // transformableAttributeType = 1800 109 | // objectIDAttributeType = 2000 110 | // compositeAttributeType = 2100 111 | assertionFailure("Unsupported Attribute value type \(newValue)") 112 | } 113 | } 114 | 115 | @inlinable public var isAttribute : Bool { return true } 116 | @inlinable public var isRelationship : Bool { return false } 117 | 118 | public var options : [ Option ] { 119 | // TBD: 120 | // - ephemeral (stored in entity?!) 121 | var options = [ Option ]() 122 | if preservesValueInHistoryOnDeletion { 123 | options.append(.preserveValueOnDeletion) 124 | } 125 | if allowsExternalBinaryDataStorage { options.append(.externalStorage) } 126 | if isIndexedBySpotlight { options.append(.spotlight) } 127 | if isUnique { options.append(.unique) } 128 | if isTransient { options.append(.ephemeral) } 129 | 130 | if let valueTransformerName, !valueTransformerName.isEmpty { 131 | options.append(.transformable(by: valueTransformerName)) 132 | } 133 | 134 | if #available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) { 135 | if allowsCloudEncryption { options.append(.allowsCloudEncryption) } 136 | } 137 | return options 138 | } 139 | } 140 | 141 | 142 | // MARK: - Initializers 143 | 144 | public extension NSAttributeDescription { 145 | 146 | // SwiftData has this to match the `@Attribute` macro. We can pass in some 147 | // more data. 148 | convenience init(_ options: Option..., originalName: String? = nil, 149 | hashModifier: String? = nil) 150 | { 151 | self.init() 152 | if let originalName { renamingIdentifier = originalName } 153 | if let hashModifier { versionHashModifier = hashModifier } 154 | setOptions(options) 155 | } 156 | 157 | // The ManagedModels version to match the `@Attribute` macro that can receive 158 | // a few more datapoints. 159 | convenience init(_ options: Option..., originalName: String? = nil, 160 | hashModifier: String? = nil, 161 | defaultValue: Any? = nil, 162 | name: String, valueType: Any.Type) 163 | { 164 | self.init(name: name, originalName: originalName, options: options, 165 | valueType: valueType, defaultValue: defaultValue, 166 | hashModifier: hashModifier) 167 | } 168 | 169 | convenience init(name: String, originalName: String? = nil, 170 | options: [ Option ] = [], valueType: Any.Type, 171 | defaultValue: Any? = nil, hashModifier: String? = nil) 172 | { 173 | self.init() 174 | if !name.isEmpty { self.name = name } 175 | if let originalName { renamingIdentifier = originalName } 176 | if let hashModifier { versionHashModifier = hashModifier } 177 | if let defaultValue { self.defaultValue = defaultValue } 178 | isOptional = valueType is any AnyOptional.Type 179 | 180 | assert(valueTransformerName == nil) 181 | valueTransformerName = nil 182 | setOptions(options) 183 | if valueType != Any.self { self.valueType = valueType } 184 | } 185 | } 186 | 187 | private extension NSAttributeDescription { 188 | 189 | func setOptions(_ options: [ Option ]) { 190 | preservesValueInHistoryOnDeletion = false 191 | allowsExternalBinaryDataStorage = false 192 | isIndexedBySpotlight = false 193 | isTransient = false 194 | if #available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) { 195 | allowsCloudEncryption = false 196 | } 197 | 198 | for option in options { 199 | switch option.value { 200 | case .unique: 201 | isUnique = true 202 | 203 | case .preserveValueOnDeletion: preservesValueInHistoryOnDeletion = true 204 | case .externalStorage: allowsExternalBinaryDataStorage = true 205 | case .spotlight: isIndexedBySpotlight = true 206 | case .ephemeral: isTransient = true 207 | 208 | case .transformableByName(let name): 209 | assert(valueTransformerName == nil) 210 | attributeType = .transformableAttributeType 211 | valueTransformerName = name 212 | if !ValueTransformer.valueTransformerNames().contains(.init(name)) { 213 | print("WARNING: Named transformer is not registered: \(name)", 214 | "in attribute:", self) 215 | } 216 | case .transformableByType(let type): 217 | let name = NSStringFromClass(type) 218 | if !ValueTransformer.valueTransformerNames().contains(.init(name)) { 219 | // no access to valueTransformerForName? 220 | let transformer = type.init() 221 | ValueTransformer 222 | .setValueTransformer(transformer, forName: .init(name)) 223 | } 224 | valueTransformerName = name 225 | attributeType = .transformableAttributeType 226 | 227 | case .allowsCloudEncryption: // FIXME: restrict availability 228 | if #available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) { 229 | allowsCloudEncryption = true 230 | } 231 | else { 232 | fatalError("Cloud encryption not supported!") 233 | } 234 | } 235 | } 236 | } 237 | } 238 | -------------------------------------------------------------------------------- /Sources/ManagedModels/SchemaCompatibility/NSAttributeType+Data.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2023 ZeeZide GmbH. 4 | // 5 | 6 | import CoreData 7 | 8 | extension NSAttributeType { 9 | 10 | func swiftBaseType(isOptional: Bool) -> Any.Type? { 11 | switch self { // FIXME: return Int for 32bit 12 | case .integer16AttributeType : 13 | if isOptional { return Int16? .self } else { return Int16 .self } 14 | case .integer32AttributeType : 15 | if isOptional { return Int32? .self } else { return Int32 .self } 16 | case .integer64AttributeType : 17 | if isOptional { return Int? .self } else { return Int .self } 18 | case .decimalAttributeType : 19 | if isOptional { return Decimal?.self } else { return Decimal.self } 20 | case .doubleAttributeType : 21 | if isOptional { return Double? .self } else { return Double.self } 22 | case .floatAttributeType : 23 | if isOptional { return Float? .self } else { return Float .self } 24 | case .stringAttributeType : 25 | if isOptional { return String? .self } else { return String.self } 26 | case .booleanAttributeType : 27 | if isOptional { return Bool? .self } else { return Bool .self } 28 | case .dateAttributeType : 29 | if isOptional { return Date? .self } else { return Date .self } 30 | case .binaryDataAttributeType : 31 | if isOptional { return Data? .self } else { return Data .self } 32 | case .UUIDAttributeType : 33 | if isOptional { return UUID? .self } else { return UUID .self } 34 | case .URIAttributeType : 35 | if isOptional { return URL?.self } else { return URL.self } 36 | case .undefinedAttributeType, .transformableAttributeType, 37 | .objectIDAttributeType: 38 | return nil 39 | default: // for composite 40 | return nil 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Sources/ManagedModels/SchemaCompatibility/NSDeleteRule+Data.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2023 ZeeZide GmbH. 4 | // 5 | 6 | import CoreData 7 | 8 | public extension NSDeleteRule { 9 | static let noAction = Self.noActionDeleteRule 10 | static let nullify = Self.nullifyDeleteRule 11 | static let cascade = Self.cascadeDeleteRule 12 | static let deny = Self.denyDeleteRule 13 | } 14 | -------------------------------------------------------------------------------- /Sources/ManagedModels/SchemaCompatibility/NSEntityDescription+Data.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2023 ZeeZide GmbH. 4 | // 5 | 6 | import CoreData 7 | 8 | public extension CoreData.NSEntityDescription { 9 | // Note: `uniquenessConstraints` is String only in SwiftData 10 | 11 | @inlinable 12 | var storedProperties : [ NSPropertyDescription ] { 13 | properties.filter { !$0.isTransient } 14 | } 15 | @inlinable 16 | var storedPropertiesByName : [ String : NSPropertyDescription ] { 17 | propertiesByName.filter { !$0.value.isTransient } 18 | } 19 | 20 | @inlinable 21 | var attributes : [ NSAttributeDescription ] { 22 | properties.compactMap { $0 as? NSAttributeDescription } 23 | } 24 | @inlinable 25 | var relationships : [ NSRelationshipDescription ] { 26 | properties.compactMap { $0 as? NSRelationshipDescription } 27 | } 28 | 29 | @inlinable 30 | var superentityName : String? { superentity?.name } 31 | 32 | // TBD: Not sure this is how it works (i.e. whether inherited are part of 33 | // the props or not). 34 | 35 | @inlinable 36 | var inheritedProperties : [ NSPropertyDescription ] { 37 | guard let superentity else { return [] } 38 | return superentity.inheritedProperties + superentity.properties 39 | } 40 | 41 | @inlinable 42 | var inheritedPropertiesByName : [ String : NSPropertyDescription ] { 43 | guard let superentity else { return [:] } 44 | if superentity.inheritedPropertiesByName.isEmpty { 45 | return superentity.propertiesByName 46 | } 47 | else { 48 | var copy = superentity.inheritedPropertiesByName 49 | for ( key, value ) in superentity.propertiesByName { 50 | copy[key] = value 51 | } 52 | return copy 53 | } 54 | } 55 | 56 | @inlinable 57 | var _objectType : (any PersistentModel.Type)? { 58 | set { managedObjectClassName = newValue.flatMap { NSStringFromClass($0) } } 59 | get { 60 | guard let managedObjectClassName, 61 | let clazz = NSClassFromString(managedObjectClassName), 62 | let model = clazz as? NSManagedObject.Type else 63 | { 64 | return nil 65 | } 66 | return model as? any PersistentModel.Type 67 | } 68 | } 69 | 70 | @inlinable 71 | var _mangledName : String? { 72 | set { managedObjectClassName = newValue } 73 | get { managedObjectClassName } 74 | } 75 | } 76 | 77 | 78 | // MARK: - Convenience Initializers 79 | 80 | public extension NSEntityDescription { 81 | 82 | convenience init(_ name: String) { 83 | self.init() 84 | self.name = name 85 | } 86 | 87 | convenience init(_ name: String, subentities: NSEntityDescription..., 88 | properties: NSPropertyDescription...) 89 | { 90 | self.init(name) 91 | self.subentities = subentities 92 | addProperties(properties) 93 | } 94 | convenience init(_ name: String, properties: NSPropertyDescription...) { 95 | self.init(name) 96 | addProperties(properties) 97 | } 98 | } 99 | 100 | 101 | // MARK: - Internal Helpers 102 | 103 | extension NSEntityDescription { 104 | 105 | func addProperties(_ newProperties: [ NSPropertyDescription ]) { 106 | for newProperty in newProperties { 107 | properties.append(newProperty) 108 | } 109 | } 110 | } 111 | 112 | extension NSEntityDescription { 113 | 114 | func isPropertyUnique(_ property: NSPropertyDescription) -> Bool { 115 | self.uniquenessConstraints.contains(where: { propSet in 116 | // one or more NSAttributeDescription or NSString instances 117 | for prop in propSet { 118 | if let propName = prop as? String { 119 | if propName == name { return true } 120 | } 121 | else if let propAttr = prop as? NSPropertyDescription { 122 | return propAttr === self || propAttr.name == name 123 | } 124 | } 125 | return false 126 | }) 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /Sources/ManagedModels/SchemaCompatibility/NSManagedObjectModel+Data.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2023-2024 ZeeZide GmbH. 4 | // 5 | 6 | import CoreData 7 | 8 | public extension NSManagedObjectModel { 9 | // TBD: 10 | // - schemaEncodingVersion 11 | // - encodingVersion 12 | // - version 13 | 14 | @inlinable 15 | convenience init(_ entities: NSEntityDescription..., 16 | version: Schema.Version = Version(1, 0, 0)) 17 | { 18 | self.init() 19 | self.entities = entities 20 | } 21 | 22 | convenience init(_ types: [ any PersistentModel.Type ], 23 | version: Schema.Version = Version(1, 0, 0)) 24 | { 25 | self.init() 26 | self.entities = SchemaBuilder().lookupAllEntities(for: types) 27 | } 28 | 29 | @inlinable 30 | convenience init(versionedSchema: any VersionedSchema.Type) { 31 | self.init(versionedSchema.models, 32 | version: versionedSchema.versionIdentifier) 33 | } 34 | } 35 | 36 | 37 | // MARK: - Cached ManagedObjectModels 38 | 39 | private let lock = NSLock() // TODO: Use new UnfairLock if available 40 | #if swift(>=5.10) 41 | nonisolated(unsafe) 42 | private var map = [ Set : NSManagedObjectModel ]() 43 | nonisolated(unsafe) private let sharedBuilder = SchemaBuilder() 44 | #else // 5.9: nonisolated(unsafe) not available, nonisolated nor working on var 45 | private var map = [ Set : NSManagedObjectModel ]() 46 | nonisolated private let sharedBuilder = SchemaBuilder() 47 | #endif 48 | 49 | public extension NSManagedObjectModel { 50 | 51 | /** 52 | * This caches a model for the types in the given `VersionedSchema`. 53 | * I.e. it will return the same `NSManagedObjectModel` when given the same 54 | * types. 55 | * 56 | * - Parameters: 57 | * - versionedSchema: The versioned schema to derive the model from. 58 | * - Returns: A `NSManagedObjectModel` representing the schema. 59 | */ 60 | @inlinable 61 | static func model(for versionedSchema: VersionedSchema.Type) 62 | -> NSManagedObjectModel 63 | { 64 | model(for: versionedSchema.models) 65 | } 66 | 67 | /** 68 | * This caches a model for the types passed in. 69 | * I.e. it will return the same `NSManagedObjectModel` when given the same 70 | * types. 71 | * 72 | * - Parameters: 73 | * - types: A set of `PersistentModel` types, e.g. `[Person.self]`. 74 | * - Returns: A `NSManagedObjectModel` representing the types. 75 | */ 76 | static func model(for types: [ any PersistentModel.Type ]) 77 | -> NSManagedObjectModel 78 | { 79 | // The idea here is that CD is sensitive w/ creating multiple models for the 80 | // same entities/classes. May be true or not, but prefer this when possible. 81 | // The entities are cached anyways in the shared generator object. 82 | var typeIDs = Set() 83 | func addID(_ type: M.Type) { 84 | typeIDs.insert(ObjectIdentifier(M.self)) 85 | } 86 | for anyType in types { 87 | addID(anyType) 88 | } 89 | 90 | lock.lock() 91 | let cachedMOM = map[typeIDs] 92 | let mom : NSManagedObjectModel 93 | if let cachedMOM { mom = cachedMOM } 94 | else { 95 | mom = NSManagedObjectModel() 96 | mom.entities = sharedBuilder.lookupAllEntities(for: types) 97 | map[typeIDs] = mom 98 | } 99 | lock.unlock() 100 | return mom 101 | } 102 | } 103 | 104 | 105 | // MARK: - Test Helpers 106 | 107 | internal extension NSManagedObjectModel { 108 | 109 | /// Initializer for testing purposes. 110 | convenience init(_ types: [ any PersistentModel.Type ], 111 | version: Schema.Version = Version(1, 0, 0), 112 | schemaCache: SchemaBuilder) 113 | { 114 | self.init() 115 | self.entities = schemaCache.lookupAllEntities(for: types) 116 | } 117 | 118 | /// Initializer for testing purposes. 119 | convenience init(versionedSchema: any VersionedSchema.Type, 120 | schemaCache: SchemaBuilder) 121 | { 122 | self.init(versionedSchema.models, 123 | version: versionedSchema.versionIdentifier, 124 | schemaCache: schemaCache) 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /Sources/ManagedModels/SchemaCompatibility/NSPropertyDescription+Data.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2023 ZeeZide GmbH. 4 | // 5 | 6 | extension NSPropertyDescription { 7 | private struct AssociatedKeys { 8 | #if swift(>=5.10) 9 | nonisolated(unsafe) static var propertyIsUniqueAssociatedKey: Void? = nil 10 | #else // 5.9: nonisolated(unsafe) not available 11 | static var propertyIsUniqueAssociatedKey: Void? = nil 12 | #endif 13 | } 14 | 15 | public internal(set) var isUnique: Bool { 16 | // Note: isUnique is only used during schema construction! 17 | set { 18 | if newValue { 19 | objc_setAssociatedObject( 20 | self, &AssociatedKeys.propertyIsUniqueAssociatedKey, 21 | type(of: self), // Just used as a flag, type won't go away 22 | .OBJC_ASSOCIATION_ASSIGN 23 | ) 24 | } 25 | else { 26 | objc_setAssociatedObject( 27 | self, &AssociatedKeys.propertyIsUniqueAssociatedKey, 28 | nil, // clear // clear flag 29 | .OBJC_ASSOCIATION_ASSIGN 30 | ) 31 | } 32 | #if false // do we need this? The entity might not yet be setup? 33 | guard !entity.isPropertyUnique(self) else { return } 34 | entity.uniquenessConstraints.append( [ self ]) 35 | #endif 36 | } 37 | get { 38 | objc_getAssociatedObject( 39 | self, 40 | &AssociatedKeys.propertyIsUniqueAssociatedKey 41 | ) != nil 42 | ? true 43 | : entity.isPropertyUnique(self) 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Sources/ManagedModels/SchemaCompatibility/NSRelationshipDescription+Data.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2023 ZeeZide GmbH. 4 | // 5 | 6 | import CoreData 7 | 8 | extension CoreData.NSRelationshipDescription { 9 | 10 | public typealias DeleteRule = NSDeleteRule 11 | } 12 | 13 | extension CoreData.NSRelationshipDescription: SchemaProperty {} 14 | 15 | public extension CoreData.NSRelationshipDescription { 16 | 17 | @inlinable var isToOneRelationship : Bool { !isToMany } 18 | 19 | @inlinable var isAttribute : Bool { return false } 20 | @inlinable var isRelationship : Bool { return true } 21 | 22 | @inlinable var options : [ Option ] { isUnique ? [ .unique ] : [] } 23 | 24 | var keypath : AnyKeyPath? { 25 | set { writableRelationshipInfo.keypath = newValue } 26 | get { relationshipInfo?.keypath } 27 | } 28 | 29 | var inverseKeyPath : AnyKeyPath? { 30 | set { writableRelationshipInfo.inverseKeyPath = newValue } 31 | get { relationshipInfo?.inverseKeyPath } 32 | } 33 | 34 | var valueType : Any.Type { 35 | set { 36 | if newValue == Any.self && relationshipInfo == nil { return } 37 | writableRelationshipInfo.valueType = newValue 38 | } 39 | get { relationshipInfo?.valueType ?? Any.self } 40 | } 41 | 42 | var inverseName : String? { 43 | set { 44 | if let inverseName = newValue { 45 | writableRelationshipInfo.inverseName = inverseName 46 | } 47 | else { 48 | relationshipInfo?.inverseName = nil 49 | } 50 | } 51 | get { 52 | relationshipInfo?.inverseName ?? inverseRelationship?.name 53 | } 54 | } 55 | 56 | var destination : String { 57 | set { 58 | if !newValue.isEmpty { 59 | writableRelationshipInfo.destination = newValue 60 | } 61 | else { 62 | relationshipInfo?.destination = nil 63 | } 64 | } 65 | get { 66 | relationshipInfo?.destination ?? destinationEntity?.name ?? "" 67 | } 68 | } 69 | } 70 | 71 | 72 | extension CoreData.NSRelationshipDescription { 73 | 74 | /** 75 | * Returns the ``PersistentModel`` type targeted by the relationship, 76 | * based on the ``valueType`` property. 77 | */ 78 | var modelType: (any PersistentModel.Type)? { 79 | if relationshipInfo?.valueType == nil { 80 | if let destinationEntity = destinationEntity { 81 | if let type = destinationEntity._objectType { 82 | return type 83 | } 84 | } 85 | assertionFailure( 86 | "Could not determine model type of relationship: \(self)?") 87 | return nil 88 | } 89 | 90 | // TBD: If that's too expensive, we could cache it? 91 | switch RelationshipTargetType(valueType) { 92 | case .attribute(_): 93 | assertionFailure("Detected relationship type as an attribute? \(self)") 94 | return nil 95 | case .toOne(modelType: let modelType, optional: _): 96 | return modelType 97 | case .toMany(collectionType: _, modelType: let modelType): 98 | return modelType 99 | case .toOrderedSet(optional: _): 100 | assertionFailure( 101 | "Attempt to get the `modelType` of an NSOrderedSet relship. \(self)" 102 | ) 103 | return nil 104 | } 105 | } 106 | } 107 | 108 | 109 | // MARK: - Initializer 110 | 111 | public extension CoreData.NSRelationshipDescription { 112 | 113 | // Note: This matches what the `Relationship` macro takes. 114 | convenience init(_ options: Option..., deleteRule: NSDeleteRule = .nullify, 115 | minimumModelCount: Int? = 0, maximumModelCount: Int? = 0, 116 | originalName: String? = nil, inverse: AnyKeyPath? = nil, 117 | hashModifier: String? = nil, // TBD 118 | name: String? = nil, valueType: Any.Type = Any.self) 119 | { 120 | // Note The original doesn't take a name, because it is supposed to match 121 | // the `@Relationship` macro. That's also why we order those last :-) 122 | precondition(minimumModelCount ?? 0 >= 0) 123 | precondition(maximumModelCount ?? 0 >= 0) 124 | self.init() 125 | 126 | self.name = name ?? "" 127 | self.valueType = valueType 128 | self.renamingIdentifier = originalName ?? "" 129 | self.versionHashModifier = hashModifier 130 | self.deleteRule = deleteRule 131 | self.inverseKeyPath = inverse 132 | 133 | if options.contains(.unique) { isUnique = true } 134 | 135 | if let minimumModelCount { self.minCount = minimumModelCount } 136 | if let maximumModelCount { 137 | self.maxCount = maximumModelCount 138 | } 139 | else { 140 | if valueType is any RelationshipCollection.Type { 141 | self.maxCount = 0 142 | } 143 | else if valueType is NSOrderedSet.Type || 144 | valueType is Optional.Type 145 | { 146 | self.maxCount = 0 147 | } 148 | else { 149 | self.maxCount = 1 // the toOne marker! 150 | } 151 | } 152 | } 153 | } 154 | 155 | 156 | // MARK: - Storage 157 | 158 | extension CoreData.NSRelationshipDescription { 159 | 160 | func internalCopy() -> Self { 161 | guard let copy = self.copy() as? Self else { 162 | fatalError("Could not copy relationship \(self)") 163 | } 164 | assert(copy !== self, "Copy didn't produce a copy?") 165 | 166 | // Ensure copy of unique marker 167 | if isUnique { copy.isUnique = true } 168 | // Ensure copy of extra info 169 | if let relationshipInfo { 170 | copy.relationshipInfo = relationshipInfo.internalCopy() 171 | } 172 | return copy 173 | } 174 | } 175 | 176 | extension CoreData.NSRelationshipDescription { 177 | // Information that will get lost after serialization or regular copying. 178 | // Which should be fine, we only need it for the active, declared schema, 179 | // and that will have those things. 180 | 181 | final class MacroInfo: NSObject { 182 | var keypath : AnyKeyPath? 183 | var inverseKeyPath : AnyKeyPath? 184 | var valueType : Any.Type? 185 | var inverseName : String? 186 | var destination : String? 187 | var isToOneRelationship : Bool? 188 | 189 | override func copy() -> Any { internalCopy() } 190 | 191 | func internalCopy() -> MacroInfo { 192 | let copy = MacroInfo() 193 | copy.keypath = keypath 194 | copy.inverseKeyPath = inverseKeyPath 195 | copy.valueType = valueType 196 | copy.inverseName = inverseName 197 | copy.destination = destination 198 | copy.isToOneRelationship = isToOneRelationship 199 | return copy 200 | } 201 | } 202 | 203 | private struct AssociatedKeys { 204 | #if swift(>=5.10) 205 | nonisolated(unsafe) static var relationshipInfoAssociatedKey: Void? = nil 206 | #else // 5.9: nonisolated(unsafe) not available 207 | static var relationshipInfoAssociatedKey: Void? = nil 208 | #endif 209 | } 210 | 211 | var writableRelationshipInfo : MacroInfo { 212 | if let info = objc_getAssociatedObject( 213 | self, &AssociatedKeys.relationshipInfoAssociatedKey) as? MacroInfo 214 | { 215 | return info 216 | } 217 | 218 | let info = MacroInfo() 219 | self.relationshipInfo = info 220 | return info 221 | } 222 | 223 | var relationshipInfo: MacroInfo? { 224 | // Note: isUnique is only used during schema construction! 225 | set { 226 | objc_setAssociatedObject( 227 | self, &AssociatedKeys.relationshipInfoAssociatedKey, 228 | newValue, .OBJC_ASSOCIATION_RETAIN 229 | ) 230 | } 231 | get { 232 | objc_getAssociatedObject( 233 | self, &AssociatedKeys.relationshipInfoAssociatedKey 234 | ) as? MacroInfo 235 | } 236 | } 237 | } 238 | -------------------------------------------------------------------------------- /Sources/ManagedModels/SchemaGeneration/NSEntityDescription+Generation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2023 ZeeZide GmbH. 4 | // 5 | 6 | import CoreData 7 | 8 | extension NSEntityDescription { 9 | 10 | /** 11 | * Create a new ``Schema/Entity`` given a ``PersistentModel`` type. 12 | * 13 | * This only fills the data that is local to the model, i.e. it doesn't 14 | * traverse into destination types for relationships and such. 15 | * 16 | * This is not a general purpose builder, use ``SchemaBuilder`` instead. 17 | */ 18 | convenience init(_ type: M.Type) 19 | where M: NSManagedObject & PersistentModel 20 | { 21 | self.init() 22 | self.name = _typeName(M.self, qualified: false) 23 | managedObjectClassName = NSStringFromClass(type) 24 | 25 | if let s = M._$originalName { renamingIdentifier = s } 26 | if let s = M._$hashModifier { versionHashModifier = s } 27 | 28 | for propMeta in M.schemaMetadata { 29 | let property = processProperty(propMeta) 30 | 31 | assert(attributesByName [property.name] == nil && 32 | relationshipsByName[property.name] == nil) 33 | properties.append(property) 34 | 35 | if property.isUnique && !self.isPropertyUnique(property) { 36 | uniquenessConstraints.append([ property ]) 37 | } 38 | } 39 | } 40 | 41 | // MARK: - Properties 42 | 43 | private typealias PropertyMetadata = NSManagedObjectModel.PropertyMetadata 44 | 45 | private func processProperty(_ meta: PropertyMetadata) 46 | -> NSPropertyDescription 47 | { 48 | // Check whether a pre-filled property has been set. 49 | if let templateProperty = meta.metadata { 50 | return processProperty(meta, template: templateProperty) 51 | } 52 | else { 53 | return createProperty(meta) 54 | } 55 | } 56 | 57 | private func processProperty

(_ propMeta: PropertyMetadata, template: P) 58 | -> NSPropertyDescription 59 | where P: NSPropertyDescription 60 | { 61 | let targetType = type(of: propMeta.keypath).valueType 62 | 63 | // Note that we make a copy of the objects, they might be used in 64 | // different setups/configs. 65 | 66 | if let template = template as? NSAttributeDescription { 67 | let attribute = template.internalCopy() 68 | fixup(attribute, targetType: targetType, meta: propMeta) 69 | return attribute 70 | } 71 | 72 | if let template = template as? NSRelationshipDescription { 73 | let relationship = template.internalCopy() 74 | switch RelationshipTargetType(targetType) { 75 | case .attribute(_): 76 | // TBD: Rather throw? 77 | assertionFailure("Relationship target type is not an object?") 78 | fixup(relationship, targetType: targetType, isToOne: true, 79 | meta: propMeta) 80 | return relationship 81 | 82 | case .toOne(modelType: _, optional: _): 83 | fixup(relationship, targetType: targetType, isToOne: true, 84 | meta: propMeta) 85 | return relationship 86 | 87 | case .toMany(collectionType: _, modelType: _): 88 | fixup(relationship, targetType: targetType, isToOne: false, 89 | meta: propMeta) 90 | return relationship 91 | 92 | case .toOrderedSet(optional: _): 93 | fixupOrderedSet(relationship, meta: propMeta) 94 | return relationship 95 | } 96 | } 97 | 98 | // TBD: Rather throw? 99 | print("Unexpected property metadata object:", template) 100 | assertionFailure("Unexpected property metadata object: \(template)") 101 | return createProperty(propMeta) 102 | } 103 | 104 | private func createProperty(_ propMeta: PropertyMetadata) 105 | -> NSPropertyDescription 106 | { 107 | let valueType = type(of: propMeta.keypath).valueType 108 | 109 | // Need to reflect to decide what the keypath is pointing too. 110 | switch RelationshipTargetType(valueType) { 111 | 112 | case .attribute(_): 113 | let attribute = CoreData.NSAttributeDescription( 114 | name: propMeta.name, 115 | valueType: valueType, 116 | defaultValue: propMeta.defaultValue 117 | ) 118 | fixup(attribute, targetType: valueType, meta: propMeta) 119 | return attribute 120 | 121 | case .toOne(modelType: _, optional: let isOptional): 122 | let relationship = CoreData.NSRelationshipDescription() 123 | relationship.minCount = isOptional ? 1 : 0 124 | relationship.maxCount = 1 // the toOne marker! 125 | relationship.isOptional = isOptional 126 | relationship.valueType = valueType 127 | fixup(relationship, targetType: valueType, isToOne: true, 128 | meta: propMeta) 129 | return relationship 130 | 131 | case .toMany(collectionType: _, modelType: _): 132 | let relationship = CoreData.NSRelationshipDescription() 133 | relationship.valueType = valueType 134 | relationship.isOptional = valueType is any AnyOptional.Type 135 | fixup(relationship, targetType: valueType, isToOne: false, 136 | meta: propMeta) 137 | assert(relationship.maxCount != 1) 138 | return relationship 139 | 140 | case .toOrderedSet(optional: let isOptional): 141 | let relationship = CoreData.NSRelationshipDescription() 142 | relationship.valueType = valueType 143 | relationship.isOptional = isOptional 144 | relationship.isOrdered = true 145 | fixupOrderedSet(relationship, meta: propMeta) 146 | assert(relationship.maxCount != 1) 147 | return relationship 148 | } 149 | } 150 | 151 | 152 | // MARK: - Fixups 153 | // Those `fixup` functions take potentially half-filled objects and add 154 | // in the extra values from the metadata. 155 | 156 | private func fixup(_ attribute: NSAttributeDescription, targetType: Any.Type, 157 | meta: PropertyMetadata) 158 | { 159 | if attribute.name.isEmpty { attribute.name = meta.name } 160 | if attribute.valueType == Any.self { 161 | attribute.valueType = targetType 162 | attribute.isOptional = targetType is any AnyOptional 163 | } 164 | if attribute.defaultValue == nil, let metaDefault = meta.defaultValue { 165 | attribute.defaultValue = metaDefault 166 | } 167 | } 168 | 169 | private func fixup(_ relationship: NSRelationshipDescription, 170 | targetType: Any.Type, 171 | isToOne: Bool, 172 | meta: PropertyMetadata) 173 | { 174 | 175 | // TBD: Rather throw? 176 | if relationship.name.isEmpty { relationship.name = meta.name } 177 | 178 | if isToOne { 179 | relationship.maxCount = 1 // toOne marker! 180 | } 181 | else { 182 | // Note: In SwiftData arrays are not ordered. 183 | relationship.isOrdered = targetType is NSOrderedSet.Type 184 | assert(relationship.maxCount != 1, "toMany w/ maxCount 1?") 185 | } 186 | 187 | if relationship.keypath == nil { relationship.keypath = meta.keypath } 188 | if relationship.valueType == Any.self { 189 | relationship.valueType = targetType 190 | } 191 | if relationship.valueType != Any.self { 192 | relationship.isOptional = relationship.valueType is any AnyOptional.Type 193 | if !isToOne { 194 | relationship.isOrdered = relationship.valueType is NSOrderedSet.Type 195 | } 196 | } 197 | } 198 | 199 | private func fixupOrderedSet(_ relationship: NSRelationshipDescription, 200 | meta: PropertyMetadata) 201 | { 202 | 203 | // TBD: Rather throw? 204 | assert(meta.defaultValue == nil, "Relationship w/ default value?") 205 | if relationship.name.isEmpty { relationship.name = meta.name } 206 | relationship.isOrdered = true 207 | 208 | if relationship.keypath == nil { relationship.keypath = meta.keypath } 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /Sources/ManagedModels/SchemaGeneration/NSRelationshipDescription+Inverse.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2023 ZeeZide GmbH. 4 | // 5 | 6 | import CoreData 7 | 8 | extension CoreData.NSRelationshipDescription { 9 | 10 | func setInverseRelationship(_ newInverseRelationship: 11 | NSRelationshipDescription) 12 | { 13 | assert(self.inverseRelationship == nil 14 | || self.inverseRelationship === newInverseRelationship) 15 | 16 | // A regular non-Model relationship 17 | 18 | if self.inverseRelationship == nil || 19 | self.inverseRelationship !== newInverseRelationship 20 | { 21 | self.inverseRelationship = newInverseRelationship 22 | } 23 | // get only in baseclass: inverseRelationship.inverseName 24 | if inverseName == nil || inverseName != newInverseRelationship.name { 25 | inverseName = newInverseRelationship.name 26 | } 27 | if destinationEntity == nil || 28 | destinationEntity != newInverseRelationship.entity 29 | { 30 | destinationEntity = newInverseRelationship.entity 31 | } 32 | if newInverseRelationship.destinationEntity == nil || 33 | newInverseRelationship.destinationEntity != entity 34 | { 35 | newInverseRelationship.destinationEntity = entity 36 | } 37 | 38 | if newInverseRelationship.inverseRelationship == nil || 39 | newInverseRelationship.inverseRelationship !== self 40 | { 41 | newInverseRelationship.inverseRelationship = self 42 | } 43 | 44 | // Extra model stuff 45 | 46 | assert(inverseKeyPath == nil 47 | || inverseKeyPath == newInverseRelationship.keypath) 48 | assert(inverseName == nil || inverseName == newInverseRelationship.name) 49 | 50 | if inverseKeyPath == nil || 51 | inverseKeyPath != newInverseRelationship.keypath 52 | { 53 | inverseKeyPath = newInverseRelationship.keypath 54 | } 55 | if inverseName == nil || inverseName != newInverseRelationship.name { 56 | inverseName = newInverseRelationship.name 57 | } 58 | 59 | if newInverseRelationship.inverseKeyPath == nil || 60 | newInverseRelationship.inverseKeyPath != keypath 61 | { 62 | newInverseRelationship.inverseKeyPath = keypath 63 | } 64 | if newInverseRelationship.inverseName == nil || 65 | newInverseRelationship.inverseName != name 66 | { 67 | // also fill inverse if not set 68 | newInverseRelationship.inverseName = name 69 | } 70 | 71 | assert(keypath == newInverseRelationship.inverseKeyPath) 72 | assert(name == newInverseRelationship.inverseName) 73 | assert(inverseKeyPath == newInverseRelationship.keypath) 74 | assert(inverseName == newInverseRelationship.name) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /Sources/ManagedModels/SwiftUI/FetchRequest+Extras.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2023 ZeeZide GmbH. 4 | // 5 | #if canImport(SwiftUI) 6 | 7 | import SwiftUI 8 | import CoreData 9 | 10 | @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) 11 | public extension FetchRequest { 12 | 13 | struct SortDescriptor { 14 | public let keyPath : PartialKeyPath 15 | public let order : NSSortDescriptor.SortOrder 16 | 17 | @inlinable 18 | public init(_ keyPath: KeyPath, 19 | order: NSSortDescriptor.SortOrder = .forward) 20 | { 21 | self.keyPath = keyPath 22 | self.order = order 23 | } 24 | } 25 | 26 | @MainActor 27 | @inlinable 28 | init(filter predicate : NSPredicate? = nil, 29 | sort keyPath : KeyPath, 30 | order : NSSortDescriptor.SortOrder = .forward, 31 | animation : Animation? = nil) 32 | where Result: PersistentModel & NSManagedObject & NSFetchRequestResult 33 | { 34 | guard let meta = Result 35 | .schemaMetadata.first(where: { $0.keypath == keyPath }) else 36 | { 37 | fatalError("Could not map keypath to persisted property?") 38 | } 39 | self.init( 40 | sortDescriptors: [ 41 | NSSortDescriptor(key: meta.name, ascending: order == .forward) 42 | ], 43 | predicate: predicate, 44 | animation: animation 45 | ) 46 | } 47 | 48 | @MainActor 49 | init(filter predicate : NSPredicate? = nil, 50 | sort sortDescriptors : [ SortDescriptor ], 51 | animation : Animation? = nil) 52 | where Result: PersistentModel & NSManagedObject & NSFetchRequestResult 53 | { 54 | self.init( 55 | sortDescriptors: sortDescriptors.map { sd in 56 | guard let meta = Result 57 | .schemaMetadata.first(where: { $0.keypath == sd.keyPath }) else 58 | { 59 | fatalError("Could not map keypath to persisted property?") 60 | } 61 | return NSSortDescriptor(key: meta.name, ascending: sd.order == .forward) 62 | }, 63 | predicate: predicate, 64 | animation: animation 65 | ) 66 | } 67 | 68 | @MainActor 69 | init(filter predicate : NSPredicate? = nil, 70 | sort sortDescriptors : [ NSSortDescriptor ], 71 | animation : Animation? = nil) 72 | where Result: PersistentModel & NSManagedObject & NSFetchRequestResult 73 | { 74 | self.init( 75 | sortDescriptors: sortDescriptors, 76 | predicate: predicate, 77 | animation: animation 78 | ) 79 | } 80 | } 81 | #endif // canImport(SwiftUI) 82 | -------------------------------------------------------------------------------- /Sources/ManagedModels/SwiftUI/ModelContainer+SwiftUI.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2023 ZeeZide GmbH. 4 | // 5 | #if canImport(SwiftUI) 6 | 7 | import SwiftUI 8 | import CoreData 9 | 10 | // TBD: also on Scene! 11 | 12 | public extension EnvironmentValues { 13 | 14 | @inlinable 15 | var modelContainer : NSManagedObjectContext { 16 | set { self.managedObjectContext = newValue } 17 | get { self.managedObjectContext } 18 | } 19 | } 20 | 21 | public extension View { 22 | 23 | @inlinable 24 | func modelContainer(_ container: ModelContainer) -> some View { 25 | self.modelContext(container.viewContext) 26 | } 27 | 28 | @MainActor 29 | @ViewBuilder 30 | func modelContainer( 31 | for modelTypes : [ any PersistentModel.Type ], 32 | inMemory : Bool = false, 33 | isAutosaveEnabled : Bool = true, 34 | isUndoEnabled : Bool = false, 35 | onSetup: @escaping (Result) -> Void = { _ in } 36 | ) -> some View 37 | { 38 | let result = makeModelContainer( 39 | for: modelTypes, inMemory: inMemory, 40 | isAutosaveEnabled: isAutosaveEnabled, isUndoEnabled: isUndoEnabled, 41 | onSetup: onSetup 42 | ) 43 | 44 | switch result { 45 | case .success(let container): 46 | self.modelContainer(container) 47 | case .failure: 48 | self // TBD. Could also overlay an error or sth 49 | } 50 | } 51 | 52 | @MainActor 53 | @inlinable 54 | func modelContainer( 55 | for modelType : any PersistentModel.Type, 56 | inMemory : Bool = false, 57 | isAutosaveEnabled : Bool = true, 58 | isUndoEnabled : Bool = false, 59 | onSetup: @escaping (Result) -> Void = { _ in } 60 | ) -> some View 61 | { 62 | self.modelContainer( 63 | for: [ modelType ], inMemory: inMemory, 64 | isAutosaveEnabled: isAutosaveEnabled, isUndoEnabled: isUndoEnabled, 65 | onSetup: onSetup 66 | ) 67 | } 68 | } 69 | 70 | @available(iOS 14.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) 71 | public extension Scene { 72 | 73 | @inlinable 74 | func modelContainer(_ container: ModelContainer) -> some Scene { 75 | self.modelContext(container.viewContext) 76 | } 77 | 78 | @MainActor 79 | @SceneBuilder 80 | func modelContainer( 81 | for modelTypes : [ any PersistentModel.Type ], 82 | inMemory : Bool = false, 83 | isAutosaveEnabled : Bool = true, 84 | isUndoEnabled : Bool = false, 85 | onSetup: @escaping (Result) -> Void = { _ in } 86 | ) -> some Scene 87 | { 88 | let result = makeModelContainer( 89 | for: modelTypes, inMemory: inMemory, 90 | isAutosaveEnabled: isAutosaveEnabled, isUndoEnabled: isUndoEnabled, 91 | onSetup: onSetup 92 | ) 93 | 94 | // So a SceneBuilder doesn't have a conditional. Can only crash on fail? 95 | self.modelContainer(try! result.get()) 96 | } 97 | 98 | @MainActor 99 | @inlinable 100 | func modelContainer( 101 | for modelType : any PersistentModel.Type, 102 | inMemory : Bool = false, 103 | isAutosaveEnabled : Bool = true, 104 | isUndoEnabled : Bool = false, 105 | onSetup: @escaping (Result) -> Void = { _ in } 106 | ) -> some Scene 107 | { 108 | self.modelContainer( 109 | for: [ modelType ], inMemory: inMemory, 110 | isAutosaveEnabled: isAutosaveEnabled, isUndoEnabled: isUndoEnabled, 111 | onSetup: onSetup 112 | ) 113 | } 114 | } 115 | 116 | 117 | // MARK: - Primitive 118 | 119 | // Note: The docs say that a container is only ever created once! So cache it. 120 | @MainActor 121 | private var modelToContainer = [ ObjectIdentifier: NSPersistentContainer ]() 122 | 123 | @MainActor 124 | private func makeModelContainer( 125 | for modelTypes : [ any PersistentModel.Type ], 126 | inMemory : Bool = false, 127 | isAutosaveEnabled : Bool = true, 128 | isUndoEnabled : Bool = false, 129 | onSetup: @escaping (Result) -> Void = { _ in } 130 | ) -> Result 131 | { 132 | let model = NSManagedObjectModel.model(for: modelTypes) // caches! 133 | if let container = modelToContainer[ObjectIdentifier(model)] { 134 | return .success(container) 135 | } 136 | 137 | let result = _makeModelContainer( 138 | for: model, 139 | configuration: ModelConfiguration(isStoredInMemoryOnly: inMemory) 140 | ) 141 | switch result { 142 | case .success(let container): 143 | modelToContainer[ObjectIdentifier(model)] = container // cache 144 | case .failure(_): break 145 | } 146 | 147 | onSetup(result) // TBD: this could be delayed for async contexts? 148 | return result 149 | } 150 | 151 | /// Return a `Result` for a container with the given configuration. 152 | private func _makeModelContainer( 153 | for model : NSManagedObjectModel, 154 | configuration: ModelConfiguration 155 | ) -> Result 156 | { 157 | let result : Result 158 | do { 159 | let container = try NSPersistentContainer( 160 | for: model, 161 | configurations: configuration 162 | // TBD: maybe pass in onSetup for async containers 163 | ) 164 | result = .success(container) 165 | } 166 | catch { 167 | result = .failure(error) 168 | } 169 | return result 170 | } 171 | 172 | #endif // canImport(SwiftUI) 173 | -------------------------------------------------------------------------------- /Sources/ManagedModels/SwiftUI/ModelContext+SwiftUI.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2023 ZeeZide GmbH. 4 | // 5 | #if canImport(SwiftUI) 6 | import SwiftUI 7 | import CoreData 8 | 9 | public extension EnvironmentValues { 10 | 11 | @inlinable 12 | var modelContext : NSManagedObjectContext { 13 | set { self.managedObjectContext = newValue } 14 | get { self.managedObjectContext } 15 | } 16 | } 17 | 18 | public extension View { 19 | 20 | @inlinable 21 | func modelContext(_ context: NSManagedObjectContext) -> some View { 22 | self 23 | .environment(\.managedObjectContext, context) 24 | } 25 | } 26 | 27 | @available(iOS 14.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) 28 | public extension Scene { 29 | 30 | @inlinable 31 | func modelContext(_ context: NSManagedObjectContext) -> some Scene { 32 | self 33 | .environment(\.managedObjectContext, context) 34 | } 35 | } 36 | #endif // canImport(SwiftUI) 37 | -------------------------------------------------------------------------------- /Sources/ManagedModels/Utilities/AnyOptional.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2023 ZeeZide GmbH. 4 | // 5 | 6 | /** 7 | * An internal type eraser for the `Optional` enum. 8 | * 9 | * Note that `Optional` is not a protocol, so `any Optional` doesn't fly. 10 | * 11 | * To check whether a type is an optional type: 12 | * ```swift 13 | * Int.self is any AnyOptional.Type // false 14 | * Int?.self is any AnyOptional.Type // true 15 | * ``` 16 | */ 17 | public protocol AnyOptional { 18 | 19 | associatedtype Wrapped 20 | 21 | /// Returns `true` if the optional has a value attached, i.e. is not `nil` 22 | var isSome : Bool { get } 23 | 24 | /// Returns the attached value as an `Any`, or `nil`. 25 | var value : Any? { get } 26 | 27 | /// Returns the dynamic type of the `Wrapped` value of the optional. 28 | static var wrappedType : Any.Type { get } 29 | 30 | static var noneValue : Self { get } 31 | } 32 | 33 | extension Optional : AnyOptional { 34 | 35 | @inlinable 36 | public static var noneValue : Self { .none } 37 | 38 | @inlinable 39 | public var isSome : Bool { 40 | switch self { 41 | case .none: return false 42 | case .some: return true 43 | } 44 | } 45 | 46 | @inlinable 47 | public var value : Any? { 48 | switch self { 49 | case .none: nil 50 | case .some(let unwrapped): unwrapped 51 | } 52 | } 53 | 54 | @inlinable 55 | public static var wrappedType : Any.Type { Wrapped.self } 56 | } 57 | -------------------------------------------------------------------------------- /Sources/ManagedModels/Utilities/NSSortDescriptors+Extras.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2023 ZeeZide GmbH. 4 | // 5 | 6 | import CoreData 7 | 8 | public extension NSSortDescriptor { 9 | 10 | enum SortOrder: Hashable { 11 | case forward, reverse 12 | } 13 | } 14 | 15 | /** 16 | * Create an NSSortDescriptor for a Swift KeyPath targeting a 17 | * ``PersistentModel``. 18 | * 19 | * - Parameters: 20 | * - keyPath: The keypath to sort on. 21 | * - order: Does it go forward or backwards? 22 | * - Returns: An `NSSortDescriptor` reflecting the parameters. 23 | */ 24 | @inlinable 25 | public func SortDescriptor(_ keyPath: KeyPath, 26 | order: NSSortDescriptor.SortOrder = .forward) 27 | -> NSSortDescriptor 28 | where M: PersistentModel & NSManagedObject 29 | { 30 | if let meta = M.schemaMetadata.first(where: { $0.keypath == keyPath }) { 31 | NSSortDescriptor(key: meta.name, ascending: order == .forward) 32 | } 33 | else { 34 | NSSortDescriptor(keyPath: keyPath, ascending: order == .forward) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Sources/ManagedModels/Utilities/OrderedSet.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2023 ZeeZide GmbH. 4 | // 5 | 6 | import Foundation 7 | 8 | // Generic subclasses of '@objc' classes cannot have an explicit '@objc' 9 | // because they are not directly visible from Objective-C. 10 | // W/o @objc the declaration works, but then @NSManaged complains about the 11 | // thing not being available in ObjC. 12 | #if false 13 | @objc public final class OrderedSet: NSOrderedSet 14 | where Element: Hashable 15 | { 16 | // This is to enable the use of NSOrderedSet w/ `RelationshipCollection`. 17 | } 18 | #endif 19 | -------------------------------------------------------------------------------- /Tests/ManagedModelTests/BasicModelTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2023 ZeeZide GmbH. 4 | // 5 | 6 | import XCTest 7 | import CoreData 8 | @testable import ManagedModels 9 | 10 | final class BasicModelTests: XCTestCase { 11 | 12 | let container = try? ModelContainer( 13 | for: Fixtures.PersonAddressSchema.managedObjectModel, 14 | configurations: ModelConfiguration(isStoredInMemoryOnly: true) 15 | ) 16 | 17 | func testEntityName() throws { 18 | let addressType = Fixtures.PersonAddressSchema.Address.self 19 | XCTAssertEqual(addressType.entity().name, "Address") 20 | } 21 | 22 | func testPersonTemporaryPersistentIdentifier() throws { 23 | let newAddress = Fixtures.PersonAddressSchema.Person() 24 | 25 | let id : NSManagedObjectID = newAddress.persistentModelID 26 | XCTAssertEqual(id.entityName, "Person") 27 | XCTAssertNil(id.storeIdentifier) // isTemporary! 28 | 29 | // Note: "t" prefix for `isTemporary`, "p" is primary key, e.g. p73 30 | // - also registered as the primary key 31 | // "x-coredata:///Person/t4DA54E01-0940-45F4-9E16-956E3C7993B52" 32 | let url = id.uriRepresentation() 33 | XCTAssertEqual(url.scheme, "x-coredata") 34 | XCTAssertNil(url.host) // not store assigned 35 | XCTAssertTrue(url.path.hasPrefix("/Person/t")) // <= "t" is temporary! 36 | } 37 | 38 | func testAddressTemporaryPersistentIdentifier() throws { 39 | // Failed to call designated initializer on NSManagedObject class 40 | // '_TtCOO17ManagedModelTests8Fixtures19PersonAddressSchema7Address' 41 | let newAddress = Fixtures.PersonAddressSchema.Address() 42 | 43 | let id : NSManagedObjectID = newAddress.persistentModelID 44 | XCTAssertEqual(id.entityName, "Address") 45 | XCTAssertNil(id.storeIdentifier) // isTemporary! 46 | 47 | // Note: "t" prefix for `isTemporary`, "p" is primary key, e.g. p73 48 | // - also registered as the primary key 49 | // "x-coredata:///Address/t4DA54E01-0940-45F4-9E16-956E3C7993B52" 50 | let url = id.uriRepresentation() 51 | XCTAssertEqual(url.scheme, "x-coredata") 52 | XCTAssertNil(url.host) // not store assigned 53 | XCTAssertTrue(url.path.hasPrefix("/Address/t")) // <= "t" is temporary! 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Tests/ManagedModelTests/CodablePropertiesTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2024 ZeeZide GmbH. 4 | // 5 | 6 | import XCTest 7 | import Foundation 8 | import CoreData 9 | @testable import ManagedModels 10 | 11 | final class CodablePropertiesTests: XCTestCase { 12 | 13 | private lazy var container = try? ModelContainer( 14 | for: Fixtures.CodablePropertiesSchema.managedObjectModel, 15 | configurations: ModelConfiguration(isStoredInMemoryOnly: true) 16 | ) 17 | 18 | func testEntityName() throws { 19 | _ = container 20 | let entityType = Fixtures.CodablePropertiesSchema.StoredAccess.self 21 | XCTAssertEqual(entityType.entity().name, "StoredAccess") 22 | } 23 | 24 | func testPropertySetup() throws { 25 | let valueType = Fixtures.CodablePropertiesSchema.AccessSIP.self 26 | let attribute = CoreData.NSAttributeDescription( 27 | name: "sip", 28 | valueType: valueType, 29 | defaultValue: nil 30 | ) 31 | XCTAssertEqual(attribute.name, "sip") 32 | XCTAssertEqual(attribute.attributeType, .transformableAttributeType) 33 | 34 | let transformerName = try XCTUnwrap( 35 | ValueTransformer.valueTransformerNames().first(where: { 36 | $0.rawValue.range(of: "CodableTransformerVOO17ManagedModelTests8") 37 | != nil 38 | }) 39 | ) 40 | let transformer = try XCTUnwrap(ValueTransformer(forName: transformerName)) 41 | _ = transformer // to clear unused-wraning 42 | 43 | XCTAssertTrue(attribute.valueType == Any.self) 44 | // Fixtures.CodablePropertiesSchema.AccessSIP.self 45 | XCTAssertNotNil(attribute.valueTransformerName) 46 | XCTAssertEqual(attribute.valueTransformerName, transformerName.rawValue) 47 | } 48 | 49 | func testCodablePropertyEntity() throws { 50 | let entity = try XCTUnwrap( 51 | container?.managedObjectModel.entitiesByName["StoredAccess"] 52 | ) 53 | 54 | // Creating the entity should have registered the transformer for the 55 | // CodableBox. 56 | let transformerName = try XCTUnwrap( 57 | ValueTransformer.valueTransformerNames().first(where: { 58 | $0.rawValue.range(of: "CodableTransformerVOO17ManagedModelTests8") 59 | != nil 60 | }) 61 | ) 62 | let transformer = try XCTUnwrap(ValueTransformer(forName: transformerName)) 63 | _ = transformer // to clear unused-wraning 64 | 65 | let attribute = try XCTUnwrap(entity.attributesByName["sip"]) 66 | XCTAssertEqual(attribute.name, "sip") 67 | XCTAssertTrue(attribute.valueType == Any.self) 68 | // Fixtures.CodablePropertiesSchema.AccessSIP.self) 69 | XCTAssertNotNil(attribute.valueTransformerName) 70 | XCTAssertEqual(attribute.valueTransformerName, transformerName.rawValue) 71 | } 72 | 73 | func testOptionalCodablePropertyEntity() throws { 74 | let entity = try XCTUnwrap( 75 | container?.managedObjectModel.entitiesByName["StoredAccess"] 76 | ) 77 | 78 | // Creating the entity should have registered the transformer for the 79 | // CodableBox. 80 | let transformerName = try XCTUnwrap( 81 | ValueTransformer.valueTransformerNames().first(where: { 82 | $0.rawValue.range(of: "CodableTransformerGSqVOO17ManagedModelTests8") 83 | != nil 84 | }) 85 | ) 86 | let transformer = try XCTUnwrap(ValueTransformer(forName: transformerName)) 87 | _ = transformer // to clear unused-wraning 88 | 89 | let attribute = try XCTUnwrap(entity.attributesByName["optionalSip"]) 90 | XCTAssertEqual(attribute.name, "optionalSip") 91 | XCTAssertTrue(attribute.valueType == Any?.self) 92 | // Fixtures.CodablePropertiesSchema.AccessSIP?.self) 93 | XCTAssertNotNil(attribute.valueTransformerName) 94 | XCTAssertEqual(attribute.valueTransformerName, transformerName.rawValue) 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /Tests/ManagedModelTests/CodableRawRepresentableTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2024 ZeeZide GmbH. 4 | // 5 | 6 | import XCTest 7 | import Foundation 8 | import CoreData 9 | @testable import ManagedModels 10 | 11 | final class CodableRawRepresentableTests: XCTestCase { 12 | // https://github.com/Data-swift/ManagedModels/issues/29 13 | 14 | private lazy var container = try? ModelContainer( 15 | for: Fixtures.ToDoListSchema.managedObjectModel, 16 | configurations: ModelConfiguration(isStoredInMemoryOnly: true) 17 | ) 18 | 19 | func testEntityName() throws { 20 | _ = container // required to register the entity type mapping 21 | let entityType = Fixtures.ToDoListSchema.ToDo.self 22 | XCTAssertEqual(entityType.entity().name, "ToDo") 23 | } 24 | 25 | func testPropertySetup() throws { 26 | let valueType = Fixtures.ToDoListSchema.ToDo.Priority.self 27 | let attribute = CoreData.NSAttributeDescription( 28 | name: "priority", 29 | valueType: valueType, 30 | defaultValue: nil 31 | ) 32 | XCTAssertEqual(attribute.name, "priority") 33 | XCTAssertEqual(attribute.attributeType, .integer64AttributeType) 34 | 35 | XCTAssertTrue(attribute.valueType == Int.self) 36 | XCTAssertNil(attribute.valueTransformerName) 37 | } 38 | 39 | func testModel() throws { 40 | _ = container // required to register the entity type mapping 41 | let todo = Fixtures.ToDoListSchema.ToDo() 42 | todo.priority = .high 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Tests/ManagedModelTests/CoreDataAssumptionsTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2023 ZeeZide GmbH. 4 | // 5 | 6 | import XCTest 7 | import CoreData 8 | @testable import ManagedModels 9 | 10 | final class CoreDataAssumptionsTests: XCTestCase { 11 | 12 | func testRelationshipDefaults() throws { 13 | let relationship = NSRelationshipDescription() 14 | XCTAssertEqual(relationship.minCount, 0) 15 | XCTAssertEqual(relationship.maxCount, 0) 16 | XCTAssertEqual(relationship.deleteRule, .nullifyDeleteRule) 17 | } 18 | 19 | func testToManySetup() throws { 20 | let relationship = NSRelationshipDescription() 21 | relationship.name = "addresses" 22 | //relationship.destinationEntity = 23 | // Fixtures.PersonAddressSchema.Address.entity() 24 | //relationship.inverseRelationship = 25 | // Fixtures.PersonAddressSchema.Address.entity().relationshipsByName["person"] 26 | 27 | // This just seems to be the default. 28 | XCTAssertTrue(relationship.isToMany) 29 | } 30 | 31 | func testToOneSetup() throws { 32 | let relationship = NSRelationshipDescription() 33 | relationship.name = "person" 34 | relationship.maxCount = 1 // toOne marker! 35 | #if false // old 36 | relationship.destinationEntity = 37 | Fixtures.PersonAddressSchema.Person.entity() 38 | #endif 39 | XCTAssertFalse(relationship.isToMany) 40 | } 41 | 42 | func testAttributeCopying() throws { 43 | let attribute = NSAttributeDescription() 44 | attribute.name = "Hello" 45 | attribute.attributeValueClassName = "NSNumber" 46 | attribute.attributeType = .integer32AttributeType 47 | 48 | let copiedAttribute = 49 | try XCTUnwrap(attribute.copy() as? NSAttributeDescription) 50 | XCTAssertFalse(attribute === copiedAttribute) 51 | XCTAssertEqual(attribute.name, copiedAttribute.name) 52 | XCTAssertEqual(attribute.attributeValueClassName, 53 | copiedAttribute.attributeValueClassName) 54 | XCTAssertEqual(attribute.attributeType, copiedAttribute.attributeType) 55 | } 56 | 57 | func testRelationshipCopying() throws { 58 | let relationship = NSRelationshipDescription() 59 | relationship.name = "Hello" 60 | relationship.isOrdered = true 61 | relationship.maxCount = 10 62 | 63 | let copiedRelationship = 64 | try XCTUnwrap(relationship.copy() as? NSRelationshipDescription) 65 | XCTAssertFalse(relationship === copiedRelationship) 66 | XCTAssertEqual(relationship.name, copiedRelationship.name) 67 | XCTAssertEqual(relationship.isOrdered, copiedRelationship.isOrdered) 68 | XCTAssertEqual(relationship.maxCount, copiedRelationship.maxCount) 69 | } 70 | 71 | 72 | func testAttributeValueClassIsNotEmpty() throws { 73 | do { 74 | let attribute = NSAttributeDescription() 75 | attribute.name = "Hello" 76 | attribute.attributeType = .stringAttributeType 77 | XCTAssertEqual(attribute.attributeValueClassName, "NSString") 78 | } 79 | do { 80 | let attribute = NSAttributeDescription() 81 | attribute.name = "Hello" 82 | attribute.attributeType = .integer16AttributeType 83 | XCTAssertEqual(attribute.attributeValueClassName, "NSNumber") 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /Tests/ManagedModelTests/FetchRequestTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2023 ZeeZide GmbH. 4 | // 5 | 6 | import XCTest 7 | import CoreData 8 | import SwiftUI 9 | @testable import ManagedModels 10 | 11 | final class FetchRequestTests: SwiftUITestCase { 12 | 13 | typealias TestSchema = Fixtures.PersonAddressSchema 14 | typealias Person = TestSchema.Person 15 | typealias Address = TestSchema.Address 16 | 17 | private let context : ModelContext? = { () -> ModelContext? in 18 | guard let container = try? ModelContainer( 19 | for: TestSchema.managedObjectModel, 20 | configurations: ModelConfiguration(isStoredInMemoryOnly: true) 21 | ) else { return nil } 22 | // let context = ModelContext(concurrencyType: .mainQueueConcurrencyType) 23 | let context = container.mainContext 24 | XCTAssertFalse(context.autosaveEnabled) // still seems to be on? 25 | 26 | let donald : Person 27 | do { 28 | let person = Person(context: context) 29 | person.firstname = "Donald" 30 | person.lastname = "Duck" 31 | person.addresses = [] 32 | donald = person 33 | } 34 | 35 | do { 36 | let address = Address(context: context) 37 | address.appartment = "404" 38 | address.street = "Am Geldspeicher 1" 39 | address.person = donald 40 | } 41 | do { 42 | let address = Address(context: context) 43 | address.appartment = "409" 44 | address.street = "Dusseldorfer Straße 10" 45 | address.person = donald 46 | } 47 | do { 48 | let address = Address(context: context) 49 | address.appartment = "204" 50 | address.street = "No Content 4" 51 | address.person = donald 52 | } 53 | 54 | do { 55 | try context.save() 56 | } 57 | catch { 58 | print("Error:", error) // throws nilError 59 | XCTAssert(false, "Error thrown") 60 | } 61 | return context 62 | }() 63 | 64 | func testFetchRequest() throws { 65 | let context = try XCTUnwrap(context) 66 | 67 | let fetchRequest = Address.fetchRequest() 68 | let models = try context.fetch(fetchRequest) 69 | XCTAssertEqual(models.count, 3) 70 | } 71 | 72 | @MainActor 73 | func testFetchCount() throws { 74 | let context = try XCTUnwrap(context) 75 | 76 | class TestResults { 77 | var fetchedCount = 0 78 | } 79 | 80 | struct TestView: View { 81 | let results : TestResults 82 | let expectation : XCTestExpectation 83 | 84 | @FetchRequest( 85 | sort: \Address.appartment, 86 | animation: .none 87 | ) 88 | private var values: FetchedResults

89 | 90 | var body: Text { // This must NOT be an `EmptyView`! 91 | results.fetchedCount = values.count 92 | expectation.fulfill() 93 | return Text(verbatim: "Dummy") 94 | } 95 | } 96 | 97 | let expectation = XCTestExpectation(description: "Test Query Count") 98 | let results = TestResults() 99 | let view = TestView(results: results, expectation: expectation) 100 | .modelContext(context) 101 | 102 | try constructView(view, waitingFor: expectation) 103 | 104 | XCTAssertEqual(results.fetchedCount, 3) 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /Tests/ManagedModelTests/Fixtures.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2023 ZeeZide GmbH. 4 | // 5 | 6 | import ManagedModels 7 | 8 | enum Fixtures { 9 | 10 | @Model 11 | final class UniquePerson: NSManagedObject { 12 | @Attribute(.unique) 13 | var firstname : String 14 | var lastname : String 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Tests/ManagedModelTests/ObjCMarkedPropertiesTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ObjCMarkedPropertiesTests.swift 3 | // ManagedModels 4 | // 5 | // Created by Adam Kopeć on 12/02/2025. 6 | // 7 | import XCTest 8 | import Foundation 9 | import CoreData 10 | @testable import ManagedModels 11 | 12 | final class ObjCMarkedPropertiesTests: XCTestCase { 13 | func getAllObjCPropertyNames() -> [String] { 14 | let classType: AnyClass = Fixtures.AdvancedCodablePropertiesSchema.AdvancedStoredAccess.self 15 | 16 | var count: UInt32 = 0 17 | var properties = [String]() 18 | class_copyPropertyList(classType, &count)?.withMemoryRebound(to: objc_property_t.self, capacity: Int(count), { pointer in 19 | var ptr = pointer 20 | for _ in 0.. String { 31 | let classType: AnyClass = Fixtures.AdvancedCodablePropertiesSchema.AdvancedStoredAccess.self 32 | 33 | let property = class_getProperty(classType, propertyName) 34 | XCTAssertNotNil(property, "Property \(propertyName) not found") 35 | guard let property else { return "" } 36 | let attributes = property_getAttributes(property) 37 | let attributesString = String(cString: attributes!) 38 | 39 | return attributesString 40 | } 41 | 42 | func testPropertiesMarkedObjC() { 43 | let tokenAttributes = getObjCAttributes(propertyName: "token") 44 | XCTAssertTrue(tokenAttributes.contains("T@\"NSString\""), "Property token is not marked as @objc (\(tokenAttributes))") 45 | 46 | let expiresAttributes = getObjCAttributes(propertyName: "expires") 47 | XCTAssertTrue(expiresAttributes.contains("T@\"NSDate\""), "Property expires is not marked as @objc (\(expiresAttributes))") 48 | 49 | let integerAttributes = getObjCAttributes(propertyName: "integer") 50 | XCTAssertTrue(!integerAttributes.isEmpty, "Property integer is not marked as @objc (\(integerAttributes))") 51 | 52 | let arrayAttributes = getObjCAttributes(propertyName: "array") 53 | XCTAssertTrue(arrayAttributes.contains("T@\"NSArray\""), "Property array is not marked as @objc (\(arrayAttributes))") 54 | 55 | let array2Attributes = getObjCAttributes(propertyName: "array2") 56 | XCTAssertTrue(arrayAttributes.contains("T@\"NSArray\""), "Property array2 is not marked as @objc (\(array2Attributes))") 57 | 58 | let numArrayAttributes = getObjCAttributes(propertyName: "numArray") 59 | XCTAssertTrue(numArrayAttributes.contains("T@\"NSArray\""), "Property numArray is not marked as @objc (\(numArrayAttributes))") 60 | 61 | let optionalArrayAttributes = getObjCAttributes(propertyName: "optionalArray") 62 | XCTAssertTrue(optionalArrayAttributes.contains("T@\"NSArray\""), "Property optionalArray is not marked as @objc (\(optionalArrayAttributes))") 63 | 64 | let optionalArray2Attributes = getObjCAttributes(propertyName: "optionalArray2") 65 | XCTAssertTrue(optionalArray2Attributes.contains("T@\"NSArray\""), "Property optionalArray2 is not marked as @objc (\(optionalArray2Attributes))") 66 | 67 | let optionalNumArrayAttributes = getObjCAttributes(propertyName: "optionalNumArray") 68 | XCTAssertTrue(optionalNumArrayAttributes.contains("T@\"NSArray\""), "Property optionalNumArray is not marked as @objc (\(optionalNumArrayAttributes))") 69 | 70 | let optionalNumArray2Attributes = getObjCAttributes(propertyName: "optionalNumArray2") 71 | XCTAssertTrue(optionalNumArray2Attributes.contains("T@\"NSArray\""), "Property optionalNumArray2 is not marked as @objc (\(optionalNumArray2Attributes))") 72 | 73 | let objcSetAttributes = getObjCAttributes(propertyName: "objcSet") 74 | XCTAssertTrue(objcSetAttributes.contains("T@\"NSSet\""), "Property objcSet is not marked as @objc (\(objcSetAttributes))") 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /Tests/ManagedModelTests/RelationshipSetupTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2023 ZeeZide GmbH. 4 | // 5 | 6 | import XCTest 7 | import CoreData 8 | import SwiftUI 9 | @testable import ManagedModels 10 | 11 | final class RelationshipSetupTests: SwiftUITestCase { 12 | 13 | typealias TestSchema = Fixtures.PersonAddressSchema 14 | typealias Person = TestSchema.Person 15 | typealias Address = TestSchema.Address 16 | 17 | private let context : ModelContext? = { () -> ModelContext? in 18 | guard let container = try? ModelContainer( 19 | for: TestSchema.managedObjectModel, 20 | configurations: ModelConfiguration(isStoredInMemoryOnly: true) 21 | ) else { return nil } 22 | // let context = ModelContext(concurrencyType: .mainQueueConcurrencyType) 23 | let context = container.mainContext 24 | XCTAssertFalse(context.autosaveEnabled) // still seems to be on? 25 | return context 26 | }() 27 | 28 | func testToManyFill() throws { 29 | let donald : Person 30 | do { 31 | let person = Person(context: context) 32 | person.firstname = "Donald" 33 | person.lastname = "Duck" 34 | person.addresses = [] 35 | donald = person 36 | } 37 | 38 | do { 39 | let address = Address(context: context) 40 | address.appartment = "404" 41 | address.street = "Am Geldspeicher 1" 42 | address.person = donald 43 | } 44 | do { 45 | let address = Address(context: context) 46 | address.appartment = "409" 47 | address.street = "Dusseldorfer Straße 10" 48 | address.person = donald 49 | } 50 | do { 51 | let address = Address(context: context) 52 | address.appartment = "204" 53 | address.street = "No Content 4" 54 | address.person = donald 55 | } 56 | 57 | XCTAssertEqual(donald.addresses.count, 3) 58 | } 59 | 60 | func testToOneFill() throws { 61 | let person = Person(context: context) 62 | person.firstname = "Donald" 63 | person.lastname = "Duck" 64 | 65 | let address = Address(context: context) 66 | address.appartment = "404" 67 | address.street = "Am Geldspeicher 1" 68 | person.addresses = [ address ] 69 | 70 | XCTAssertEqual(person.addresses.count, 1) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Tests/ManagedModelTests/Schemas/AdvancedCodablePropertiesSchema.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AdvancedCodablePropertiesSchema.swift 3 | // ManagedModels 4 | // 5 | // Created by Adam Kopeć on 04/02/2025. 6 | // 7 | 8 | import ManagedModels 9 | 10 | extension Fixtures { 11 | // https://github.com/Data-swift/ManagedModels/issues/36 12 | 13 | enum AdvancedCodablePropertiesSchema: VersionedSchema { 14 | static var models : [ any PersistentModel.Type ] = [ 15 | AdvancedStoredAccess.self 16 | ] 17 | 18 | public static let versionIdentifier = Schema.Version(0, 1, 0) 19 | 20 | @Model 21 | final class AdvancedStoredAccess: NSManagedObject { 22 | var token : String 23 | var expires : Date 24 | var integer : Int 25 | var distance: Int? 26 | var avgSpeed: Double? 27 | var sip : AccessSIP 28 | var numArray: [Int] 29 | var array : [String] 30 | var array2 : Array 31 | var optionalNumArray : [Int]? 32 | var optionalNumArray2: Array? 33 | var optionalArray : [String]? 34 | var optionalArray2 : Array? 35 | var optionalSip : AccessSIP? 36 | var codableSet : Set 37 | var objcSet : Set 38 | var objcNumSet : Set 39 | var codableArray : [AccessSIP] 40 | var optCodableSet : Set? 41 | var optCodableArray : [AccessSIP]? 42 | } 43 | 44 | struct AccessSIP: Codable, Hashable { 45 | var username : String 46 | var password : String 47 | var realm : String 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Tests/ManagedModelTests/Schemas/CodablePropertySchema.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2024 ZeeZide GmbH. 4 | // 5 | 6 | import ManagedModels 7 | 8 | extension Fixtures { 9 | // https://github.com/Data-swift/ManagedModels/issues/27 10 | 11 | enum CodablePropertiesSchema: VersionedSchema { 12 | static var models : [ any PersistentModel.Type ] = [ 13 | StoredAccess.self 14 | ] 15 | 16 | public static let versionIdentifier = Schema.Version(0, 1, 0) 17 | 18 | @Model 19 | final class StoredAccess: NSManagedObject { 20 | var token : String 21 | var expires : Date 22 | var sip : AccessSIP 23 | var optionalSip : AccessSIP? 24 | } 25 | 26 | struct AccessSIP: Codable { 27 | var username : String 28 | var password : String 29 | var realm : String 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Tests/ManagedModelTests/Schemas/ExpandedPersonAddressSchema.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2023 ZeeZide GmbH. 4 | // 5 | 6 | import ManagedModels 7 | 8 | extension Fixtures { 9 | 10 | enum ExpandedPersonAddressSchema: VersionedSchema { 11 | 12 | static var models : [ any PersistentModel.Type ] = [ 13 | Person.self, 14 | Address.self 15 | ] 16 | 17 | public static let versionIdentifier = Schema.Version(0, 1, 0) 18 | 19 | 20 | final class Person: NSManagedObject, PersistentModel { 21 | 22 | // TBD: Why are the inits required? 23 | // @NSManaged property cannot have an initial value?! 24 | @NSManaged var firstname : String 25 | @NSManaged var lastname : String 26 | @NSManaged var addresses : [ Address ] 27 | 28 | // init() is a convenience initializer, it looks up the the entity for the 29 | // object? 30 | // Can we generate inits? 31 | 32 | init(firstname: String, lastname: String, addresses: [ Address ]) { 33 | // Note: Could do Self.entity! 34 | super.init(entity: Self.entity(), insertInto: nil) 35 | self.firstname = firstname 36 | self.lastname = lastname 37 | self.addresses = addresses 38 | } 39 | 40 | public static let schemaMetadata : [ Schema.PropertyMetadata ] = [ 41 | .init(name: "firstname" , keypath: \Person.firstname), 42 | .init(name: "lastname" , keypath: \Person.lastname), 43 | .init(name: "addresses" , keypath: \Person.addresses) 44 | ] 45 | public static let _$originalName : String? = nil 46 | public static let _$hashModifier : String? = nil 47 | } 48 | 49 | final class Address: NSManagedObject, PersistentModel { 50 | 51 | @NSManaged var street : String 52 | @NSManaged var appartment : String? 53 | @NSManaged var person : Person 54 | 55 | init(street: String, appartment: String? = nil, person: Person) { 56 | super.init(entity: Self.entity(), insertInto: nil) 57 | self.street = street 58 | self.appartment = appartment 59 | self.person = person 60 | } 61 | 62 | public static let schemaMetadata : [ Schema.PropertyMetadata ] = [ 63 | .init(name: "street" , keypath: \Address.street), 64 | .init(name: "appartment" , keypath: \Address.appartment), 65 | .init(name: "person" , keypath: \Address.person) 66 | ] 67 | public static let _$originalName : String? = nil 68 | public static let _$hashModifier : String? = nil 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /Tests/ManagedModelTests/Schemas/PersonAddressOptionalToOne.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2023 ZeeZide GmbH. 4 | // 5 | 6 | import ManagedModels 7 | 8 | extension Fixtures { 9 | 10 | enum PersonAddressOptionalToOneSchema: VersionedSchema { 11 | static var models : [ any PersistentModel.Type ] = [ 12 | Person.self, 13 | Address.self 14 | ] 15 | 16 | public static let versionIdentifier = Schema.Version(0, 1, 0) 17 | 18 | 19 | @Model class Person: NSManagedObject { 20 | var addresses : Set
// [ Address ] 21 | } 22 | 23 | @Model class Address: NSManagedObject { 24 | @Relationship(deleteRule: .nullify, originalName: "PERSON") 25 | var person : Person? 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Tests/ManagedModelTests/Schemas/PersonAddressSchema.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2023 ZeeZide GmbH. 4 | // 5 | 6 | import ManagedModels 7 | 8 | extension Fixtures { 9 | 10 | static let PersonAddressMOM = PersonAddressSchema.managedObjectModel 11 | 12 | enum PersonAddressSchema: VersionedSchema { 13 | static var models : [ any PersistentModel.Type ] = [ 14 | Person.self, 15 | Address.self 16 | ] 17 | 18 | public static let versionIdentifier = Schema.Version(0, 1, 0) 19 | 20 | 21 | @Model 22 | final class Person: NSManagedObject { 23 | 24 | var firstname : String 25 | var lastname : String 26 | var addresses : Set
// [ Address ] 27 | } 28 | 29 | enum AddressType: Int { 30 | case home, work 31 | } 32 | 33 | @Model 34 | final class Address /*test*/ : NSManagedObject { 35 | 36 | var street : String 37 | var appartment : String? 38 | var type : AddressType 39 | var person : Person 40 | 41 | // Either: super.init(entity: Self.entity(), insertInto: nil) 42 | // Or: mark this as `convenience` 43 | convenience init(street: String, appartment: String? = nil, type: AddressType, person: Person) { 44 | //super.init(entity: Self.entity(), insertInto: nil) 45 | self.init() 46 | self.street = street 47 | self.appartment = appartment 48 | self.type = type 49 | self.person = person 50 | } 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Tests/ManagedModelTests/Schemas/PersonAddressSchemaNoInverse.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2023 ZeeZide GmbH. 4 | // 5 | 6 | import ManagedModels 7 | 8 | extension Fixtures { 9 | 10 | enum PersonAddressSchemaNoInverse: VersionedSchema { 11 | static var models : [ any PersistentModel.Type ] = [ 12 | Person.self, 13 | Address.self 14 | ] 15 | 16 | public static let versionIdentifier = Schema.Version(0, 1, 0) 17 | 18 | 19 | @Model 20 | final class Person: NSManagedObject, PersistentModel { 21 | 22 | var firstname : String 23 | var lastname : String 24 | var addresses : [ Address ] 25 | 26 | init(firstname: String, lastname: String, addresses: [ Address ]) { 27 | super.init(entity: Self.entity(), insertInto: nil) 28 | self.firstname = firstname 29 | self.lastname = lastname 30 | self.addresses = addresses 31 | } 32 | } 33 | 34 | @Model 35 | final class Address /*test*/ : NSManagedObject, PersistentModel { 36 | 37 | var street : String 38 | var appartment : String? 39 | 40 | convenience init(street: String, appartment: String? = nil) { 41 | self.init() 42 | self.street = street 43 | self.appartment = appartment 44 | } 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Tests/ManagedModelTests/Schemas/ToDoListSchema.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2024 ZeeZide GmbH. 4 | // 5 | 6 | import ManagedModels 7 | 8 | extension Fixtures { 9 | 10 | enum ToDoListSchema: VersionedSchema { 11 | static var models : [ any PersistentModel.Type ] = [ 12 | ToDo.self, ToDoList.self 13 | ] 14 | 15 | public static let versionIdentifier = Schema.Version(0, 1, 0) 16 | 17 | @Model 18 | final class ToDo: NSManagedObject { 19 | 20 | var title : String 21 | var isDone : Bool 22 | var priority : Priority 23 | var created : Date 24 | var due : Date? 25 | var list : ToDoList 26 | 27 | enum Priority: Int, Comparable, CaseIterable, Codable { 28 | case veryLow = 1 29 | case low = 2 30 | case medium = 3 31 | case high = 4 32 | case veryHigh = 5 33 | 34 | static func < (lhs: Self, rhs: Self) -> Bool { 35 | lhs.rawValue < rhs.rawValue 36 | } 37 | } 38 | 39 | convenience init(list : ToDoList, 40 | title : String, 41 | isDone : Bool = false, 42 | priority : Priority = .medium, 43 | created : Date = Date(), 44 | due : Date? = nil) 45 | { 46 | // This is important so that the objects don't end up in different 47 | // contexts. 48 | self.init(context: list.modelContext) 49 | 50 | self.list = list 51 | self.title = title 52 | self.isDone = isDone 53 | self.priority = priority 54 | self.created = created 55 | self.due = due 56 | } 57 | 58 | var isOverDue : Bool { 59 | guard let due else { return false } 60 | return due < Date() 61 | } 62 | } 63 | 64 | @Model 65 | final class ToDoList: NSManagedObject { 66 | 67 | var title = "" 68 | var toDos = [ ToDo ]() 69 | 70 | convenience init(title: String) { 71 | self.init() 72 | self.title = title 73 | } 74 | 75 | var hasOverdueItems : Bool { toDos.contains { $0.isOverDue && !$0.isDone } } 76 | 77 | enum Doneness { case all, none, some } 78 | 79 | var doneness : Doneness { 80 | let hasDone = toDos.contains { $0.isDone } 81 | let hasUndone = toDos.contains { !$0.isDone } 82 | switch ( hasDone, hasUndone ) { 83 | case ( true , true ) : return .some 84 | case ( true , false ) : return .all 85 | case ( false , true ) : return .none 86 | case ( false , false ) : return .all // empty 87 | } 88 | } 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /Tests/ManagedModelTests/Schemas/TransformablePropertySchema.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TransformablePropertySchema.swift 3 | // Created by Adam Kopeć on 11/02/2024. 4 | // 5 | 6 | import ManagedModels 7 | 8 | extension Fixtures { 9 | // https://github.com/Data-swift/ManagedModels/issues/4 10 | 11 | enum TransformablePropertiesSchema: VersionedSchema { 12 | static var models : [ any PersistentModel.Type ] = [ 13 | StoredAccess.self 14 | ] 15 | 16 | public static let versionIdentifier = Schema.Version(0, 1, 0) 17 | 18 | @Model 19 | final class StoredAccess: NSManagedObject { 20 | var token : String 21 | var expires : Date 22 | @Attribute(.transformable(by: AccessSIPTransformer.self)) 23 | var sip : AccessSIP 24 | @Attribute(.transformable(by: AccessSIPTransformer.self)) 25 | var optionalSIP : AccessSIP? 26 | } 27 | 28 | class AccessSIP: NSObject { 29 | var username : String 30 | var password : String 31 | 32 | init(username: String, password: String) { 33 | self.username = username 34 | self.password = password 35 | } 36 | } 37 | 38 | class AccessSIPTransformer: ValueTransformer { 39 | override class func transformedValueClass() -> AnyClass { 40 | return AccessSIP.self 41 | } 42 | 43 | override class func allowsReverseTransformation() -> Bool { 44 | return true 45 | } 46 | 47 | override func transformedValue(_ value: Any?) -> Any? { 48 | guard let data = value as? Data else { return nil } 49 | guard let array = try? JSONDecoder().decode([String].self, from: data) else { return nil } 50 | return AccessSIP(username: array[0], password: array[1]) 51 | } 52 | 53 | override func reverseTransformedValue(_ value: Any?) -> Any? { 54 | guard let sip = value as? AccessSIP else { return nil } 55 | return try? JSONEncoder().encode([sip.username, sip.password]) 56 | } 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Tests/ManagedModelTests/SwiftUITestCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Helge Heß. 3 | // Copyright © 2023 ZeeZide GmbH. 4 | // 5 | 6 | import XCTest 7 | #if os(macOS) 8 | import AppKit 9 | #elseif canImport(UIKit) 10 | import UIKit 11 | #endif 12 | import SwiftUI 13 | 14 | class SwiftUITestCase: XCTestCase { 15 | #if os(macOS) 16 | private lazy var window : NSWindow? = { 17 | let window = NSWindow( 18 | contentRect: .init(x: 0, y: 0, width: 720, height: 480), 19 | styleMask: .utilityWindow, backing: .buffered, defer: false 20 | ) 21 | return window 22 | }() 23 | 24 | func constructView(_ view: V, 25 | waitingFor expectation: XCTestExpectation) throws 26 | { 27 | let window = try XCTUnwrap(self.window) 28 | window.contentViewController = NSHostingController(rootView: view) 29 | window.contentViewController?.view.layout() 30 | wait(for: [expectation], timeout: 2) 31 | } 32 | #elseif canImport(UIKit) 33 | private lazy var window : UIWindow? = { 34 | let window = UIWindow(frame: .init(x: 0, y: 0, width: 720, height: 480)) 35 | window.isHidden = false 36 | return window 37 | }() 38 | 39 | func constructView(_ view: V, 40 | waitingFor expectation: XCTestExpectation) throws 41 | { 42 | let window = try XCTUnwrap(self.window) 43 | window.rootViewController = UIHostingController(rootView: view) 44 | window.rootViewController?.view.layoutIfNeeded() 45 | wait(for: [expectation], timeout: 2) 46 | } 47 | #endif 48 | } 49 | -------------------------------------------------------------------------------- /Tests/ManagedModelTests/TransformablePropertiesTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TransformablePropertiesTests.swift 3 | // Created by Adam Kopeć on 11/02/2024. 4 | // 5 | 6 | import XCTest 7 | import Foundation 8 | import CoreData 9 | @testable import ManagedModels 10 | 11 | final class TransformablePropertiesTests: XCTestCase { 12 | 13 | private let container = try? ModelContainer( 14 | for: Fixtures.TransformablePropertiesSchema.managedObjectModel, 15 | configurations: ModelConfiguration(isStoredInMemoryOnly: true) 16 | ) 17 | 18 | func testEntityName() throws { 19 | let entityType = Fixtures.TransformablePropertiesSchema.StoredAccess.self 20 | XCTAssertEqual(entityType.entity().name, "StoredAccess") 21 | } 22 | 23 | func testPropertySetup() throws { 24 | let valueType = Fixtures.TransformablePropertiesSchema.AccessSIP.self 25 | let attribute = CoreData.NSAttributeDescription( 26 | name: "sip", 27 | options: [.transformable(by: Fixtures.TransformablePropertiesSchema.AccessSIPTransformer.self)], 28 | valueType: valueType, 29 | defaultValue: nil 30 | ) 31 | XCTAssertEqual(attribute.name, "sip") 32 | XCTAssertEqual(attribute.attributeType, .transformableAttributeType) 33 | 34 | let transformerName = try XCTUnwrap( 35 | ValueTransformer.valueTransformerNames().first(where: { 36 | $0.rawValue.range(of: "AccessSIPTransformer") 37 | != nil 38 | }) 39 | ) 40 | let transformer = try XCTUnwrap(ValueTransformer(forName: transformerName)) 41 | _ = transformer // to clear unused-wraning 42 | 43 | XCTAssertTrue(attribute.valueType == 44 | NSObject.self) 45 | XCTAssertNotNil(attribute.valueTransformerName) 46 | XCTAssertEqual(attribute.valueTransformerName, transformerName.rawValue) 47 | } 48 | 49 | func testTransformablePropertyEntity() throws { 50 | let entity = try XCTUnwrap( 51 | container?.managedObjectModel.entitiesByName["StoredAccess"] 52 | ) 53 | 54 | // Creating the entity should have registered the transformer for the 55 | // CodableBox. 56 | let transformerName = try XCTUnwrap( 57 | ValueTransformer.valueTransformerNames().first(where: { 58 | $0.rawValue.range(of: "AccessSIPTransformer") 59 | != nil 60 | }) 61 | ) 62 | let transformer = try XCTUnwrap(ValueTransformer(forName: transformerName)) 63 | _ = transformer // to clear unused-wraning 64 | 65 | let attribute = try XCTUnwrap(entity.attributesByName["sip"]) 66 | XCTAssertEqual(attribute.name, "sip") 67 | XCTAssertTrue(attribute.valueType == 68 | NSObject.self) 69 | XCTAssertNotNil(attribute.valueTransformerName) 70 | XCTAssertEqual(attribute.valueTransformerName, transformerName.rawValue) 71 | } 72 | } 73 | --------------------------------------------------------------------------------