I am following factory design pattern . From the view model state response I am rendering the view. For movie details view I am rendering
MovieDetailsDisplayViewController.
view content.
Here is view model code ..
enum MoviesDetailsViewModelState {
case loading(Movie)
case loaded(MovieDetails)
case pageLoaded(Page<Movie>)
case error
var title: String? {
switch self {
case .loaded(let movie):
return movie.title
case .loading(let movie):
return movie.title
case .error:
return nil
case .pageLoaded:
return nil
}
}
var movie: MovieDetails? {
switch self {
case .loaded(let movie):
return movie
case .loading, .error:
return nil
case .pageLoaded:
return nil
}
}
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
print(moviePage)
case .failure(let error):
self.state = .error
print(error)
}
}
}
}
Here I have
func fetchData()
function to get the movies details . This function is attached with MovieDetailsViewController with state case.
case .loaded(let details):
self.showMovieDetails(details)
which is calling the function private func showMovieDetails(_ movieDetails: MovieDetails) to display the content view ..
Here is the code complete code for it ..
final class MovieDetailsViewController: UIViewController {
private let viewModel: MoviesDetailsViewModel
private var currentViewController: UIViewController!
init(viewModel: MoviesDetailsViewModel) {
self.viewModel = viewModel
super.init(nibName: nil, bundle: nil)
navigationItem.largeTitleDisplayMode = .never
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
navigationItem.leftBarButtonItem = UIBarButtonItem.backButton(target: self, action: #selector(didTapBack(_:)))
updateFromViewModel()
bindViewModel()
viewModel.fetchData()
}
private func bindViewModel() {
viewModel.updatedState = { [weak self] in
guard let self else { return }
DispatchQueue.main.async {
self.updateFromViewModel()
}
}
}
private func updateFromViewModel() {
let state = viewModel.state
title = state.title
switch state {
case .loading(let movie):
self.showLoading(movie)
case .loaded(let details):
self.showMovieDetails(details)
case .error:
self.showError()
case .pageLoaded(let page):
self.showSimiliarMovieDetails(page)
}
}
private func showLoading(_ movie: Movie) {
let loadingViewController = LoadingViewController()
addChild(loadingViewController)
loadingViewController.view.frame = view.bounds
loadingViewController.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
view.addSubview(loadingViewController.view)
loadingViewController.didMove(toParent: self)
currentViewController = loadingViewController
}
private func showMovieDetails(_ movieDetails: MovieDetails) {
let containerView = UIViewController()
let displayViewController = MovieDetailsDisplayViewController(movieDetails: movieDetails)
let smiliarMovieViewController = SmiliarMovieViewController(viewModel: viewModel)
containerView.addChild(smiliarMovieViewController)
containerView.addChild(displayViewController)
displayViewController.view.frame = view.bounds
displayViewController.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
smiliarMovieViewController.view.frame = view.bounds
smiliarMovieViewController.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
containerView.willMove(toParent: nil)
transition(
from: currentViewController,
to: containerView,
duration: 0.25,
options: [.transitionCrossDissolve],
animations: nil
) { (_) in
self.currentViewController.removeFromParent()
self.currentViewController = containerView
self.currentViewController.didMove(toParent: self)
}
}
private func showSimiliarMovieDetails(_ similiarMovieDetails: Page<Movie>) {
let smiliarMovieViewController = SmiliarMovieViewController(viewModel: viewModel)
addChild(smiliarMovieViewController)
smiliarMovieViewController.view.frame = view.bounds
smiliarMovieViewController.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
currentViewController?.willMove(toParent: nil)
transition(
from: currentViewController,
to: smiliarMovieViewController,
duration: 0.25,
options: [.transitionCrossDissolve],
animations: nil
) { (_) in
self.currentViewController.removeFromParent()
self.currentViewController = smiliarMovieViewController
self.currentViewController.didMove(toParent: self)
}
}
private func showError() {
let alertController = UIAlertController(title: "", message: LocalizedString(key: "moviedetails.load.error.body"), preferredStyle: .alert)
let alertAction = UIAlertAction(title: LocalizedString(key: "moviedetails.load.error.actionButton"), style: .default, handler: nil)
alertController.addAction(alertAction)
present(alertController, animated: true, completion: nil)
}
@objc private func didTapBack(_ sender: UIBarButtonItem) {
navigationController?.popViewController(animated: true)
}
}
Now I need another view content to be merged into MovieDetailsDisplayViewController when I select the table view cell and navigate to MovieDetailsDisplayViewController which is fired in
case .loaded(let details):
self.showMovieDetails(details)
case .pageLoaded(let page):
self.showSimiliarMovieDetails(page)
I want to merge private func showMovieDetails(_ movieDetails: MovieDetails) view content and private func showSimiliarMovieDetails(_ similiarMovieDetails: Page) view content into single view .. which state is mentioned.
I have tried to create single view and add the instance of both view into single view but it create lots of different issue as well . Then I tried to create just UIView into MovieDetailsDisplayViewController but I was suggested that was not good approach and I am also not able to display the content of the view as well. Is there any alternative to solve it . If needed I can provide the view controller as well for more clear explanation.
Here is the common view controller code ..
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() {
let dvc = MovieDetailsViewController(viewModel: viewModel)
addChild(dvc)
view.addSubview(dvc.view)
dvc.view.translatesAutoresizingMaskIntoConstraints = false
dvc.didMove(toParent: self)
let lvc = LoadingViewController()
addChild(lvc)
lvc.view.translatesAutoresizingMaskIntoConstraints = false
lvc.didMove(toParent: self)
let svc = SmiliarMovieViewController(viewModel: viewModel)
addChild(svc)
svc.view.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(svc.view)
svc.didMove(toParent: self)
NSLayoutConstraint.activate([
dvc.view.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
dvc.view.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
dvc.view.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
svc.view.topAnchor.constraint(equalTo: dvc.view.bottomAnchor, constant: 50),
svc.view.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
svc.view.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
svc.view.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
dvc.view.heightAnchor.constraint(equalTo: svc.view.heightAnchor)
])
}
@objc private func didTapBack(_ sender: UIBarButtonItem) {
navigationController?.popViewController(animated: true)
}
}
