Booysenberry
Booysenberry

Reputation: 255

.onReceive firing twice | SwiftUI

I have a SwiftUI view that includes a Picker. I'm using a Switch statement inside .onReceive of the Picker to call a function. The function calls an external API.

The problem is that the function is being called twice whenever the view is initialised i.e duplicating the data. I'm can't figure out why .onReceive is being called twice.

I think it might have something to do with the func being called when I init the Picker Model and then getting another notification from the Picker itself but I'm not sure how to work around it.

Here's my code:

Picker Model

import Foundation

class PickerModel: ObservableObject {
    
    @Published var filter = 0
    
    let pickerOptions = ["Popular", "Top Rated"]
    
}

View containing the Picker:

import SwiftUI

struct FilteredMoviesGridView: View {
    
    @ObservedObject private var filteredMovieVM = FilteredMovieGridViewModel()
    @ObservedObject private var pickerModel = PickerModel()
    
    private var twoColumnGrid = [GridItem(.flexible()), GridItem(.flexible())]
    
    var body: some View {
        
        NavigationView {
            
            VStack {
                
                Picker(selection: $pickerModel.filter, label: Text("Select")) {
                    ForEach(0 ..< pickerModel.pickerOptions.count) {
                        Text(pickerModel.pickerOptions[$0])
                    }
                }.onReceive(pickerModel.$filter) { (value) in
                    switch value {
                    case 0:
                        filteredMovieVM.movies.removeAll()
                        filteredMovieVM.currentPage = 1
                        filteredMovieVM.fetchMovies(filter: "popularity")
                    case 1:
                        filteredMovieVM.movies.removeAll()
                        filteredMovieVM.currentPage = 1
                        filteredMovieVM.fetchMovies(filter: "vote_average")
                    default:
                        filteredMovieVM.movies.removeAll()
                        filteredMovieVM.currentPage = 1
                        filteredMovieVM.fetchMovies(filter: "popularity")
                    }
                }.pickerStyle(SegmentedPickerStyle())
                
                ScrollView {
                    
                    LazyVGrid(columns: twoColumnGrid, spacing: 10) {
                        
                        ForEach(filteredMovieVM.movies, id:\.id) { movie in
                            
                            NavigationLink(destination: MovieDetailView(movie: movie)) {
                                
                                MovieGridItemView(movies: movie)
                                
                            }.buttonStyle(PlainButtonStyle())
                            
                            .onAppear(perform: {
                                if movie == self.filteredMovieVM.movies.last {
                                    
                                    switch pickerModel.filter {
                                    case 0:
                                        self.filteredMovieVM.checkTotalMovies(filter: "popularity")
                                    case 1:
                                        self.filteredMovieVM.checkTotalMovies(filter: "vote_average")
                                    default:
                                        self.filteredMovieVM.checkTotalMovies(filter: "popularity")
                                    }
                                }
                            })
                        }
                    }
                }
                .navigationBarTitle("Movies")
            }
        }.accentColor(.white)
    }
}

The View Model containing the function:

import Foundation

class FilteredMovieGridViewModel: ObservableObject {
    
    @Published var movies = [Movie]()
    private var filteredMovies = [MovieList]()
   
    var currentPage = 1
    
    func checkTotalMovies(filter: String) {
        
        if filteredMovies.count < 20 {
            fetchMovies(filter: filter)
        }
    }
    
    func fetchMovies(filter: String) {
        
        WebService().getMoviesByFilter(filter: filter, page: currentPage) { movie in
            
            if let movie = movie {
                self.filteredMovies.append(movie)
                for movie in movie.movies {
                    self.movies.append(movie)
                    print(self.movies.count)
                }
            }
        }
        if let totalPages = filteredMovies.first?.totalPages {
            
            if currentPage <= totalPages {
                currentPage += 1
            }
        }
    }
}

Any help would be greatly appreciated.

Upvotes: 3

Views: 4216

Answers (2)

malhal
malhal

Reputation: 30569

You can use dropFirst() to ignore the first value which is sent every time the publisher is computed, e.g.

.onReceive(pickerModel.$filter.dropFirst())

However, standard code would have a @State for the filter and then use onChange to update the data model.

Upvotes: 0

Jon Shier
Jon Shier

Reputation: 12770

Most likely you're recreating your ObservedObjects whenever your FilteredMoviesGridView is recreated. This can happen whenever SwiftUI's runtime thinks it needs to recreate it. So your view creation should be cheap and you should make sure not to accidentally recreate resources you need. Luckily SwiftUI in iOS 14, etc. has made it much easier to fix this problem. Instead of using @ObservedObject, use @StateObject, which will keep the same instance alive as your view is recreated.

Upvotes: 5

Related Questions