Reputation: 755
I have two models like these:
@Model
class Post {
...
@Relationship(inverse: \Comment.post) var comments: [Comment]?
}
@Model
class Comment {
...
var post: Post
}
And in one of my views I use a query like the one below where I show a list of posts and their comments count:
struct PostsList: View {
@Query var posts: [Post]
var body: some View {
List {
ForEach(posts) { post in
PostListItem(post: post)
}
}
}
}
struct PostListItem: View {
@State var post: Post
var body: some View {
Text("\(post.title)")
Text("\(post.comments?.count ?? 0) comments")
}
}
When I insert a new comment with a given post, and then I go back to the view which shows the list of posts and their comments count, the count is not updated for the post I used unless I relaunch the app.
Is there a way to make SwiftData reload a given query, not when the entities returned changed, but when a related one did?
Upvotes: 5
Views: 3716
Reputation: 755
It turns out the issue was simply in the way I pass the single Post
to PostListItem
.
Given that I am not an expert in SwiftUI, the only two way I found until now to solve this issue are the following.
Solution 1: Remove the subview PostListItem
and render each Post
directly in the List.
struct PostsList: View {
@Query var posts: [Post]
var body: some View {
List {
ForEach(posts) { post in
Text("\(post.title)")
Text("\(post.comments?.count ?? 0) comments")
}
}
}
}
Solution 2: Wrap the Post
in a view model annotated with @Observable
and use it as state in PostListItem
.
struct PostsList: View {
@Query var posts: [Post]
var body: some View {
List {
ForEach(posts) { post in
PostListItem(model: PostListItemModel(post: post))
}
}
}
}
@Observable
class PostListItemModel {
var post: Post
init(post: Post) {
self.post = post
}
}
struct PostListItem: View {
var model: PostListItemModel
var body: some View {
Text("\(model.post.title)")
Text("\(model.post.comments?.count ?? 0) comments")
}
}
Upvotes: 6
Reputation: 20068
I tried to recreate your code and here is the implementation.
struct PostListView: View {
let posts: [Post]
var body: some View {
List(posts) { post in
NavigationLink {
PostDetailView(post: post)
} label: {
PostView(post: post)
}
}.navigationTitle("Posts")
}
}
struct PostDetailView: View {
@Environment(\.modelContext) private var context
let post: Post
var body: some View {
VStack {
Text("PostDetailView")
Button("Add Comment") {
// add comment
let comment = Comment(name: post.name + "Comment")
comment.post = post
post.comments.append(comment)
context.insert(comment)
}
} .navigationTitle(post.name)
}
}
One thing I noted that if I just put comment.post = post then go back to the PostListView, the list still updates but it takes a little more time (few milliseconds).
If I set both sides of relationship, meaning
comment.post = post
post.comments.append(comment)
then the list updates instantly.
Upvotes: 3
Reputation: 1240
Building on @Francesco's answer, I experimented with creating a simple property wrapper that gets the job done.
import SwiftUI
@propertyWrapper @Observable class ObservableModel<T> {
var wrappedValue: T
init(wrappedValue: T) {
self.wrappedValue = wrappedValue
}
}
To use (this also works out of the box with SwiftData @Model
classes):
struct PostListItem: View {
@ObservableModel var model: PostListItemModel
var body: some View {
Text("\(model.post.title)")
Text("\(model.post.comments?.count ?? 0) comments")
}
}
Upvotes: 5