collection view is not showing the expected data

140 Views Asked by At

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.
enter image description here

1

There are 1 best solutions below

12
GGShin On BEST ANSWER

fetchsimilarMovie() inside MoviesDetailsViewModel calls 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 MoviesDetailsViewModel after receiving the result from apiManager.execute function. But, you did not set what updatedState closure should do. Instead of just calling collectionView.reloadData() inside viewDidLoad() of SmiliarMovieViewController, you need to set it as what updatedState should do.

class SmiliarMovieViewController: UIViewController {

    private let viewModel: MoviesDetailsViewModel

    init(viewModel: MoviesDetailsViewModel) {
        self.viewModel = viewModel
        super.init(nibName: nil, bundle: nil)
        navigationItem.largeTitleDisplayMode = .never
    }
    //...
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setUpUI()
        
       // Set `updatedState`
        self.viewModel.updatedState = { [weak self] in
            self.collectionView.reloadData()
        }
        
        viewModel.fetchSimilarMovie()
        
    }

Hope this can achieve what you want to.