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)
    }
}

Accepted Reply

This bug has been fixed in Xcode 15.0 beta 2 (15A5161b).

Replies

How about:

            ForEach(department.students) { student in
                Text(student.name)
            }

You need to add @Relationship to var students: [Student] in Department model, also:

import Foundation
import SwiftData

@Model
class Department {
    ...  
    @Relationship var students: [Student]
   ...
}
  • In Under the Radar - WWDC2023 at 24:37 Josh Shaffer says "you don't even have to write @Relationship, we detect the relationships"

Add a Comment

@albert.tra Thanks, I have added @Relationship to var students in Department but doesn't work.

I have pasted the complete code, you could also check at your end.

I have tried it on Simulator for iPhone 14 Pro

@Purkylin_glow The problem with using department.students is that when the + button is tapped to add new students, it doesn't show up in the student list unless you go back to the department list and come back to Student list screen.

New changes to department.students are not observed

@Purkylin_glow @albert.tra

Not sure if this is a good solution, but the following seems to work (it has downsides):

        List {
            ForEach(students.filter { $0.department?.id == department.id }) { student in
                Text(student.name)
            }
        }

The downside is@Query would monitor changes and refresh the view even if a student is added to a totally different department.

I really wish there is a better solution ...

Seems it's a known issue:

"@Query results can appear stale or fail to refresh. (108385553)"

https://developer.apple.com/documentation/ios-ipados-release-notes/ios-ipados-17-release-notes

Post not yet marked as solved Up vote reply of malc Down vote reply of malc

Thanks a lot @malc for pointing out the release notes.

I have also filed a feedback FB12335970 just in case it helps.

This bug has been fixed in Xcode 15.0 beta 2 (15A5161b).

The problem is with your predicate.

let filter = #Predicate<Student> { student in
    student.department?.id == departmentID // <- here
}

You are comparing two UUID properties and, according to the beta 2 release notes, it doesn't like this.

SwiftData

Known Issues

• #Predicate does not support UUID, Date, and URL properties. (109539652)

@BabyJ This code works fine in Xcode 5 beta 2. This was only an issue in Xcode 5 beta 1. I suppose you meant Xcode 5 Beta 1

  • Do you mean Xcode 15?

  • Yeah meant Xcode 15, sorry my bad.

Add a Comment