Model your schema with SwiftData

RSS for tag

Discuss the WWDC23 Session Model your schema with SwiftData

Posts under wwdc2023-10195 tag

23 Posts
Sort by:

Post

Replies

Boosts

Views

Activity

SwiftData @Query crashes when trying to filter or sort using an enum or relationship
Like the title says, I've realised that when I try to use filter or sort on properties that aren't standard supported data types i.e. Using a transformable or a value type like an enum, I seem to be getting the following crash... SwiftData/DataUtilities.swift:1140: Fatal error: Unexpected type for Expansion: Optional<UIColor> Xcode expands and shows me when trying to access the wrapped value it's crashing. I'm assumung that the query property wrapper can't handle these custom data types @Query private var items: [Item] { get { _items.wrappedValue <--- Crash here } } Which seems to be pointing to a transferable property in one of my models. Below are my two models i'm using. enum Priority: Int, Codable, Identifiable, CaseIterable { case low case medium case high var title: String { switch self { case .low: return "Low" case .medium: return "Medium" case .high: return "High" } } var image: Image? { switch self { case .medium: return Image(systemName: "exclamationmark.2") case .high: return Image(systemName: "exclamationmark.3") default: return nil } } var id: Self { self } } @Model final class Item: Codable { var title: String @Attribute(originalName: "timestamp") var dueDate: Date var isCompleted: Bool var isFlagged: Bool = false var isArchived: Bool = false var isCritical: Bool? var priority: Priority? @Relationship(deleteRule: .nullify, inverse: \Category.items) var category: Category? @Attribute(.externalStorage) var image: Data? enum CodingKeys: String, CodingKey { case title case timestamp case isCritical case isCompleted case category case imageName } init(title: String = "", dueDate: Date = .now, priority: Priority? = nil, isCompleted: Bool = false) { self.title = title self.dueDate = dueDate self.priority = priority self.isCompleted = isCompleted } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.title = try container.decode(String.self, forKey: .title) self.dueDate = Date.randomDateNextWeek() ?? .now self.isCompleted = try container.decode(Bool.self, forKey: .isCompleted) self.category = try container.decodeIfPresent(Category.self, forKey: .category) if let imageName = try container.decodeIfPresent(String.self, forKey: .imageName), let imageData = UIImage(named: imageName) { self.image = imageData.jpegData(compressionQuality: 0.8) } if let isCritical = try container.decodeIfPresent(Bool.self, forKey: .isCritical), isCritical == true { self.priority = .high } } func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(title, forKey: .title) try container.encode(dueDate, forKey: .timestamp) try container.encode(isCompleted, forKey: .isCompleted) try container.encode(category, forKey: .category) } } @Model class Category: Codable { @Attribute(.unique) var title: String var items: [Item]? @Attribute(.transformable(by: ColorValueTransformer.self)) var color: UIColor? init(title: String = "", color: UIColor) { self.title = title self.color = color } enum CodingKeys: String, CodingKey { case title } required init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.title = try container.decode(String.self, forKey: .title) self.color = UIColor(possibleColors.randomElement()!) } func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(title, forKey: .title) } } And below is an example of me sorting based on my enum (Priority) & Relationship (Category name) func sort() -> [SortDescriptor<Item>]{ switch self { case .title: [SortDescriptor(\Item.title)] case .date: [SortDescriptor(\Item.dueDate)] case .category: [SortDescriptor(\Item.category?.title)] case .priority: [SortDescriptor(\Item.priority?.rawValue)] } } And a filter example below creating a predicate that we will execute to return and matches found in the title or category title let highPriority = Priority.high if let query { return #Predicate { $0.priority == highPriority && ($0.title.contains(query) || $0.category?.title.contains(query) == true) && $0.isArchived == false } } I'm pretty sure this is a SwiftData bug since when using strings, bools and dates it's all fine using anything outside of that box causes these crashes...
8
1
1.8k
Feb ’24
#Predicate doesn't work with enum
Problem The following code doesn't work: let predicate = #Predicate<Car> { car in car.size == size //This doesn't work } Console Error Query encountered an error: SwiftData.SwiftDataError(_error: SwiftData.SwiftDataError._Error.unsupportedPredicate) Root cause Size is an enum, #Predicate works with other type such as String however doesn't work with enum Enum value is saved however is not filtered by #Predicate Environment Xcode: 15.0 (15A240d) - App Store macOS: 14.0 (23A339) - Release Candidate Steps to reproduce Run the app on iOS 17 or macOS Sonoma Press the Add button Notice that the list remains empty Expected behaviour List should show the newly created small car Actual behaviour List remains empty inspite of successfully creating the small car. Feedback FB13194334 Code Size enum Size: String, Codable { case small case medium case large } Car import SwiftData @Model class Car { let id: UUID let name: String let size: Size init( id: UUID, name: String, size: Size ) { self.id = id self.name = name self.size = size } } ContentView struct ContentView: View { var body: some View { NavigationStack { CarList(size: .small) } } CarList import SwiftUI import SwiftData struct CarList: View { let size: Size @Environment(\.modelContext) private var modelContext @Query private var cars: [Car] init(size: Size) { self.size = size let predicate = #Predicate<Car> { car in car.size == size //This doesn't work } _cars = Query(filter: predicate, sort: \.name) } var body: some View { List(cars) { car in VStack(alignment: .leading) { Text(car.name) Text("\(car.size.rawValue)") Text(car.id.uuidString) .font(.footnote) } } .toolbar { Button("Add") { createCar() } } } private func createCar() { let name = "aaa" let car = Car( id: UUID(), name: name, size: size ) modelContext.insert(car) } }
5
1
1.1k
Sep ’23
SwiftData Many to One Cascade Delete not working as I expect in Xcode15 Beta 7 & 8
This approach worked in Beta 5, but does not work in Betas 7 & 8. (I skipped Beta 6, so I don't know if it worked then). I'm not sure if it's a SwiftData bug or a mistake on my part since the API has changed since the WWDC video. (I opened Apple Feedback: FB13120831) I am trying to implement a cascade delete on a many to one relationship. See the two models below. When I deleted the Recipe Model object, I expect that any of the FoodMenus that have that recipe will also be deleted. But they are not. The Recipe is deleted and the FoodMenu is still there. When I put a deleteRule: .cascade on the FoodMenu.recipe, the cascade does work. The Menu and the Associated recipe are both deleted. But that's not the behavior I want. @Model final class Recipe { var name: String var id: UUID @Relationship(deleteRule: .cascade, inverse: \FoodMenu.recipe) var menus: [FoodMenu]? init(name: String, id: UUID, menus: [FoodMenu]? = []) { self.name = name self.id = id self.menus = menus } } @Model final class FoodMenu { var name: String var id: UUID var recipe: Recipe? = nil //I believe the below definition should be the same behavior as above and it works as expected. relationship is nullified when the FoodMenu is deleted. Recipe left in place. // @Relationship (deleteRule: .nullify, inverse: \Recipe.menus) // var recipe: Recipe? = nil init(name: String, id: UUID) { self.name = name self.id = id } }
1
0
937
Sep ’23
SwiftData with two Stores
Hi, has anybody managed to get two sqlite stores working? If I define the stores with a configuration for each it seems like that only the first configuration and and therefore the store is recognised. This is how I define the configuration and container: import SwiftData @main struct SwiftDataTestApp: App { var modelContainer: ModelContainer init() { let fullSchema = Schema([ SetModel.self, NewsModel.self ]) let setConfiguration = ModelConfiguration( "setconfig", schema: Schema([SetModel.self]), url: FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!.appendingPathComponent("Sets.sqlite"), readOnly: false) let newsConfiguration = ModelConfiguration( "newsconfig", schema: Schema([NewsModel.self]), url: FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first!.appendingPathComponent("News.sqlite"), readOnly: false) modelContainer = try! ModelContainer(for: fullSchema, configurations: [setConfiguration,newsConfiguration]) } var body: some Scene { WindowGroup { ContentView() } .modelContainer(modelContainer) } } ContentView is just a basic TabView with a tab for news and a tab for sets. If I run the program this way the sets tab is shown correctly but switching to News fails. If I change the order of the configurations and write the one for news first like this: modelContainer = try! ModelContainer(for: fullSchema, configurations: [newsConfiguration, setConfiguration]) then the news tab is shown correctly and switching to sets tab fails. NewsModel and SetModel only differ in the class name Import Foundation import SwiftData @Model public class NewsModel{ public var name: String init(name: String) { self.name = name } } Also the tab content differs only for referencing the respecting model and the name: import SwiftData struct NewsTab: View { @Query private var news: [NewsModel] @Environment(\.modelContext) private var modelContext var body: some View { ScrollView{ LazyVStack{ ForEach(news){actNews in Text("Hello, News \(actNews.name)") } } .onAppear { let news = NewsModel(name: "News from \(Date())") modelContext.insert(news) try! modelContext.save() } } } } The error message is "NSFetchRequest could not locate an NSEntityDescription for entity name 'NewsModel'" (and SetsModel respectively when change the order of the configuration) Do I explicitly need to tell the modelContext which configuration it should use or is this done automatically? I'm a little lost here and hope someone can help me. Best regards, Sven
5
0
1.8k
Feb ’24
Can't query for the existence of an optional to-one relationship?
Hi, say in my model I have members and each member optionally can have a relationship to a Club. So the relationship in the Member entity would be modelled like so: @Relationship(.nullify, inverse: \Club.members) var club: Club? Now I would like to fetch al Members with no Club relationship. I would assume that this would work with a predicate like this: let noClubPred = #Predicate<Member> { member in member.club == nil } Unfortunately this gives me the following error when compiling: Generic parameter 'RHS' could not be inferred. Has anybody an idea how to phrase this predicate correctly, or is this a beta issue and it should actually work? Thank you! Cheers, Michael
2
1
905
Sep ’23
Error in SwiftData migrationPlan execution
I'm testing the new SwiftData to see if I can use it on one of my apps. Specifically I'm trying to learn how to use the schema migrations. I made a new project using Xcode 15 beta 4. I selected to use SwiftData and iCloud. I ran the generated project and I added a couple of items to the screen. Then, I wanted to make a schema migration, so I changed the Item model from: @Model final class Item { var timestamp: Date init(timestamp: Date) { self.timestamp = timestamp } } to also include a string title: @Model final class Item { var timestamp: Date var title: String init(timestamp: Date, title: String) { self.timestamp = timestamp self.title = title } } I also made some changes to the SwiftUI code to include an empty string when adding a new item, so it could compile: let newItem = Item(timestamp: Date(), title: "") And then I added a new file containing the migration, according to what I saw on the video sessions: // // SwiftDataMigrations.swift // Test 1 // // Created by Diego on 12/07/23. // import Foundation import SwiftData enum ItemSchemaV1: VersionedSchema { static var versionIdentifier: String? static var models: [any PersistentModel.Type] { [Item.self] } @Model final class Item { var timestamp: Date init(timestamp: Date) { self.timestamp = timestamp } } } enum ItemSchemaV2: VersionedSchema { static var versionIdentifier: String? static var models: [any PersistentModel.Type] { [Item.self] } @Model final class Item { var timestamp: Date var title: String init(timestamp: Date, title: String) { self.timestamp = timestamp self.title = title } } } enum ItemsMigrationPlan: SchemaMigrationPlan { static var schemas: [any VersionedSchema.Type] { [ItemSchemaV1.self, ItemSchemaV2.self] } static var stages: [MigrationStage] { [migrateV1toV2] } static let migrateV1toV2 = MigrationStage.lightweight(fromVersion: ItemSchemaV1.self, toVersion: ItemSchemaV2.self) } After that, I specified the migrationPlan in the app: // // Test_1App.swift // Test 1 // // Created by Diego on 12/07/23. // import SwiftUI import SwiftData @main struct Test_1App: App { var container = try! ModelContainer( for: Item.self, migrationPlan: ItemsMigrationPlan.self ) var body: some Scene { WindowGroup { ContentView() } .modelContainer(container) } } And then I tried to run the app but it crashed on launch with this error: SwiftData/ModelContext.swift:179: Fatal error: Container does not have any data stores Does anyone have any idea about what I might be missing? The only thing that didn't matched the videos was that when creating a ModelContainer, the code on the video showed var container = ModelContainer... but the compiler showed the error Call can throw, but errors cannot be thrown out of a property initializer, so I added the try!. Also, on the VersionedSchema, I had to add static var versionIdentifier: String?. Other than that I have no idea. Thanks in advance.
5
1
1.3k
Sep ’23
Inserting a Model entity with a relationship results in a runtime error.
Hi, when inserting an entity with a relationship I get the following runtime error: Illegal attempt to establish a relationship 'group' between objects in different contexts [...]. The model looks like this: @Model class Person { var name: String @Relationship(.nullify, inverse: \Group.members) var group: Group init(name: String) { self.name = name } } @Model class Group { var name: String @Relationship(.cascade) public var members: [Person] init(name: String) { self.name = name } } It can be reproduced using this (contrived) bit of code: let group = Group(name: "Group A") ctx.insert(group) try! ctx.save() let descriptor = FetchDescriptor<Group>() let groups = try ctx.fetch(descriptor) XCTAssertFalse(groups.isEmpty) XCTAssertEqual(groups.count, 1) XCTAssertTrue(groups.first?.name == "Group A") let person = Person(name: "Willy") person.group = group ctx.insert(person) try ctx.save() (See also full test case below). Anybody experiencing similar issues? Bug or feature? Cheers, Michael Full test case: import SwiftData import SwiftUI import XCTest // MARK: - Person - @Model class Person { var name: String @Relationship(.nullify, inverse: \Group.members) var group: Group init(name: String) { self.name = name } } // MARK: - Group - @Model class Group { var name: String @Relationship(.cascade) public var members: [Person] init(name: String) { self.name = name } } // MARK: - SD_PrototypingTests - final class SD_PrototypingTests: XCTestCase { var container: ModelContainer! var ctx: ModelContext! override func setUpWithError() throws { let fullSchema = Schema([Person.self, Group.self,]) let dbCfg = ModelConfiguration(schema: fullSchema) container = try ModelContainer(for: fullSchema, dbCfg) ctx = ModelContext(container) _ = try ctx.delete(model: Group.self) _ = try ctx.delete(model: Person.self) } override func tearDownWithError() throws { guard let dbURL = container.configurations.first?.url else { XCTFail("Could not find db URL") return } do { try FileManager.default.removeItem(at: dbURL) } catch { XCTFail("Could not delete db: \(error)") } } func testRelAssignemnt_FB12363892() throws { let group = Group(name: "Group A") ctx.insert(group) try! ctx.save() let descriptor = FetchDescriptor<Group>() let groups = try ctx.fetch(descriptor) XCTAssertFalse(groups.isEmpty) XCTAssertEqual(groups.count, 1) XCTAssertTrue(groups.first?.name == "Group A") let person = Person(name: "Willy") person.group = group ctx.insert(person) try ctx.save() } }
4
3
1.8k
Aug ’23
Do I need to add my own unique id?
Hi, if I have a @Model class there's always an id: PersistentIdentifier.ID underneath which, according to the current documentation "The value that uniquely identifies the associated model within the containing store.". So I am wondering if it is (good) enough to rely on this attribute to uniquely identify @Model class entities, or if there are edge cases where it does not work (like maybe when using CloudKit)? If anybody saw some information regarding this, please let me know :-) Cheers, Michael
4
3
1.2k
Aug ’23
Can't test equality of two model entities with a predicate
Hi, given this model: @Model class OnlyName { var name: String init(name: String) { self.name = name } } I would assume that I could write a predicate like this: #Predicate<OnlyName> { $0.name == other.name }, where other is also an instance of OnlyName for example returned by an earlier fetch. Unfortunately this results in the following compiler errors: Initializer 'init(_:)' requires that 'OnlyName' conform to 'Encodable' Initializer 'init(_:)' requires that 'OnlyName' conform to 'Decodable' Any idea if this is a bug in SwiftData or if I am missing something? Cheers, Michael
2
0
856
Jun ’23
Relationships are not persisted unless there is an inverse?
Hi, I encountered the issue, that unless an inverse relationship is modelled, the relationship is not persisted. This can be reproduced with the sample code below: Press the "Add Person" button twice Then press the "Add group" button You now can see that the group has to member, but once you restart the app the members a gone. Once an inverse relationship is added (see commented code) the relationships are persisted. Any idea if this is intended behaviour? import SwiftData import SwiftUI // MARK: - Person - @Model class Person { var name: String // uncomment to make it work @Relationship(.nullify) var group: Group? init(name: String) { self.name = name } } // MARK: - Group - @Model class Group { var name: String // uncomment to make it work @Relationship(.nullify, inverse: \Person.group) public var members: [Person] @Relationship(.nullify) public var members: [Person] // comment to make it work init(name: String) { self.name = name } } // MARK: - SD_PrototypingApp - @main struct SD_PrototypingApp: App { var body: some Scene { WindowGroup { ContentView() } .modelContainer(for: [Person.self, Group.self]) } } // MARK: - ContentView - struct ContentView: View { @Environment(\.modelContext) private var modelContext @Query private var groups: [Group] @Query private var persons: [Person] var body: some View { VStack { ForEach(groups) { group in Text("\(group.name): \(group.members.count)") } ForEach(persons) { person in Text("Person: \(person.name)") } Button { assert(persons.isEmpty == false) if groups.isEmpty { let group = Group(name: "Group A") group.members = persons modelContext.insert(group) try! modelContext.save() } } label: { Text("Add a group") } .disabled(!groups.isEmpty || persons.isEmpty) Button { let person = Person(name: "Person \(Int.random(in: 0 ... 1_000_000))") modelContext.insert(person) } label: { Text("Add Person") } } } }
0
1
1k
Jun ’23
SwiftData Query relationships not working
Overview I have 2 models: Deparment and Student Each Department can contain multiple students Each Student can only be in one Department I have DepartmentList, tapping on the department should take it to the StudentList which lists all students in the department Problem When I use Query in StudentList to filter only students for a specific department id, no students are shown. Questions: What should I do to list the students in a department? (see complete code below). let filter = #Predicate<Student> { student in student.department?.id == departmentID } let query = Query(filter: filter, sort: \.name) _students = query Complete code App @main struct SchoolApp: App { var body: some Scene { WindowGroup { ContentView() } .modelContainer(for: [Department.self, Student.self]) } } Department import Foundation import SwiftData @Model class Department { var id: UUID var name: String var students: [Student] init( id: UUID, name: String, students: [Student] = [] ) { self.id = id self.name = name self.students = students } } Student import Foundation import SwiftData @Model class Student { var id: UUID var name: String @Relationship(inverse: \Department.students) var department: Department? init( id: UUID, name: String, department: Department? = nil ) { self.id = id self.name = name self.department = department } } ContentView import SwiftUI struct ContentView: View { @State private var selectedDepartment: Department? var body: some View { NavigationSplitView { DepartmentList(selectedDepartment: $selectedDepartment) } detail: { if let department = selectedDepartment { StudentList(department: department) } else { Text("no department selected") } } .task { printStoreFilePath() } } private func printStoreFilePath() { let urls = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask) if let path = urls.map({ $0.path(percentEncoded: false) }).first { print("Storage: \(path)") } } } DepartmentList import SwiftUI import SwiftData struct DepartmentList: View { @Binding var selectedDepartment: Department? @Query(sort: \.name) private var departments: [Department] @Environment(\.modelContext) private var modelContext var body: some View { List(selection: $selectedDepartment) { ForEach(departments) { department in NavigationLink(value: department) { Text(department.name) } } } .toolbar { ToolbarItem { Button { addDepartment() } label: { Label("Add", systemImage: "plus") } } } } private func addDepartment() { guard let index = (1000..<10000).randomElement() else { return } let department = Department(id: UUID(), name: "Department \(index)") modelContext.insert(department) } } StudentList import SwiftUI import SwiftData struct StudentList: View { var department: Department @Query private var students: [Student] @Environment(\.modelContext) private var modelContext init(department: Department) { self.department = department let departmentID = department.id let filter = #Predicate<Student> { student in student.department?.id == departmentID } let query = Query(filter: filter, sort: \.name) _students = query } var body: some View { List { ForEach(students) { student in Text(student.name) } } .toolbar { ToolbarItem { Button { addStudent() } label: { Label("Add", systemImage: "plus") } } } } private func addStudent() { guard let index = (1000..<10000).randomElement() else { return } let student = Student( id: UUID(), name: "Student \(index)", department: department ) modelContext.insert(student) } }
10
4
2.8k
Jul ’23
[SwiftData]Why did not I receive willMigrate and didMigrate callbacks during the schemaMigrationPlan
I used Schema and MigrationPlan to initialize the Model Container. SwiftDataDemoApp.swift Then I modified the Schema and created a MigrationStage through the custom method, which was configured in the stages array of the SchemaMigrationPlan. Models.swift But when I run the app, I don't receive the willMigrate and didMigrate callbacks Xcode Version: 15.0 beta (15A5160n) MacOS: 13.3.1 (a) (22E772610a)
2
0
666
Jun ’23
How to create a ModelContainer with [PersistentModel] and SchemaMigrationPlan
I can't find the method to create ModelContainer that appears in the video at 8: 52. The code in the video is as follows: let container = ModelContainer( for: Trip.self, migrationPlan: SampleTripsMigrationPlan.self ) This method doesn't seem to exist in Xcode15. I found other method to create ModelContainer using Schema and Schema MigrationPlan in Xcode15. public convenience init(for givenSchema: Schema, migrationPlan: (SchemaMigrationPlan.Type)? = nil, _ configurations: ModelConfiguration...) throws And I tried to create a Schema to use this method, like this: let container = try! ModelContainer(for: .init([Person.self]), migrationPlan: MigrationPlan.self) But an error occurred during runtime SwiftData/ModelContext.swift:177: Fatal error: Container does not have any data stores SwiftDataDemoApp.swift How to create a ModelContainer with [PersistentModel] and SchemaMigrationPlan ? Xcode Version: 15.0 beta (15A5160n) MacOS: 13.3.1 (a) (22E772610a)
1
2
983
Jun ’23
@Attribute 'unique' and complex keys
The 'unique' attribute is a really nice feature, BUT. In some of my apps, the unique identifier for an object is a combination of multiple attributes. (Example: a book title is not unique, but a combination of book title and author list is.) How do I model this with SwiftData? I cannot use @Attribute(.unique) on either the title OR the author list, but I want SwiftData to provide the same "insert or update" logic. Is this possible?
3
4
1.5k
Jun ’23
No "upsert" when working with .unique attributes
Hi, in the session the following is mentioned: If a trip already exists with that name, then the persistent back end will update to the latest values. This is called an upsert. An upsert starts as an insert. If the insert collides with existing data, it becomes an update and updates the properties of the existing data. Nevertheless, if I have a unique constraint on an (String) attribute and try to insert the same again, I end up in the debugger in the generated getter of the attribute: @Attribute(.unique) public var name: String { get { _$observationRegistrar.access(self, keyPath: \.name) return self.getValue(for: \.name) // <- here } EXC_BREAKPOINT (code=1, subcode=0x1a8d6b724) Am I missing something? If this is expected behaviour, how should I prevent this crash (other than checking for uniqueness before every insert)? Thank you! Cheers, Michael
4
5
1.8k
Aug ’23
SwiftData Configurations for Private and Public CloudKit
I did manage to save my Entities to CloudKit with SwiftData but the default database is the private database. I need to store some Entities in the private and other Entities in the public CloudKit database. How do I manage that with SwiftData? With CoreData I always used different configurations for both private and public and added the entities to one or the other.
5
4
3.6k
Nov ’23