SwiftUI List not updating inside pull to refresh view

Viewed 1212

I have a List inside a Custom Pull to refresh view. When the array inside SeeAllViewModel is updated, this list in the view is not being updated. Not only that but the counter is not being updated also. When I put the list outside this CustomScrollView it updates just fine. So I'm guessing there is something wrong with my CustomScrollView. Any idea why this is happening? Also I will provide the code for my ViewModel, just in case.

struct SeeAllView: View {
    @ObservedObject var seeAllViewModel: SeeAllViewModel
    
    var body: some View {
        GeometryReader { geometry in
            VStack {
                Text("\(self.seeAllViewModel.category.items.count)") // updated on refresh
                CustomScrollView(width: geometry.size.width, height: geometry.size.height, viewModel: self.seeAllViewModel) {
                    VStack {
                    Text("\(self.seeAllViewModel.category.items.count)")  // not being updated
                    List {
                        ForEach(self.seeAllViewModel.category.items) { (item: Item) in
                            ItemRowView(itemViewModel: ItemViewModel(item: item))
                        }
                    }
                    .listStyle(GroupedListStyle())
                    .navigationBarTitle(Text(self.seeAllViewModel.category.title.firstCapitalized))
                    }
                }
                Button(action: {
                    self.seeAllViewModel.refresh()
                }) { Text("refresh")
                }
            }
        }
    }
}

CustomScrollView

struct CustomScrollView<Content: View, VM: LoadProtocol> : UIViewRepresentable {
    var width : CGFloat
    var height : CGFloat
    
    let viewModel: VM
    let content: () -> Content
    
    func makeCoordinator() -> Coordinator {
        Coordinator(self, viewModel: viewModel)
    }
    
    func makeUIView(context: Context) -> UIScrollView {
        let control = UIScrollView()
        control.refreshControl = UIRefreshControl()
        control.refreshControl?.addTarget(context.coordinator, action: #selector(Coordinator.handleRefreshControl), for: .valueChanged)
        
        let childView = UIHostingController(rootView: content())
        
        childView.view.frame = CGRect(x: 0, y: 0, width: width, height: height)
        
        control.addSubview(childView.view)
        return control
    }
    
    func updateUIView(_ uiView: UIScrollView, context: Context) { }
    
    class Coordinator: NSObject {
        var control: CustomScrollView<Content, VM>
        var viewModel: VM
        init(_ control: CustomScrollView, viewModel: VM) {
            self.control = control
            self.viewModel = viewModel
        }
        
        @objc func handleRefreshControl(sender: UIRefreshControl) {
            sender.endRefreshing()
            viewModel.refresh()
        }
    }

}

SeeAllViewModel

class SeeAllViewModel: ObservableObject, LoadProtocol {
    @Published var category: Category
    
    init(category: Category) {
        self.category = category
    }
    
    func refresh() {
        //everytime you need more data fetched and on database updates to your snapshot this will be triggered
        let query = self.category.query.start(afterDocument: self.category.lastDocumentSnapshot!).limit(to: 1)
        query.addSnapshotListener { (snapshot, error) in
            guard let snapshot = snapshot else {
                print("Error retrieving cities: \(error.debugDescription)")
                return
            }
            
            guard let lastSnapshot = snapshot.documents.last else {
                // The collection is empty.
                return
            }
            
            self.category.lastDocumentSnapshot = lastSnapshot
            
            // Construct a new query starting after this document,
            
            // Use the query for pagination.
            self.category.items += snapshot.documents.map { document -> Item in
                return Item(document: document)
            }
        }
    }
    
}
1 Answers

It appears that dynamic property update cannot pass boundary of different hosting controller, so the solution is pass it (in this case observable object) inside explicitly.

Tested with Xcode 11.4 / iOS 13.4 on replicated code

demo

So, custom view is constructed as

CustomScrollView(width: geometry.size.width, height: geometry.size.height, viewModel: self.seeAllViewModel) {
    // Separate internals to subview and pass view modal there
    RefreshInternalView(seeAllViewModel: self.seeAllViewModel)
}

and here is separated view, nothing special - just extracted everything from above

struct RefreshInternalView: View {
    @ObservedObject var seeAllViewModel: SeeAllViewModel
    var body: some View {
        VStack {
        Text("\(self.seeAllViewModel.category.items.count)")  // not being updated
        List {
            ForEach(self.seeAllViewModel.category.items) { (item: Item) in
                ItemRowView(itemViewModel: ItemViewModel(item: item))
            }
        }
        .listStyle(GroupedListStyle())
        .navigationBarTitle(Text(self.seeAllViewModel.category.title.firstCapitalized))
        }
    }
 }
Related