Skip to content

Commit

Permalink
✨ Added Episode + TVSeason models
Browse files Browse the repository at this point in the history
💄 Added Episode list to TVShow details
  • Loading branch information
qeude committed May 12, 2020
1 parent 0f8c4df commit 26e7a18
Show file tree
Hide file tree
Showing 19 changed files with 375 additions and 50 deletions.
30 changes: 27 additions & 3 deletions NetflixLike.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,16 @@
01751D5D2423992A00B04BF4 /* TVShowPosterImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01751D5C2423992A00B04BF4 /* TVShowPosterImage.swift */; };
01751D6224242BDD00B04BF4 /* MoviePosterImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01751D6124242BDD00B04BF4 /* MoviePosterImage.swift */; };
01751D6424242C7500B04BF4 /* MovieCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01751D6324242C7500B04BF4 /* MovieCell.swift */; };
01751D662424EDB500B04BF4 /* Genre.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01751D652424EDB500B04BF4 /* Genre.swift */; };
01751D682424F16000B04BF4 /* TVShowDetailsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01751D672424F16000B04BF4 /* TVShowDetailsViewModel.swift */; };
018401AE241D22B300EBAB3A /* HorizontalMoviesListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 018401AD241D22B300EBAB3A /* HorizontalMoviesListView.swift */; };
018401B0241D22D600EBAB3A /* MoviesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 018401AF241D22D600EBAB3A /* MoviesViewModel.swift */; };
018401B2241D2F3100EBAB3A /* TVShowCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 018401B1241D2F3100EBAB3A /* TVShowCell.swift */; };
019284D32353AF3C00CDC83B /* ImageLoaderViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 019284D22353AF3C00CDC83B /* ImageLoaderViewModel.swift */; };
019F49BE24696CF800040EB9 /* Episode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 019F49BD24696CF800040EB9 /* Episode.swift */; };
019F49C024696E0100040EB9 /* TVSeason.swift in Sources */ = {isa = PBXBuildFile; fileRef = 019F49BF24696E0100040EB9 /* TVSeason.swift */; };
019F49C22469EC9C00040EB9 /* TVSeasonListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 019F49C12469EC9C00040EB9 /* TVSeasonListViewModel.swift */; };
019F49C42469F00E00040EB9 /* TVSeasonListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 019F49C32469F00E00040EB9 /* TVSeasonListView.swift */; };
01D00F85241E5F31007EA0B9 /* Color.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01D00F84241E5F31007EA0B9 /* Color.swift */; };
01D00F8D241EC869007EA0B9 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 01D00F8B241EC869007EA0B9 /* Localizable.strings */; };
D53D31EBDDB72BEABD2541FE /* Pods_NetflixLikeTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0AF47E95927B7892C0B22166 /* Pods_NetflixLikeTests.framework */; };
Expand Down Expand Up @@ -116,10 +122,16 @@
01751D5C2423992A00B04BF4 /* TVShowPosterImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TVShowPosterImage.swift; sourceTree = "<group>"; };
01751D6124242BDD00B04BF4 /* MoviePosterImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoviePosterImage.swift; sourceTree = "<group>"; };
01751D6324242C7500B04BF4 /* MovieCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovieCell.swift; sourceTree = "<group>"; };
01751D652424EDB500B04BF4 /* Genre.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Genre.swift; sourceTree = "<group>"; };
01751D672424F16000B04BF4 /* TVShowDetailsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TVShowDetailsViewModel.swift; sourceTree = "<group>"; };
018401AD241D22B300EBAB3A /* HorizontalMoviesListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HorizontalMoviesListView.swift; sourceTree = "<group>"; };
018401AF241D22D600EBAB3A /* MoviesViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoviesViewModel.swift; sourceTree = "<group>"; };
018401B1241D2F3100EBAB3A /* TVShowCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TVShowCell.swift; sourceTree = "<group>"; };
019284D22353AF3C00CDC83B /* ImageLoaderViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageLoaderViewModel.swift; sourceTree = "<group>"; };
019F49BD24696CF800040EB9 /* Episode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Episode.swift; sourceTree = "<group>"; };
019F49BF24696E0100040EB9 /* TVSeason.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TVSeason.swift; sourceTree = "<group>"; };
019F49C12469EC9C00040EB9 /* TVSeasonListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TVSeasonListViewModel.swift; sourceTree = "<group>"; };
019F49C32469F00E00040EB9 /* TVSeasonListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TVSeasonListView.swift; sourceTree = "<group>"; };
01D00F84241E5F31007EA0B9 /* Color.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Color.swift; sourceTree = "<group>"; };
01D00F8C241EC869007EA0B9 /* Base */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = Base; path = Base.lproj/Localizable.strings; sourceTree = "<group>"; };
0AF47E95927B7892C0B22166 /* Pods_NetflixLikeTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_NetflixLikeTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
Expand Down Expand Up @@ -296,6 +308,9 @@
children = (
01553B6A234BCCA10018CDF3 /* Movie.swift */,
01433EBE240D99B80003FA6E /* TVShow.swift */,
01751D652424EDB500B04BF4 /* Genre.swift */,
019F49BD24696CF800040EB9 /* Episode.swift */,
019F49BF24696E0100040EB9 /* TVSeason.swift */,
);
path = Entities;
sourceTree = "<group>";
Expand All @@ -312,16 +327,17 @@
isa = PBXGroup;
children = (
01751D5A242397FF00B04BF4 /* TVShowDetails.swift */,
019F49C32469F00E00040EB9 /* TVSeasonListView.swift */,
);
path = TVShowDetails;
sourceTree = "<group>";
};
01751D5E24242B5500B04BF4 /* TvShowsView */ = {
01751D5E24242B5500B04BF4 /* TVShowsView */ = {
isa = PBXGroup;
children = (
01475897241BA8E60066864D /* TVShowsView.swift */,
);
path = TvShowsView;
path = TVShowsView;
sourceTree = "<group>";
};
01751D5F24242B8A00B04BF4 /* TVShowCell */ = {
Expand All @@ -345,7 +361,7 @@
01A1F9F82374A97800E734E6 /* Views */ = {
isa = PBXGroup;
children = (
01751D5E24242B5500B04BF4 /* TvShowsView */,
01751D5E24242B5500B04BF4 /* TVShowsView */,
01751D59242397E500B04BF4 /* TVShowDetails */,
01A1F9FC2374AACA00E734E6 /* Common */,
01433ECD240DAB4E0003FA6E /* HomeView.swift */,
Expand Down Expand Up @@ -373,7 +389,9 @@
children = (
019284D22353AF3C00CDC83B /* ImageLoaderViewModel.swift */,
01433ED5240E80A90003FA6E /* TVShowsViewModel.swift */,
01751D672424F16000B04BF4 /* TVShowDetailsViewModel.swift */,
018401AF241D22D600EBAB3A /* MoviesViewModel.swift */,
019F49C12469EC9C00040EB9 /* TVSeasonListViewModel.swift */,
);
path = "View Models";
sourceTree = "<group>";
Expand Down Expand Up @@ -614,9 +632,11 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
01751D682424F16000B04BF4 /* TVShowDetailsViewModel.swift in Sources */,
01433ECE240DAB4E0003FA6E /* HomeView.swift in Sources */,
01553B41234BCA550018CDF3 /* AppDelegate.swift in Sources */,
018401B2241D2F3100EBAB3A /* TVShowCell.swift in Sources */,
01751D662424EDB500B04BF4 /* Genre.swift in Sources */,
01553B43234BCA550018CDF3 /* SceneDelegate.swift in Sources */,
011A8F8F23DDD36900C83627 /* APIRequest.swift in Sources */,
01433EBF240D99B80003FA6E /* TVShow.swift in Sources */,
Expand All @@ -626,15 +646,18 @@
01433ED6240E80A90003FA6E /* TVShowsViewModel.swift in Sources */,
01433EEA240F00DF0003FA6E /* HTTPParameter.swift in Sources */,
014758962419991E0066864D /* APIEndpoints+TVShows.swift in Sources */,
019F49C22469EC9C00040EB9 /* TVSeasonListViewModel.swift in Sources */,
019284D32353AF3C00CDC83B /* ImageLoaderViewModel.swift in Sources */,
01553B45234BCA550018CDF3 /* MainView.swift in Sources */,
01751D6424242C7500B04BF4 /* MovieCell.swift in Sources */,
01433EE6240EF6020003FA6E /* ActivityIndicator.swift in Sources */,
01433EE4240EF4FA0003FA6E /* AsyncImage.swift in Sources */,
01751D6224242BDD00B04BF4 /* MoviePosterImage.swift in Sources */,
019F49C024696E0100040EB9 /* TVSeason.swift in Sources */,
01553B6B234BCCA10018CDF3 /* Movie.swift in Sources */,
01D00F85241E5F31007EA0B9 /* Color.swift in Sources */,
01751D5B242397FF00B04BF4 /* TVShowDetails.swift in Sources */,
019F49BE24696CF800040EB9 /* Episode.swift in Sources */,
011A8F9B23DDDE4900C83627 /* APIResponse.swift in Sources */,
0147588E241991FE0066864D /* APIEndpoints.swift in Sources */,
01433ED4240DB0CC0003FA6E /* MoviesView.swift in Sources */,
Expand All @@ -646,6 +669,7 @@
01433ED2240DB0BF0003FA6E /* HorizontalTVShowsListView.swift in Sources */,
011A8F8D23DDD2BF00C83627 /* APIClient.swift in Sources */,
018401B0241D22D600EBAB3A /* MoviesViewModel.swift in Sources */,
019F49C42469F00E00040EB9 /* TVSeasonListView.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down
41 changes: 41 additions & 0 deletions NetflixLike/Sources/Entities/Episode.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
//
// Episode.swift
// NetflixLike
//
// Created by Quentin Eude on 11/05/2020.
// Copyright © 2020 Quentin Eude. All rights reserved.
//

import Foundation
struct Episode: Decodable, Encodable {
let id: Int
let name: String?
let airDate: String?
let episodeNumber: Int
let overview: String?
let seasonNumber: Int
let stillPath: String?
let voteAverage: Double
let voteCount: Int


var stillUrl: URL? {
guard let stillPath = stillPath else {
return nil
}
let url = URL(string: "\(APIClient.baseImageStringUrl)\(stillPath)")
return url
}

enum CodingKeys: String, CodingKey {
case id
case name
case airDate = "air_date"
case episodeNumber = "episode_number"
case overview
case seasonNumber = "season_number"
case stillPath = "still_path"
case voteAverage = "vote_average"
case voteCount = "vote_count"
}
}
18 changes: 18 additions & 0 deletions NetflixLike/Sources/Entities/Genre.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
//
// Genre.swift
// NetflixLike
//
// Created by Quentin Eude on 20/03/2020.
// Copyright © 2020 Quentin Eude. All rights reserved.
//

import Foundation
struct Genre: Decodable, Identifiable {
let id: Int
let name: String

enum CodingKeys: String, CodingKey {
case id
case name
}
}
2 changes: 2 additions & 0 deletions NetflixLike/Sources/Entities/Movie.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ struct Movie: Decodable, Identifiable {
let adult: Bool
let voteCount: Int
let genreIds: [Int]?
let genres: [Genre]?
let posterPath: String?
let backdropPath: String?
let releaseDate: String?
Expand Down Expand Up @@ -45,6 +46,7 @@ struct Movie: Decodable, Identifiable {
case video
case adult
case voteCount = "vote_count"
case genres
case genreIds = "genre_ids"
case posterPath = "poster_path"
case backdropPath = "backdrop_path"
Expand Down
24 changes: 24 additions & 0 deletions NetflixLike/Sources/Entities/TVSeason.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
//
// TVSeason.swift
// NetflixLike
//
// Created by Quentin Eude on 11/05/2020.
// Copyright © 2020 Quentin Eude. All rights reserved.
//

import Foundation
struct TVSeason: Decodable, Encodable {
let id: Int
let name: String
let overview: String?
let seasonNumber: Int
let posterPath: String?

enum CodingKeys: String, CodingKey {
case id
case name
case overview
case seasonNumber = "season_number"
case posterPath = "poster_path"
}
}
4 changes: 4 additions & 0 deletions NetflixLike/Sources/Entities/TVShow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,11 @@ struct TVShow: Decodable, Identifiable {
let voteAverage: Double
let voteCount: Int
let genreIds: [Int]?
let genres: [Genre]?
let posterPath: String?
let backdropPath: String?
let firstAirDate: String
let seasons: [TVSeason]?

var posterUrl: URL? {
guard let posterPath = posterPath else {
Expand Down Expand Up @@ -53,8 +55,10 @@ struct TVShow: Decodable, Identifiable {
case voteAverage = "vote_average"
case voteCount = "vote_count"
case genreIds = "genre_ids"
case genres
case posterPath = "poster_path"
case backdropPath = "backdrop_path"
case firstAirDate = "first_air_date"
case seasons
}
}
14 changes: 11 additions & 3 deletions NetflixLike/Sources/Services/APIService/APIResponse.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@
import Foundation

struct APIResponseList<ResultType: Decodable>: Decodable {
let page: Int
let totalResults: Int
let totalPages: Int
let page: Int?
let totalResults: Int?
let totalPages: Int?
let results: [ResultType]

enum CodingKeys: String, CodingKey {
Expand All @@ -21,3 +21,11 @@ struct APIResponseList<ResultType: Decodable>: Decodable {
case results
}
}

struct APIResponseTVSeason: Decodable {
let episodes: [Episode]

enum CodingKeys: String, CodingKey {
case episodes
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,5 @@ extension APIEndpoints {
static let topRatedTVShows = APIRequest<APIResponseList<TVShow>>(path: "tv/top_rated")
static func recommendationsTVShows(tvShowId: Int) -> APIRequest<APIResponseList<TVShow>> { return APIRequest(path: "tv/\(tvShowId)/recommendations") }
static func tvShow(tvShowId: Int) -> APIRequest<TVShow> { return APIRequest(path: "tv/\(tvShowId)")}
static func tvSeason(tvShowId: Int, tvSeasonNumber: Int) -> APIRequest<APIResponseTVSeason> { return APIRequest(path: "tv/\(tvShowId)/season/\(tvSeasonNumber)")}
}
40 changes: 40 additions & 0 deletions NetflixLike/Sources/View Models/TVSeasonListViewModel.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
//
// SeasonListViewModel.swift
// NetflixLike
//
// Created by Quentin Eude on 11/05/2020.
// Copyright © 2020 Quentin Eude. All rights reserved.
//

import Foundation
import Combine

class TVSeasonListViewModel: ObservableObject {
@Published var episodes = [Episode]()

private var disposables = Set<AnyCancellable>()
private(set) var isLoading = false
private static let episodeProcessingQueue = DispatchQueue(label: "episode-processing")

init(tvShowId: Int, tvSeasonNumber: Int) {
load(tvShowId: tvShowId, tvSeasonNumber: tvSeasonNumber)
}

func load(tvShowId: Int, tvSeasonNumber: Int) {
isLoading = true
APIClient().send(APIEndpoints.tvSeason(tvShowId: tvShowId, tvSeasonNumber: tvSeasonNumber))
.sink(receiveCompletion: { (completion) in
switch completion {
case .failure:
self.isLoading = false
self.episodes = []
case .finished:
break
}
}, receiveValue: { (response) in
self.isLoading = false
self.episodes = response.episodes
})
.store(in: &disposables)
}
}
49 changes: 49 additions & 0 deletions NetflixLike/Sources/View Models/TVShowDetailsViewModel.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
//
// TVShowDetailsViewModel.swift
// NetflixLike
//
// Created by Quentin Eude on 20/03/2020.
// Copyright © 2020 Quentin Eude. All rights reserved.
//

import Foundation
import Combine

class TVShowDetailsViewModel: ObservableObject {
enum State {
case initial
case loading
case error
case data
}

@Published var tvShow: TVShow?
@Published var state: State = .initial

private var disposables = Set<AnyCancellable>()

init(tvShowId: Int) {
fetchTvShowDetails(tvShowId: tvShowId)
}

private func fetchTvShowDetails(tvShowId: Int) {
self.state = .loading
APIClient().send(APIEndpoints.tvShow(tvShowId: tvShowId)).sink(receiveCompletion: { (completion) in
switch completion {
case .failure:
self.tvShow = nil
self.state = .error
case .finished:
break
}
}, receiveValue: { (response) in
self.tvShow = response
self.state = .data
})
.store(in: &disposables)
}

private func fetchEpisodes(tvShowId: Int, tvSeasonNumber: Int) -> AnyPublisher<APIResponseTVSeason, Error> {
return APIClient().send(APIEndpoints.tvSeason(tvShowId: tvShowId, tvSeasonNumber: tvSeasonNumber))
}
}
Loading

0 comments on commit 26e7a18

Please sign in to comment.