How to merge two view content using factory design pattern in swift

80 Views Asked by At

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)
    }
}

Here is the screenshot .. enter image description here

0

There are 0 best solutions below