Francesco Papagno
Francesco Papagno

Reputation: 755

Child view not refreshing for changes on SwiftData model

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

Answers (3)

Francesco Papagno
Francesco Papagno

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

azamsharp
azamsharp

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

Jason Armstrong
Jason Armstrong

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

Related Questions