I create a pagination screen using TMDb api in SwiftUI for GridView :
var body: some View {
ZStack {
if(viewModel.movies.isEmpty) {
loadingOrErrorView
} else {
ScrollView {
LazyVGrid(columns: [GridItem(.adaptive(minimum: Constants.gridItemSize))]) {
ForEach(viewModel.movies, id: .self.id) { movie in
MovieColumn(movie: movie).onAppear {
if movie == viewModel.movies.last, viewModel.hasMoreData {
Task {
await viewModel.loadMoreData()
}
}
}
}
}
Spacer()
loadingOrErrorView
}
}
}.task {
await viewModel.loadMoreData()
}
}
@ViewBuilder
private var loadingOrErrorView: some View {
switch viewModel.viewState {
case .loading:
ProgressView()
case .failure(let error):
errorView(error: error)
case .idle:
EmptyView()
}
}
private func errorView(error: Error) -> some View {
VStack {
Text(error.localizedDescription)
.foregroundColor(.red)
.multilineTextAlignment(.center)
.padding()
Button(action: {
Task {
await viewModel.loadMoreData()
}
}) {
Text("Retry")
.padding()
.background(Color.blue)
.foregroundColor(.white)
.cornerRadius(8)
}
}
}
And here is my ViewModel :
@Observable
class PaginationViewModel {
var movies: [Movie] = []
var hasMoreData = true
var viewState: ViewState = .idle
@ObservationIgnored
private var currentPage = 1
@ObservationIgnored
private let networkService: NetworkService
init(networkService: NetworkService) {
self.networkService = networkService
}
@MainActor
func loadMoreData() async {
guard viewState != .loading && hasMoreData else { return }
self.viewState = .loading
do {
let request = TMDbRequest(path: .movies, page: currentPage)
let newItems: TMDbWrapper = try await networkService.perform(request: request)
self.movies.append(contentsOf: newItems.movies)
self.currentPage += 1
self.hasMoreData = newItems.movies.count == Constants.PAGE_SIZE
self.viewState = .idle
} catch {
self.viewState = .failure(error: error)
}
}
private struct Constants {
static let PAGE_SIZE = 20
}
}
enum ViewState: Equatable {
case idle
case loading
case failure(error: Error)
static func == (lhs: ViewState, rhs: ViewState) -> Bool {
switch (lhs, rhs) {
case (.idle, .idle): return true
case (.loading, .loading): return true
case (.failure(error: _), .failure(error: _)): return true
default: return false
}
}
}
As you see I have implemented loading and error view logic in two different place. One for initial state and one for loading next page. Is there any solution to improve this logic while using GridView? I also wonder if you have a more optimize solution for Pagination in SwiftUI?
Source code : https://github.com/alirezaeiii/Pagination-Assignment/tree/main