Model your schema with SwiftData

RSS for tag

Discuss the WWDC23 Session Model your schema with SwiftData

View Session

Posts under wwdc2023-10195 tag

31 Posts
Sort by:
Post not yet marked as solved
0 Replies
443 Views
Hello, the way I understand the schema migrations in conjunction with the originalName modifier, is that I will always need this property rapper for future migrations of my app. Is that correct? @Attribute(originalName: "start_date") var startDate: Date @Attribute(originalName: "end_date") var endDate: Date Let's say I do a V4 migration and do another change (irrelevant in this context; can be anything). Do startDate and endDate each need @Attribute(originalName: <#String>) or has SwiftData already renamed those attributes in the store and therefore I wouldn't need this anymore? Thank you! Denis
Posted
by
Post not yet marked as solved
3 Replies
1.5k Views
What is the best way to use swift data to store one off data models? For example an application state that you want to be persisted across application launches. The only examples I've seen use arrays of items which wouldn't work for having just one application state. Is it possible to query just one item?
Posted
by
Post not yet marked as solved
1 Replies
709 Views
I am watching Swiftdata videos, but so far they haven't mentioned CloudKit explicitly, and I'm not sure what is the relationship between SwiftData and Cloudkit at this time. Are these models automatically synced with Cloudkit? If that's the case, do the migration plans work for cloudkit as well?
Posted
by
Post not yet marked as solved
0 Replies
337 Views
I understand that in the demo we have version 1 to 3 in the schema plan, but where does the current version live, the one with relationship decorations? And how is the current version's migration type declared or handled?
Posted
by
Post marked as solved
5 Replies
3.4k Views
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.
Posted
by
Post not yet marked as solved
4 Replies
1.7k Views
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
Posted
by
Post not yet marked as solved
3 Replies
1.4k Views
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?
Posted
by
Post not yet marked as solved
1 Replies
958 Views
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)
Posted
by
Post not yet marked as solved
2 Replies
643 Views
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)
Posted
by
Post marked as solved
10 Replies
2.7k Views
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) } }
Posted
by
Post not yet marked as solved
5 Replies
1.8k Views
If I use a Codable enum as the type in an array property on a Model, I get a crash. The message is something like: Illegal attempt to use a Optional(Swift.Mirror.DisplayStyle.enum) as a Property Minor change code example from a fresh project using SwiftData: @Model final class Item { var timestamp: Date var choices: [Choice] init(timestamp: Date, choices: [Choice]) { self.timestamp = timestamp self.choices = choices } } enum Choice: String, Codable { case beep, bloop } ContentView (largely unchanged): import SwiftUI import SwiftData struct ContentView: View { @Environment(\.modelContext) private var modelContext @Query private var items: [Item] var body: some View { NavigationView { List { ForEach(items) { item in NavigationLink { VStack { Text("Item at \(item.timestamp, format: Date.FormatStyle(date: .numeric, time: .standard))") Text("\(item.choices.map({"\($0.rawValue)"}).joined())") } } label: { Text(item.timestamp, format: Date.FormatStyle(date: .numeric, time: .standard)) } } .onDelete(perform: deleteItems) } .toolbar { #if os(iOS) ToolbarItem(placement: .navigationBarTrailing) { EditButton() } #endif ToolbarItem { Button(action: addItem) { Label("Add Item", systemImage: "plus") } } } Text("Select an item") } } private func addItem() { withAnimation { let newItem = Item(timestamp: Date(), choices: [.bloop, .beep]) modelContext.insert(newItem) } } private func deleteItems(offsets: IndexSet) { withAnimation { for index in offsets { modelContext.delete(items[index]) } } } } #Preview { ContentView() .modelContainer(for: Item.self, inMemory: true) } This is strange and seems like a bug as if I use Codable structs in this instance it is fine. Yet, if that Codable struct has an enum as a property, it also crashes with a different error. Working: import Foundation import SwiftData @Model final class Item { var timestamp: Date var choices: [Choice] init(timestamp: Date, choices: [Choice]) { self.timestamp = timestamp self.choices = choices } } struct Choice: Codable { let foo: String } Also doesn't work: import Foundation import SwiftData @Model final class Item { var timestamp: Date var choices: [Choice] init(timestamp: Date, choices: [Choice]) { self.timestamp = timestamp self.choices = choices } } struct Choice: Codable { enum SpecificChoice: String, Codable { case beep, bloop } let foo: SpecificChoice } It seems really odd and arbitrary that an enum would cause these issues. There may be other cases involving enums, but to wrap up it seems enums as properties of Codables that live within an array on a SwiftData model, or an array of enums on a SwiftData model cause crashes.
Posted
by
Post not yet marked as solved
0 Replies
1k Views
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") } } } }
Posted
by
Post not yet marked as solved
0 Replies
849 Views
I'm stuck at an error EXC_BREAKPOINT (code=1, subcode=0x1a8d69a38)that is thrown in a class during initialization. The class is defined as: @Model public final class Parent { @Attribute(.unique) public var uuid: UUID /// The date specification. @Relationship(.cascade) public var dateSpec: DateSpec /// The title. public var title: Title /// The subtitle. public var subTitle: Subtitle public init(uuid: UUID, dateSpec: DateSpec, title: Title, subTitle: Subtitle) { self.uuid = uuid self.dateSpec = dateSpec self.title = title self.subTitle = subTitle } } The error is thrown in the var dateSpec property at the return self.getValue(for: \.dateSpec) call of the @PersistedProperty macro. DateSpec is defined this way: @Model public final class DateSpec { @Attribute(.unique) public var uuid: UUID /// The type of the date specification (`.point` or `.range`). public var type: DateSpecType @Relationship(.cascade) public var point: DatePoint @Relationship(.cascade) public var range: DateRange public init(uuid: UUID, type: DateSpecType = .none, point: DatePoint = .init(), range: DateRange = .init()) { self.uuid = uuid self.type = type self.point = point self.range = range } } And DatePoint is defined so: @Model public final class DatePoint { @Attribute(.unique) public var uuid: UUID public var format: String public var date: Date? public init(uuid: UUID, format: String, date: Date? = nil) { self.uuid = uuid self.format = format self.date = date } } (DateRange accordingly). So, as far as I understood the sessions, this should work. Or did I miss something? -- Edit: When taking out DatePoint and DateRange from the model, and replacing the properties by .transient wrappers that get/set the respective properties directly in DateSpec, then the error disappears. So, is the problem the cascading of the relationships between Parent and DateSpec, and DateSpec and DatePoint/DateRange?
Posted
by
Post marked as solved
2 Replies
820 Views
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
Posted
by
Post not yet marked as solved
4 Replies
1.1k Views
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
Posted
by
Post not yet marked as solved
4 Replies
1.7k Views
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() } }
Posted
by
Post not yet marked as solved
0 Replies
581 Views
I’m working on an app where I have some sensitive data that I want to encrypt when persisting. It looks like SwiftData has a BackingData protocol but it mentions it's for in-memory only. Is there a way to provide a custom storage for SwiftData so that I can encrypt the data before saving?
Posted
by
Post not yet marked as solved
5 Replies
1.2k Views
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.
Posted
by