Skip to content

Commit 5b56824

Browse files
committed
feat: [SwiftUI Redux Movies demo] get MovieDetail from API
feat: Enhance Webservice to properly decode MovieDetail from API - Updated Webservice to improve error handling and decoding logic. - Added `Rating` struct to handle nested `Ratings` array in the API response. - Included `ratings` field in `MovieDetail` to match API response structure. - Enhanced error handling to capture network errors, decoding errors, and no data scenarios. - Used `do-catch` block for decoding to provide detailed error reporting. These changes ensure that `movieDetail` is correctly parsed from the API response and errors are handled appropriately.
1 parent 842ccef commit 5b56824

File tree

9 files changed

+234
-14
lines changed

9 files changed

+234
-14
lines changed

SwiftUI/SwiftUIRedux_MovieApp/ReduxAsync.xcodeproj/project.pbxproj

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@
3737
9A1119222C4D04EF005C31F4 /* RatingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A1119202C4D04EF005C31F4 /* RatingView.swift */; };
3838
9A1119272C4D1156005C31F4 /* MoviesMiddleware.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A1119262C4D1156005C31F4 /* MoviesMiddleware.swift */; };
3939
9A1119282C4D1156005C31F4 /* MoviesMiddleware.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A1119262C4D1156005C31F4 /* MoviesMiddleware.swift */; };
40+
9A11192A2C4D19CC005C31F4 /* MovieDetail.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A1119292C4D19CC005C31F4 /* MovieDetail.swift */; };
41+
9A11192B2C4D19CC005C31F4 /* MovieDetail.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A1119292C4D19CC005C31F4 /* MovieDetail.swift */; };
42+
9A11192D2C4D1E78005C31F4 /* MovieDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A11192C2C4D1E78005C31F4 /* MovieDetailsView.swift */; };
43+
9A11192E2C4D1E78005C31F4 /* MovieDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A11192C2C4D1E78005C31F4 /* MovieDetailsView.swift */; };
4044
/* End PBXBuildFile section */
4145

4246
/* Begin PBXFileReference section */
@@ -60,6 +64,8 @@
6064
9A11191D2C4D018A005C31F4 /* String+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Extensions.swift"; sourceTree = "<group>"; };
6165
9A1119202C4D04EF005C31F4 /* RatingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RatingView.swift; sourceTree = "<group>"; };
6266
9A1119262C4D1156005C31F4 /* MoviesMiddleware.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoviesMiddleware.swift; sourceTree = "<group>"; };
67+
9A1119292C4D19CC005C31F4 /* MovieDetail.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovieDetail.swift; sourceTree = "<group>"; };
68+
9A11192C2C4D1E78005C31F4 /* MovieDetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovieDetailsView.swift; sourceTree = "<group>"; };
6369
/* End PBXFileReference section */
6470

6571
/* Begin PBXFrameworksBuildPhase section */
@@ -86,6 +92,7 @@
8692
340AF81E2514033B00D0E6B7 /* URLImage.swift */,
8793
9A1119202C4D04EF005C31F4 /* RatingView.swift */,
8894
342978C1250FEC1300F20610 /* ContentView.swift */,
95+
9A11192C2C4D1E78005C31F4 /* MovieDetailsView.swift */,
8996
);
9097
path = Views;
9198
sourceTree = "<group>";
@@ -173,6 +180,7 @@
173180
isa = PBXGroup;
174181
children = (
175182
341B4AC225153E4B007BB846 /* Movie.swift */,
183+
9A1119292C4D19CC005C31F4 /* MovieDetail.swift */,
176184
);
177185
path = Models;
178186
sourceTree = "<group>";
@@ -309,6 +317,7 @@
309317
files = (
310318
9A1119272C4D1156005C31F4 /* MoviesMiddleware.swift in Sources */,
311319
340AF8242514034700D0E6B7 /* ImageLoader.swift in Sources */,
320+
9A11192D2C4D1E78005C31F4 /* MovieDetailsView.swift in Sources */,
312321
34111D5D250FFE8C009064B8 /* Store.swift in Sources */,
313322
343CB8BB25193AF4008E9A7B /* View+Extensions.swift in Sources */,
314323
340AF81F2514033B00D0E6B7 /* URLImage.swift in Sources */,
@@ -319,6 +328,7 @@
319328
342978D3250FEC1500F20610 /* HelloReduxApp.swift in Sources */,
320329
341B4AC325153E4C007BB846 /* Movie.swift in Sources */,
321330
34D2F6F4251150DC00117401 /* AppReducer.swift in Sources */,
331+
9A11192A2C4D19CC005C31F4 /* MovieDetail.swift in Sources */,
322332
34D075ED25191C2F00DBA6F7 /* Webservice.swift in Sources */,
323333
34D075DB25191A4A00DBA6F7 /* MoviesReducer.swift in Sources */,
324334
);
@@ -330,6 +340,7 @@
330340
files = (
331341
9A1119282C4D1156005C31F4 /* MoviesMiddleware.swift in Sources */,
332342
340AF8252514034700D0E6B7 /* ImageLoader.swift in Sources */,
343+
9A11192E2C4D1E78005C31F4 /* MovieDetailsView.swift in Sources */,
333344
34111D5E250FFE8C009064B8 /* Store.swift in Sources */,
334345
343CB8BC25193AF4008E9A7B /* View+Extensions.swift in Sources */,
335346
340AF8202514033B00D0E6B7 /* URLImage.swift in Sources */,
@@ -340,6 +351,7 @@
340351
342978D4250FEC1500F20610 /* HelloReduxApp.swift in Sources */,
341352
341B4AC425153E4C007BB846 /* Movie.swift in Sources */,
342353
34D2F6F5251150DC00117401 /* AppReducer.swift in Sources */,
354+
9A11192B2C4D19CC005C31F4 /* MovieDetail.swift in Sources */,
343355
34D075EE25191C2F00DBA6F7 /* Webservice.swift in Sources */,
344356
34D075DC25191A4A00DBA6F7 /* MoviesReducer.swift in Sources */,
345357
);

SwiftUI/SwiftUIRedux_MovieApp/Shared/HelloReduxApp.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ struct HelloReduxApp: App {
1515
middlewares: [moviesMiddleware()])
1616

1717
WindowGroup {
18-
ContentView().environmentObject(store)
18+
MovieDetailsView(movie:
19+
Movie(title: "Batman: The Killing Joke", imdbId: "tt4853102", poster: "https://m.media-amazon.com/images/M/MV5BMTdjZTliODYtNWExMi00NjQ1LWIzN2MtN2Q5NTg5NTk3NzliL2ltYWdlXkEyXkFqcGdeQXVyNTAyODkwOQ@@._V1_SX300.jpg")).environmentObject(store)
1920
}
2021
}
2122
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
//
2+
// MovieDetail.swift
3+
// ReduxAsync
4+
//
5+
// Created by chenyilong on 2024/7/21.
6+
//
7+
8+
import Foundation
9+
10+
struct Rating: Decodable {
11+
let source: String
12+
let value: String
13+
14+
private enum CodingKeys: String, CodingKey {
15+
case source = "Source"
16+
case value = "Value"
17+
}
18+
}
19+
20+
struct MovieDetail: Decodable {
21+
let title: String
22+
let year: String
23+
let rated: String
24+
let released: String
25+
let runtime: String
26+
let genre: String
27+
let director: String
28+
let writer: String
29+
let actors: String
30+
let plot: String
31+
let language: String
32+
let country: String
33+
let awards: String
34+
let poster: String
35+
let ratings: [Rating]
36+
let metascore: String
37+
let imdbRating: String
38+
let imdbVotes: String
39+
let imdbId: String
40+
let type: String
41+
let dvd: String
42+
let boxOffice: String
43+
let production: String
44+
let website: String
45+
let response: String
46+
47+
private enum CodingKeys: String, CodingKey {
48+
case title = "Title"
49+
case year = "Year"
50+
case rated = "Rated"
51+
case released = "Released"
52+
case runtime = "Runtime"
53+
case genre = "Genre"
54+
case director = "Director"
55+
case writer = "Writer"
56+
case actors = "Actors"
57+
case plot = "Plot"
58+
case language = "Language"
59+
case country = "Country"
60+
case awards = "Awards"
61+
case poster = "Poster"
62+
case ratings = "Ratings"
63+
case metascore = "Metascore"
64+
case imdbRating = "imdbRating"
65+
case imdbVotes = "imdbVotes"
66+
case imdbId = "imdbID"
67+
case type = "Type"
68+
case dvd = "DVD"
69+
case boxOffice = "BoxOffice"
70+
case production = "Production"
71+
case website = "Website"
72+
case response = "Response"
73+
}
74+
}
75+

SwiftUI/SwiftUIRedux_MovieApp/Shared/Services/Webservice.swift

Lines changed: 51 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,40 @@ enum NetworkError: Error {
1111
case badUrl
1212
case decodingError
1313
case noData
14+
case networkError(Error)
1415
}
1516

1617
class Webservice {
18+
func getMovieDetailsById(id: String, completion: @escaping (Result<MovieDetail?, NetworkError>) -> Void) {
19+
20+
let url = URL(string: Constants.Url.urlForMovieDetailsByImdbId(id))
21+
22+
guard let movieUrl = url else {
23+
completion(.failure(.badUrl))
24+
return
25+
}
26+
27+
URLSession.shared.dataTask(with: movieUrl) { data, _, error in
28+
29+
if let error = error {
30+
completion(.failure(.networkError(error)))
31+
return
32+
}
33+
34+
guard let data = data else {
35+
completion(.failure(.noData))
36+
return
37+
}
38+
39+
do {
40+
let movieDetail = try JSONDecoder().decode(MovieDetail.self, from: data)
41+
completion(.success(movieDetail))
42+
} catch {
43+
completion(.failure(.decodingError))
44+
}
45+
46+
}.resume()
47+
}
1748

1849
func getMoviesBySearch(search: String, completion: @escaping (Result<[Movie]?, NetworkError>) -> Void) {
1950

@@ -27,19 +58,23 @@ class Webservice {
2758

2859
URLSession.shared.dataTask(with: moviesUrl) { data, _, error in
2960

30-
guard let data = data, error == nil else {
61+
if let error = error {
62+
completion(.failure(.networkError(error)))
63+
return
64+
}
65+
66+
guard let data = data else {
3167
completion(.failure(.noData))
3268
return
3369
}
3470

35-
guard let movieResponse = try? JSONDecoder().decode(MovieResponse.self, from: data)
36-
else {
71+
do {
72+
let movieResponse = try JSONDecoder().decode(MovieResponse.self, from: data)
73+
completion(.success(movieResponse.movies))
74+
} catch {
3775
completion(.failure(.decodingError))
38-
return
3976
}
4077

41-
completion(.success(movieResponse.movies))
42-
4378
}.resume()
4479
}
4580

@@ -52,21 +87,24 @@ class Webservice {
5287

5388
URLSession.shared.dataTask(with: moviesUrl) { data, _, error in
5489

55-
guard let data = data, error == nil else {
90+
if let error = error {
91+
completion(.failure(.networkError(error)))
92+
return
93+
}
94+
95+
guard let data = data else {
5696
completion(.failure(.noData))
5797
return
5898
}
5999

60-
guard let movieResponse = try? JSONDecoder().decode(MovieResponse.self, from: data)
61-
else {
100+
do {
101+
let movieResponse = try JSONDecoder().decode(MovieResponse.self, from: data)
102+
completion(.success(movieResponse.movies))
103+
} catch {
62104
completion(.failure(.decodingError))
63-
return
64105
}
65106

66-
completion(.success(movieResponse.movies))
67-
68107
}.resume()
69108

70109
}
71-
72110
}

SwiftUI/SwiftUIRedux_MovieApp/Shared/Store/Middlewares/MoviesMiddleware.swift

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,18 @@ func moviesMiddleware() -> Middleware<AppState> {
2222
dispatch(SetMoviesAction(movies: movies))
2323
}
2424

25+
case .failure(let error):
26+
print(error.localizedDescription)
27+
}
28+
}
29+
case let action as FetchMovieDetailAction:
30+
Webservice().getMovieDetailsById(id: action.imdbId) { result in
31+
switch result {
32+
case .success(let movieDetail):
33+
if let movieDetail = movieDetail {
34+
dispatch(SetMovieDetailAction(movieDetail: movieDetail))
35+
}
36+
2537
case .failure(let error):
2638
print(error.localizedDescription)
2739
}

SwiftUI/SwiftUIRedux_MovieApp/Shared/Store/Store.swift

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ struct AppState: ReduxState {
2020

2121
struct MoviesState: ReduxState {
2222
var movies: [Movie] = [Movie]()
23+
var selectedMovieDetail: MovieDetail?
2324
}
2425

2526
protocol Action { }
@@ -32,6 +33,14 @@ struct SetMoviesAction: Action {
3233
let movies: [Movie]
3334
}
3435

36+
struct FetchMovieDetailAction: Action {
37+
let imdbId: String
38+
}
39+
40+
struct SetMovieDetailAction: Action {
41+
let movieDetail: MovieDetail
42+
}
43+
3544
class Store<StoreState: ReduxState>: ObservableObject {
3645

3746
var reducer: Reducer<StoreState>

SwiftUI/SwiftUIRedux_MovieApp/Shared/Store/reducers/MoviesReducer.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ func moviesReducer(state: MoviesState, action: Action) -> MoviesState {
1414
switch action {
1515
case let action as SetMoviesAction:
1616
state.movies = action.movies
17+
case let action as SetMovieDetailAction:
18+
state.selectedMovieDetail = action.movieDetail
1719
default:
1820
break
1921
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
//
2+
// MovieDetailsView.swift
3+
// ReduxAsync
4+
//
5+
// Created by chenyilong on 2024/7/21.
6+
//
7+
8+
import SwiftUI
9+
10+
struct MovieDetailsView: View {
11+
@EnvironmentObject var store: Store<AppState>
12+
13+
let movie: Movie
14+
struct Props {
15+
let movieDetail: MovieDetail?
16+
let onLoadMovieDetail: (String) -> Void
17+
}
18+
19+
private func map(state: MoviesState, dispatch: @escaping Dispatcher) -> Props {
20+
Props(movieDetail: state.selectedMovieDetail, onLoadMovieDetail: { imdbId in
21+
dispatch(FetchMovieDetailAction(imdbId: imdbId))
22+
})
23+
}
24+
25+
var body: some View {
26+
VStack {
27+
let props = map(state: store.state.moviesState, dispatch: store.dispatch)
28+
29+
Group {
30+
if let movieDetail = props.movieDetail {
31+
VStack{
32+
HStack {
33+
Spacer()
34+
URLImage(url: movieDetail.poster)
35+
Spacer()
36+
}
37+
Text(movieDetail.title).padding().font(.title)
38+
Text(movieDetail.plot).padding()
39+
HStack {
40+
Text("Rating")
41+
RatingView(rating: .constant(movieDetail.imdbRating.toInt()))
42+
Spacer()
43+
}
44+
}
45+
46+
47+
} else {
48+
Button("Load Movie Detail") {
49+
props.onLoadMovieDetail(movie.imdbId)
50+
}
51+
}
52+
}
53+
.onAppear(perform: {
54+
props.onLoadMovieDetail(movie.imdbId)
55+
})
56+
}
57+
58+
}
59+
}
60+
61+
struct MovieDetailsView_Previews: PreviewProvider {
62+
static var previews: some View {
63+
64+
let store = Store(reducer: appReducer, state: AppState(),
65+
middlewares: [moviesMiddleware()])
66+
67+
MovieDetailsView(movie:
68+
Movie(title: "Batman: The Killing Joke", imdbId: "tt4853102", poster: "https://m.media-amazon.com/images/M/MV5BMTdjZTliODYtNWExMi00NjQ1LWIzN2MtN2Q5NTg5NTk3NzliL2ltYWdlXkEyXkFqcGdeQXVyNTAyODkwOQ@@._V1_SX300.jpg")
69+
).environmentObject(store)
70+
}
71+
}
3.3 KB
Binary file not shown.

0 commit comments

Comments
 (0)