-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathMovieListViewModel.swift
105 lines (87 loc) · 3.17 KB
/
MovieListViewModel.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
//
// MovieListViewModel.swift
// GrappaMovie
//
// Created by Kristian Emil on 27/11/2024.
//
import Foundation
import SwiftUI
import Combine
// The ViewModel manages the state and business logic for our movie list
@MainActor
class MovieListViewModel: ObservableObject {
// Published properties automatically notify SwiftUI views of changes
@Published private(set) var movies: [Movie] = []
@Published private(set) var isLoading = false
@Published private(set) var error: String?
@Published var searchText = ""
// State for pagination
private var currentPage = 1
private var canLoadMorePages = true
// Tracks if we're showing search results or popular movies
private var isSearching: Bool {
!searchText.isEmpty
}
init() {
// Set up search debouncing
setupSearchDebouncing()
}
// Load initial data
func loadInitialMovies() async {
guard movies.isEmpty else { return }
await loadMovies()
}
// Load more movies when reaching the end of the list
func loadMoreMoviesIfNeeded(currentMovie movie: Movie) async {
// If we're looking at the last few items, load more
let thresholdIndex = movies.index(movies.endIndex, offsetBy: -3)
if movies.firstIndex(where: { $0.id == movie.id }) ?? 0 >= thresholdIndex,
!isLoading && canLoadMorePages {
await loadMovies()
}
}
// Main function to load movies, either from search or popular
private func loadMovies() async {
guard !isLoading && canLoadMorePages else { return }
isLoading = true
error = nil
do {
// Decide whether to fetch search results or popular movies
let response = try await if isSearching {
await MovieService.shared.searchMovies(query: searchText, page: currentPage)
} else {
await MovieService.shared.fetchPopularMovies(page: currentPage)
}
// Append new movies to existing list
movies.append(contentsOf: response.results)
currentPage += 1
canLoadMorePages = currentPage <= response.totalPages
} catch {
self.error = error.localizedDescription
}
isLoading = false
}
// Refresh the movie list (e.g., when pulling to refresh)
func refresh() async {
currentPage = 1
canLoadMorePages = true
movies = []
await loadMovies()
}
// Set up search functionality with debouncing
private func setupSearchDebouncing() {
// We use the searchText publisher to debounce search requests
$searchText
.debounce(for: .seconds(0.5), scheduler: DispatchQueue.main)
.removeDuplicates()
.sink { [weak self] newSearchText in
guard let self = self else { return }
Task {
await self.refresh()
}
}
.store(in: &cancellables)
}
// Store our Combine cancellables
private var cancellables = Set<AnyCancellable>()
}