-
Notifications
You must be signed in to change notification settings - Fork 0
/
MovieListView.swift
157 lines (137 loc) · 4.6 KB
/
MovieListView.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
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
//
// MovieListView.swift
// GrappaMovie
//
// Created by Kristian Emil on 27/11/2024.
//
import SwiftUI
struct MovieListView: View {
// Access our view model through the environment
@StateObject private var viewModel = MovieListViewModel()
// Environment values for customization
@Environment(\.colorScheme) private var colorScheme
var body: some View {
ZStack {
// Main content list
List {
ForEach(viewModel.movies) { movie in
MovieRowView(movie: movie)
.onAppear {
// Load more content when reaching the end
Task {
await viewModel.loadMoreMoviesIfNeeded(currentMovie: movie)
}
}
}
// Loading indicator at bottom during pagination
if viewModel.isLoading {
LoadingIndicatorView()
}
}
.listStyle(.plain) // Modern inset grouped style
.refreshable { // Enable pull-to-refresh
await viewModel.refresh()
}
// Search bar in navigation bar
.searchable(text: $viewModel.searchText, prompt: "Search movies...")
// Show loading view when first loading
if viewModel.isLoading && viewModel.movies.isEmpty {
LoadingView()
}
// Show error view if there's an error
if let error = viewModel.error, viewModel.movies.isEmpty {
ErrorView(message: error) {
Task {
await viewModel.refresh()
}
}
}
}
.task {
// Load initial data when view appears
await viewModel.loadInitialMovies()
}
}
}
// A reusable loading indicator view
struct LoadingIndicatorView: View {
var body: some View {
HStack {
Spacer()
ProgressView()
.frame(height: 50)
Spacer()
}
}
}
// A full-screen loading view
struct LoadingView: View {
var body: some View {
VStack(spacing: 16) {
ProgressView()
Text("Loading movies...")
.foregroundColor(.secondary)
}
}
}
// A full-screen error view
struct ErrorView: View {
let message: String
let retryAction: () -> Void
var body: some View {
VStack(spacing: 16) {
Image(systemName: "exclamationmark.triangle")
.font(.largeTitle)
.foregroundColor(.red)
Text("Something went wrong")
.font(.headline)
Text(message)
.foregroundColor(.secondary)
.multilineTextAlignment(.center)
.padding(.horizontal)
Button("Try Again") {
retryAction()
}
.buttonStyle(.bordered)
}
}
}
// A view for each individual movie row
struct MovieRowView: View {
let movie: Movie
var body: some View {
NavigationLink(destination: MovieDetailView(movie: movie)) {
HStack(spacing: 16) {
// Movie poster
AsyncImage(url: movie.posterURL) { image in
image
.resizable()
.aspectRatio(contentMode: .fill)
} placeholder: {
Rectangle()
.foregroundColor(.gray.opacity(0.2))
}
.frame(width: 80, height: 120)
.cornerRadius(8)
// Movie info
VStack(alignment: .leading, spacing: 8) {
Text(movie.title)
.font(.headline)
.lineLimit(2)
Text(movie.formattedDate)
.font(.subheadline)
.foregroundColor(.secondary)
// Rating
HStack {
Image(systemName: "star.fill")
.foregroundColor(.yellow)
Text(String(format: "%.1f", movie.voteAverage))
.foregroundColor(.secondary)
}
.font(.subheadline)
}
.padding(.vertical, 8)
}
}
}
}