I want to display similar movie based on the movie ID is passed form table view didSelectRow function into collection view. Here is the URL which fetch the similar movie record. https://api.themoviedb.org/3/movie/129/similar?api_key=apikey. Here I am passing movie ID 129 and it fetch the similar movie. I can see in debug console I got the response correctly but it not showing the similar image into collection view. It little bit more code because I am following programmatic way to create the view .
Here is model code.. This is the generic struct Here I am passing Movie which decode the [Movie].
struct Page<T: Decodable>: Decodable {
let pageNumber: Int
let totalResults: Int
let totalPages: Int
let results: [T]
enum CodingKeys: String, CodingKey {
case pageNumber = "page"
case totalResults = "total_results"
case totalPages = "total_pages"
case results
}
}
Here is the URL network manager class ..
final class APIManager: APIManaging {
static let shared = APIManager()
let host = "https://api.themoviedb.org/3"
let apiKey = "apikey"
private let urlSession: URLSession
init(urlSession: URLSession = .shared) {
self.urlSession = urlSession
}
func execute<Value: Decodable>(_ request: Request<Value>, completion: @escaping (Result<Value, APIError>) -> Void) {
urlSession.dataTask(with: urlRequest(for: request)) { responseData, response, error in
if let data = responseData {
let response: Value
do {
response = try JSONDecoder().decode(Value.self, from: data)
} catch {
completion(.failure(.parsingError))
print(error)
return
}
completion(.success(response))
} else {
completion(.failure(.networkError))
}
}.resume()
}
private func urlRequest<Value>(for request: Request<Value>) -> URLRequest {
let url = URL(host, apiKey, request)
var result = URLRequest(url: url)
result.httpMethod = request.method.rawValue
result.cachePolicy = .reloadIgnoringLocalAndRemoteCacheData
return result
}
}
Here is the function was called form view Model ..
static func similiar(for movieID: Int) -> Request<Page<Movie>> {
return Request(method: .get, path: "movie/\(movieID)/similar")
}
Here is the table view didSelect function . Here I am selecting the cell and navigate to details view ..
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let movie = viewModel.state.movies[indexPath.row]
let viewModel = MoviesDetailsViewModel(movie: movie, apiManager: APIManager())
let viewController = CommonViewController(viewModel: viewModel)
viewModel.fetchSimilarMovie()
self.navigationController?.pushViewController(viewController, animated: true)
}
Here is the code for common view controller..
class CommonViewController: UIViewController {
private let viewModel: MoviesDetailsViewModel
private var viewController : UIViewController!
init(viewModel: MoviesDetailsViewModel) {
self.viewModel = viewModel
super.init(nibName: nil, bundle: nil)
navigationItem.largeTitleDisplayMode = .never
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
combineView()
navigationItem.leftBarButtonItem = UIBarButtonItem.backButton(target: self, action: #selector(didTapBack(_:)))
}
private func combineView() {
viewController = CommonViewController(viewModel: viewModel)
let dvc = MovieDetailsViewController(viewModel: viewModel)
let svc = SmiliarMovieViewController(viewModel: viewModel)
viewController.addChild(dvc)
viewController.addChild(svc)
}
@objc private func didTapBack(_ sender: UIBarButtonItem) {
navigationController?.popViewController(animated: true)
}
}
Here is the view model class ..
enum MoviesDetailsViewModelState {
case loading(Movie)
case loaded(MovieDetails)
case pageLoaded(Page<Movie>)
case error
/// rest of the enum here for movie and error..
var page: Page<Movie>? {
switch self {
case .loading, .error, .loaded:
return nil
case .pageLoaded(let page):
return page
}
}
}
final class MoviesDetailsViewModel {
private let apiManager: APIManaging
private let initialMovie: Movie
var moviePage = [Movie]()
init(movie: Movie, apiManager: APIManaging = APIManager()) {
self.initialMovie = movie
self.apiManager = apiManager
self.state = .loading(movie)
}
var updatedState: (() -> Void)?
var state: MoviesDetailsViewModelState {
didSet {
updatedState?()
}
}
func fetchData() {
apiManager.execute(MovieDetails.details(for: initialMovie)) { [weak self] result in
guard let self = self else { return }
switch result {
case .success(let movieDetails):
self.state = .loaded(movieDetails)
case .failure:
self.state = .error
}
}
}
func fetchSimilarMovie() {
apiManager.execute(Movie.similiar(for: initialMovie.id)) { [weak self] result in
guard let self = self else { return }
switch result {
case.success(let page):
self.state = .pageLoaded(page)
self.moviePage = page.results
case .failure(let error):
self.state = .error
print(error)
}
}
}
}
Here is my collection view code. Here I'm trying to display movie image for now.
import UIKit
class SmiliarMovieViewController: UIViewController {
private let viewModel: MoviesDetailsViewModel
init(viewModel: MoviesDetailsViewModel) {
self.viewModel = viewModel
super.init(nibName: nil, bundle: nil)
navigationItem.largeTitleDisplayMode = .never
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private lazy var collectionView: UICollectionView = {
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .horizontal
let width = UIScreen.main.bounds.width
layout.itemSize = CGSize(width: width, height: 80)
let collection = UICollectionView(frame: .zero, collectionViewLayout: layout)
collection.translatesAutoresizingMaskIntoConstraints = false
collection.dataSource = self
// collection.delegate = self
collection.register(SimilierMovieCell.self, forCellWithReuseIdentifier: SimilierMovieCell.identifier)
collection.backgroundColor = .lightGray
return collection
}()
override func viewDidLoad() {
super.viewDidLoad()
setUpUI()
self.viewModel.updatedState = {[weak self] in
DispatchQueue.main.async {
self?.collectionView.reloadData()
}
}
viewModel.fetchSimilarMovie()
}
private func setUpUI() {
view.addSubview(collectionView)
collectionView.topAnchor.constraint(equalTo: view.topAnchor, constant: 40).isActive = true
collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 40).isActive = true
collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -40).isActive = true
collectionView.heightAnchor.constraint(equalToConstant: view.frame.width/2).isActive = true
}
extension SmiliarMovieViewController: UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
let items = viewModel.moviePage.count
return items
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: SimilierMovieCell.identifier, for: indexPath) as? SimilierMovieCell
let listMovie = viewModel.moviePage[indexPath.row]
print(listMovie)
cell?.configure(listMovie)
return cell ?? SimilierMovieCell()
}
}
Here is the cell code.
class SimilierMovieCell: UICollectionViewCell {
static let identifier = "CompanyCell"
private lazy var companyImageView: UIImageView = {
let imageView = UIImageView()
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.contentMode = .scaleAspectFit
return imageView
}()
func configure(movieDetails: Movie) {
contentView.addSubview(companyImageView)
if let path = movieDetails.posterPath {
companyImageView.dm_setImage(posterPath: path)
} else {
companyImageView.image = nil
}
let safeArea = contentView.safeAreaLayoutGuide
companyImageView.topAnchor.constraint(equalTo: safeArea.topAnchor).isActive = true
companyImageView.bottomAnchor.constraint(equalTo: safeArea.bottomAnchor).isActive = true
companyImageView.leadingAnchor.constraint(equalTo: safeArea.leadingAnchor).isActive = true
companyImageView.trailingAnchor.constraint(equalTo: safeArea.trailingAnchor).isActive = true
}
}
Here is the function to download the image..
func dm_setImage(posterPath: String) {
let imageURL = URL(string: "https://image.tmdb.org/t/p/w185/\(posterPath)")
DispatchQueue.global().async {
let data = try? Data(contentsOf: imageURL!)
DispatchQueue.main.async {
guard let data = data else { return }
self.image = UIImage(data: data)
}
}
}
Here is the screenshot of the result. When I selected the tableview didselect and passing the movie ID form here I am expecting to show similar movies details at bottom of view now it empty.

fetchsimilarMovie()insideMoviesDetailsViewModelcalls an asynchronous method defined inside apiManager. Asynchronous execution does not block the codes and also does not guarantee the time when you get results. Therefore it is a must to add logic to update UI after you got the results back.In your case, you wrote the code to change the state of
MoviesDetailsViewModelafter receiving the result fromapiManager.executefunction. But, you did not set whatupdatedStateclosure should do. Instead of just callingcollectionView.reloadData()insideviewDidLoad()ofSmiliarMovieViewController, you need to set it as whatupdatedStateshould do.Hope this can achieve what you want to.