0

My ForEach in a Scrollview does not get updated when CommentViewModel comments get updated. It gets successfully updated, but for some reason, CommentView does not get updated. I have tried everything, but can't seem to find a solution. Maybe Comment should become a Hashable or Codable. But I can't quite make this work. I also tried removing the chance of Scrollview being empty, by adding an if statement or empty Text. But this was not the problem. Any help would be helpfull.

//These are the updated View

struct CommentView: View {
    @StateObject var commentViewModel = CommentViewModel()
    static let emptyScrollToString = "emptyScrollToString"
    @State var commentCommentUser = ""
    @State var showCommentComment = false
    @State var post: Post
    
    init(_ post: Post) {
        self.post = post
    }
    
    var body: some View {
        
        VStack {
            commentView
            
            Divider()
            
            if showCommentComment {
                HStack {
                    Text("Svarer \(commentCommentUser)")
                        .foregroundColor(.black)
                        .font(.system(size: 16))
                        .opacity(0.3)
                    
                    Spacer()
                    
                    Button {
                        withAnimation(Animation.spring().speed(2)) {
                            showCommentComment.toggle()
                        }
                    } label: {
                        Text("x")
                            .font(.system(size: 16))
                            .foregroundColor(.black)
                    }

                }
                .padding()
                .background(Color(r: 237, g: 237, b: 237))
            }
            
            BottomBar(post: post)
                .frame(minHeight: 50,maxHeight: 180)
                .fixedSize(horizontal: false, vertical: true)
                .shadow(radius: 60)
            
                .navigationBarTitle("Kommentar", displayMode: .inline)
        }
        .onAppear() {
            UINavigationBar.appearance().tintColor = UIColor(red: 20/255, green: 147/255, blue: 2/255, alpha: 1)
            commentViewModel.fetchComments(post: post)
        }
    }
    
    private var commentView: some View {
        ScrollView {
            ScrollViewReader { scrollViewProxy in
                VStack {
                    HStack{ Spacer() }
                        .id(Self.emptyScrollToString)
                    
                    ForEach(commentViewModel.comments, id: \.id) { comment in 
                        CommentCell(post: post, comment: comment, commentCommentUser: $commentCommentUser, showCommentComment: $showCommentComment)
                    }
                }
                .onReceive(Just(commentViewModel.comments.count)) { _ in // <-- here
                                    withAnimation(.easeOut(duration: 0.5)) {
                                        print("Scroll to top")
                                        scrollViewProxy.scrollTo(Self.emptyScrollToString, anchor: .bottom)
                                    }
                                }
            }
        }
    }
    
    public func uploadData(commentText: String) {
        guard let uid = FirebaseManager.shared.auth.currentUser?.uid else {return}
        guard let id = post.id else {return}
        
        let data = ["fromId":uid, "commentText":commentText, "likes":0, "timestamp": Timestamp()] as [String : Any]
        
        FirebaseManager.shared.firestore.collection("posts").document(id).collection("comments")
            .document().setData(data) { error in
                if error != nil {
                    print("failed to post comment", error ?? "")
                    return
                }
                
                print("Update")
                commentViewModel.fetchComments(post: post) //Gets error here
            }
    }
    
}

struct BottomBar: View {
    var commentView: CommentView
    
    init(post: Post) {
        self.commentView = CommentView(post)
    }
    
    var body: some View {
        bottomBar
    }
    
    private var bottomBar: some View {
            HStack{
                TextEditorView(string: $commentText)
                    .overlay(RoundedRectangle(cornerRadius: 12)
                        .stroke(lineWidth: 1)
                        .opacity(0.5))
                
                VStack {
                    Spacer()
                    Button {
                        commentView.uploadData() // This also reset all @State variables in Commentview, for some reason
                        commentText = ""
                    } label: {
                        Text("Slå op")
                            .font(.system(size: 20, weight: .semibold))
                            .opacity(commentText.isEmpty ? 0.5 : 1)
                            .foregroundColor(Color(r: 20, g: 147, b: 2))
                    }
                    .padding(.bottom, 10)
                }

            }
            .padding()
    }
}

struct Comment: Identifiable, Decodable {
    @DocumentID var id: String?
    let commentText: String
    let fromId: String
    var likes: Int
    let timestamp: Timestamp

    var user: PostUser?
    var didLike: Bool? = false
}

class CommentViewModel: ObservableObject {
    @Published var comments = [Comment]()
    @Published var count = 0
    let service: CommentService
    let userService = UserService()

    init(post: Post) {
        self.service = CommentService(post: post)
        fetchComments()
    }

    func fetchComments() {
        service.fetchComments { comments in
            self.comments = comments
            self.count = self.comments.count

            for i in 0 ..< comments.count {
                let uid = comments[i].fromId

                self.userService.fetchUser(withUid: uid) { user in
                    self.comments[i].user = user
                }
            }
        }
    }
}

struct CommentService {
    let post: Post

    func fetchComments(completion: @escaping([Comment]) -> Void) {
        guard let id = post.id else {return}

        FirebaseManager.shared.firestore.collection("posts").document(id).collection("comments")
            .order(by: "timestamp", descending: true)
            .getDocuments { snapshot, error in
                if error != nil {
                    print("failed fetching comments", error ?? "")
                    return
                }

                guard let docs = snapshot?.documents else {return}
                do {
                    let comments = try docs.compactMap({ try  $0.data(as: Comment.self) })
                    print("COmplete")
                    completion(comments)
                }
                catch {
                    print("failed")
                }

            }
    }
 }

This is the old views

struct Comment: Identifiable, Decodable {
    @DocumentID var id: String?
    let commentText: String
    let fromId: String
    var likes: Int
    let timestamp: Timestamp
    
    var user: PostUser?
    var didLike: Bool? = false
}

class CommentViewModel: ObservableObject {
    @Published var comments = [Comment]()
    @Published var count = 0
    let service: CommentService
    let userService = UserService()
    
    init(post: Post) {
        self.service = CommentService(post: post)
        fetchComments()
    }
    
    func fetchComments() {
        service.fetchComments { comments in
            self.comments = comments
            self.count = self.comments.count
            
            for i in 0 ..< comments.count {
                let uid = comments[i].fromId
                
                self.userService.fetchUser(withUid: uid) { user in
                    self.comments[i].user = user
                }
            }
        }
    }
}

struct CommentService {
    let post: Post
    
    func fetchComments(completion: @escaping([Comment]) -> Void) {
        guard let id = post.id else {return}
        
        FirebaseManager.shared.firestore.collection("posts").document(id).collection("comments")
            .order(by: "timestamp", descending: true)
            .getDocuments { snapshot, error in
                if error != nil {
                    print("failed fetching comments", error ?? "")
                    return
                }
                
                guard let docs = snapshot?.documents else {return}
                do {
                    let comments = try docs.compactMap({ try  $0.data(as: Comment.self) })
                    print("COmplete")
                    completion(comments)
                }
                catch {
                    print("failed")
                }

            }
    }
 }
 
 struct CommentView: View {
    @ObservedObject var commentViewModel: CommentViewModel
    static let emptyScrollToString = "emptyScrollToString"
    
    init(post: Post) {
        commentViewModel = CommentViewModel(post: post)
    }
    
    var body: some View {
        
        VStack {
            commentView
            Divider()
            BottomBar(post: commentViewModel.service.post)
                .frame(minHeight: 50,maxHeight: 180)
                .fixedSize(horizontal: false, vertical: true)
                .shadow(radius: 60)
            
                .navigationBarTitle("Kommentar", displayMode: .inline)
        }
        .onAppear() {
            UINavigationBar.appearance().tintColor = UIColor(red: 20/255, green: 147/255, blue: 2/255, alpha: 1)
        }
    }
    
    private var commentView: some View {
        ScrollView {
            ScrollViewReader { scrollViewProxy in
                VStack {
                    HStack{ Spacer() }
                        .id(Self.emptyScrollToString)
                    
                    ForEach(commentViewModel.comments, id: \.id) { comment in // Here should it update
                        let _ = print("Reload")
                        CommentCell(post: commentViewModel.service.post, comment: comment)
                    }
                }
                .onReceive(commentViewModel.$count) { _ in // It doesn't update here either
                            withAnimation(.easeOut(duration: 0.5)) {
                                print("Scroll to top")
                                scrollViewProxy.scrollTo(Self.emptyScrollToString, anchor: .bottom)
                            }
                        }
            }
        }
    }
 }

2 Answers2

1

@StateObject property wrapper own the object you created, so it will keep alive once View updated by any changes.

@ObservedObject property wrapper doesn't own the object you created, so it will recreated on View update by any changes, in this way property observer get lost and will not be able to receive changes.

So, changing your ViewModel from @ObservedObject to @StateObject will fix the issue.

Umair Khan
  • 993
  • 8
  • 14
  • This doesn't work. The view still doesn't get updated, even tho I changed it to @StateObject Umair Khan – Alexander Rubino Jun 14 '22 at 13:32
  • Umair Khan I have made these changes: @StateObject var commentViewModel: CommentViewModel init(_ post: Post) { _commentViewModel = StateObject(wrappedValue: CommentViewModel(post: post)) } – Alexander Rubino Jun 14 '22 at 13:33
0

EDIT-1: Taking your new code into consideration.

Note, it is important to have a good grip on the SwiftUI basics, especially how to use and pass ObservableObject and how to use Views. I suggest you do the tutorial again.

I have attempted to modify your code to give you an idea on how you could re-structure it. Pay atttention to the details, hope it helps.

Note, I have commented a number of lines, because I do not have Firebase and your other code, such as UserService etc... Adjust my code to suit your needs, and uncomment the relevant lines.

import Foundation
import SwiftUI
import Combine

struct BottomBar: View {
    @ObservedObject var viewModel: CommentViewModel  // <-- here
    @State var post: Post
    @State var commentText = ""
    
    var body: some View {
        HStack {
            // TextEditorView(string: $commentText)
            TextEditor(text: $commentText) // <-- for testing
                .overlay(RoundedRectangle(cornerRadius: 12)
                    .stroke(lineWidth: 1)
                    .opacity(0.5))
            
            VStack {
                Spacer()
                Button {
                    // -- here
                    viewModel.uploadData(post: post, commentText: commentText)
                    commentText = ""
                } label: {
                    Text("Slå op")
                        .font(.system(size: 20, weight: .semibold))
                        .opacity(commentText.isEmpty ? 0.5 : 1)
                }
                .padding(.bottom, 10)
            }
        }
    }
}

struct CommentView: View {
    @StateObject var commentViewModel = CommentViewModel()
    
    static let emptyScrollToString = "emptyScrollToString"
    
    @State var commentCommentUser = ""
    @State var showCommentComment = false
    @State var post: Post
    
    var body: some View {
        VStack {
            commentView
            Divider()
            if showCommentComment {
                HStack {
                    Text("Svarer \(commentCommentUser)")
                        .foregroundColor(.black)
                        .font(.system(size: 16))
                        .opacity(0.3)
                    Spacer()
                    Button {
                        withAnimation(Animation.spring().speed(2)) {
                            showCommentComment.toggle()
                        }
                    } label: {
                        Text("x")
                            .font(.system(size: 16))
                            .foregroundColor(.black)
                    }
                }.padding()
            }
            BottomBar(viewModel: commentViewModel, post: post)
                .frame(minHeight: 50,maxHeight: 180)
                .fixedSize(horizontal: false, vertical: true)
                .shadow(radius: 60)
                .navigationBarTitle("Kommentar", displayMode: .inline)
        }
        .onAppear() {
            UINavigationBar.appearance().tintColor = UIColor(red: 20/255, green: 147/255, blue: 2/255, alpha: 1)
            commentViewModel.fetchComments(post: post)
        }
    }
    
    private var commentView: some View {
        ScrollView {
            ScrollViewReader { scrollViewProxy in
                VStack {
                    Spacer()
                    ForEach(commentViewModel.comments, id: \.id) { comment in
                        CommentCell(post: post, comment: comment, commentCommentUser: $commentCommentUser, showCommentComment: $showCommentComment)
                    }
                }
                .onReceive(Just(commentViewModel.comments.count)) { _ in // <-- here
                    withAnimation(.easeOut(duration: 0.5)) {
                        print("Scroll to top")
                        scrollViewProxy.scrollTo(Self.emptyScrollToString, anchor: .bottom)
                    }
                }
            }
        }
    }
    
}

// for testing
struct CommentCell: View {
    @State var post: Post
    @State var comment: Comment
    @Binding var commentCommentUser: String
    @Binding var showCommentComment: Bool
    
    var body: some View {
        Text(comment.commentText)  // for testing
    }
}

struct Comment: Identifiable, Decodable {
    //    @DocumentID
    var id: String?  // for testing
    let commentText: String
    let fromId: String
    var likes: Int
    //    let timestamp: Timestamp  // for testing
    
    var user: PostUser?
    var didLike: Bool? = false
}

class CommentViewModel: ObservableObject {
    @Published var comments = [Comment]()
    
    let service = CommentService()  // <-- here
    // let userService = UserService()  // for testing
    
    init() { } // <-- here
    
    func fetchComments(post: Post) { // <-- here
        service.fetchComments(post: post) { comments in
            self.comments = comments
            for i in 0 ..< comments.count {
                let uid = comments[i].fromId
                //                self.userService.fetchUser(withUid: uid) { user in
                //                    self.comments[i].user = user
                //                }
            }
        }
    }
    
    func uploadData(post: Post, commentText: String) {
        service.uploadData(post: post, commentText: commentText) { isGood in
            if isGood {
                self.fetchComments(post: post)
            }
        }
    }
    
}

struct CommentService {

    func fetchComments(post: Post, completion: @escaping([Comment]) -> Void) {
        guard let id = post.id else {completion([]); return}  // <-- here
        // FirebaseManager.shared.firestore.collection("posts").document(id).collection("comments")
        //            .order(by: "timestamp", descending: true)
        //            .getDocuments { snapshot, error in
        //                if error != nil {
        //                    print("failed fetching comments", error ?? "")
        //                    return
        //                }
        //                guard let docs = snapshot?.documents else {return}
        //                do {
        //                    let comments = try docs.compactMap({ try  $0.data(as: Comment.self) })
        //                    print("COmplete")
        //                    completion(comments)
        //                }
        //                catch {
        //                    print("failed")
        //                    completion([])
        //                }
        //            }
    }
    
    func uploadData(post: Post, commentText: String, completion: @escaping(Bool) -> Void) {
        
        completion(true)  // for testing, to be removed
        
        //        guard let uid = FirebaseManager.shared.auth.currentUser?.uid else {completion(false); return}  // <--- here
        //        guard let id = post.id else {completion(false); return}  // <--- here
        //
        //        let data = ["fromId":uid, "commentText":commentText, "likes":0, "timestamp": Timestamp()] as [String : Any]
        //   FirebaseManager.shared.firestore.collection("posts").document(id).collection("comments")
        //            .document().setData(data) { error in
        //                if error != nil {
        //                    print("failed to post comment", error ?? "")
        //                    completion(false) // <--- here
        //                    return
        //                }
        //                print("Update")
        //                completion(true)  // <--- here
        //            }
    }
    
}

// for testing
struct PostUser: Identifiable, Decodable {
    var id: String?
}

// for testing
struct Post: Identifiable, Decodable {
    var id: String?
    var name = "something"
}

EDIT-2: typo fix.

in BottomBar changed viewModel.service.uploadData(post: post, commentText: commentText) {_ in}
to viewModel.uploadData(post: post, commentText: commentText)

  • workingdog support Ukraine - I have followed your code, and I have updated my post, so you can see my new CommentView. However I get an error in a function, that is called from another view. The error is: "Accessing StateObject's object without being installed on a View. This will create a new instance each time." I can't figure out how to fix this, any help? – Alexander Rubino Jun 15 '22 at 18:18
  • could you update your post and show **all** the code you are using now, especially the code of the "other" view, where you have `...I get an error in a function, that is called from another view. ..`. The error suggests you did not pass the `CommentViewModel` to that view. – workingdog support Ukraine Jun 15 '22 at 20:48
  • workingdog support Ukraine - I have now updated the post – Alexander Rubino Jun 16 '22 at 20:24
  • updated my answer taking into account your new code. I suggest you read again the tutorial at: https://developer.apple.com/tutorials/swiftui/ – workingdog support Ukraine Jun 17 '22 at 00:42
  • workingdog support Ukraine - So you don't have a solution to the problem? – Alexander Rubino Jun 17 '22 at 20:39
  • I have provided a solution in my answer that works well for me in my tests. Since there are a number of missing parts, without a minimal, reproducible example (stackoverflow.com/help/minimal-reproducible-example) it is difficult to fully test your code. Your code includes for example, remote `Firebase` database that is not available to me (or anyone but you). Have you tried my code **as presented** (uncommenting the appropriate lines), what errors do you get and where. Are there problems getting the data using `FirebaseManager `? – workingdog support Ukraine Jun 17 '22 at 23:18
  • updated my answer with a typo fix, see EDIT-2, sorry. – workingdog support Ukraine Jun 18 '22 at 00:02