Reputation: 255
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
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
Reputation: 12770
Most likely you're recreating your ObservedObject
s 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