(_ 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 |
--------------------------------------------------------------------------------