Search code examples
iosswiftviewviewcontrollerchildviewcontroller

Rendering two view controller content into single view


I want to display two view controller content into single view. I have created a common view controller and add two view content controller as child view inside the common view. But the problem when I navigate to common view controller form didSelectRowAt function it not rendering the content of child view content(DetailsViewController and SmiliarViewController) but It work when I pass the individual view..

Here is the Model for movie ..

import Foundation

    struct Movie: Decodable {
        
        let id: Int
        let title: String
        let overview: String
        let posterPath: String?
        let voteAverage: Double
        
        enum CodingKeys: String, CodingKey {
            case id
            case title
            case overview
            case posterPath = "poster_path"
            case voteAverage = "vote_average"
        }
        
    }
    
    extension Movie {
        static var topRated: Request<Page<Movie>> {
            return Request(method: .get, path: "/movie/top_rated")
        }
        
        static func similiar(for movieID: Int) -> Request<Page<Movie>> {
            return Request(method: .get, path: "movie/\(movieID)/similar")
        }
    }

Here is the model for page ..

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 model for Details.

import Foundation

struct MovieDetails: Decodable {
    
    let title: String
    let overview: String
    let backdropPath: String
    let tagline: String?
    
    enum CodingKeys: String, CodingKey {
        case title
        case overview
        case backdropPath = "backdrop_path"
        case tagline
    }
    
}

extension MovieDetails {
    static func details(for movie: Movie) -> Request<MovieDetails> {
        return Request(method: .get, path: "/movie/\(movie.id)")
    }
}

Here is the Network layer ..

enum APIError: Error {
    case networkError
    case parsingError
}

extension URL {
    func url(with queryItems: [URLQueryItem]) -> URL {
        var components = URLComponents(url: self, resolvingAgainstBaseURL: true)!
        components.queryItems = (components.queryItems ?? []) + queryItems
        return components.url!
    }
    
    init<Value>(_ host: String, _ apiKey: String, _ request: Request<Value>) {
        let queryItems = [ ("api_key", apiKey) ]
            .map { name, value in URLQueryItem(name: name, value: "\(value)") }
        
        let url = URL(string: host)!
            .appendingPathComponent(request.path)
            .url(with: queryItems)
        
        self.init(string: url.absoluteString)!
    }
}

protocol APIManaging {
    func execute<Value: Decodable>(_ request: Request<Value>, completion: @escaping (Result<Value, APIError>) -> Void)
}

final class APIManager: APIManaging {
    
    static let shared = APIManager()
    
    let host = "https://api.themoviedb.org/3"
    let apiKey = "e4f9e61f6ffd66639d33d3dde7e3159b"
    
    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 request enum ..

import Foundation

enum Method: String {
    case get = "GET"
}

struct Request<Value> {
    
    var method: Method
    var path: String
    
    init(method: Method = .get, path: String) {
        self.method = method
        self.path = path
    }
    
}

Here is the view model for movie view controller.

enum MoviesViewModelState {
    case loading
    case loaded([Movie])
    case error

    var movies: [Movie] {
        switch self {
        case .loaded(let movies):
            return movies
        case .loading, .error:
            return []
        }
    }
}

final class MoviesViewModel {

    private let apiManager: APIManaging
    var filteredMovie: [Movie] = []
    
    init(apiManager: APIManaging = APIManager()) {
        self.apiManager = apiManager
    }

    var updatedState: (() -> Void)?

    var state: MoviesViewModelState = .loading {
        didSet {
            updatedState?()
        }
    }

    func fetchData() {
        apiManager.execute(Movie.topRated) { [weak self] result in
            switch result {
            case .success(let page):
                self?.state = .loaded(page.results)
            case .failure:
                self?.state = .error
            }
        }
    }
}

Here is the Initial View Controller(Movies view controller ).

     final class MoviesViewController: UITableViewController {
    
    private let viewModel: MoviesViewModel
    // MARK: - UI Components
    private let searchController = UISearchController(searchResultsController: nil)
    
    init(viewModel: MoviesViewModel) {
        self.viewModel = viewModel
        super.init(nibName: nil, bundle: nil)
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        title = LocalizedString(key: "movies.title")

        NotificationCenter.default.addObserver(self, selector: #selector(textSizeChanged), name: UIContentSizeCategory.didChangeNotification, object: nil)

        setupSearchController()
        configureTableView()
        updateFromViewModel()
        bindViewModel()
        viewModel.fetchData()
    }

    private func configureTableView() {
        tableView.dm_registerClassWithDefaultIdentifier(cellClass: MovieCell.self)
        tableView.rowHeight = UITableView.automaticDimension

        refreshControl = UIRefreshControl()
        refreshControl?.addTarget(self, action: #selector(refreshData), for: .valueChanged)
    }

    private func bindViewModel() {
        viewModel.updatedState = { [weak self] in
            guard let self else { return }
            DispatchQueue.main.async {
                self.updateFromViewModel()
            }
        }
    }

    private func updateFromViewModel() {
        switch viewModel.state {
        case .loading, .loaded:
            tableView.reloadData()
        case .error:
            showError()
        }
        refreshControl?.endRefreshing()
    }

    // MARK: setUpSearch Property.
    private func setupSearchController() {
        
        self.searchController.searchResultsUpdater = self
        self.searchController.obscuresBackgroundDuringPresentation = false
        self.searchController.hidesNavigationBarDuringPresentation = false
        self.searchController.searchBar.placeholder = "Search Movie"
        
        self.navigationItem.searchController = searchController
        self.definesPresentationContext = false
        self.navigationItem.hidesSearchBarWhenScrolling = false
        
        searchController.delegate = self
        searchController.searchBar.delegate = self
        searchController.searchBar.showsBookmarkButton = true
        searchController.searchBar.setImage(UIImage(named: "Filter"), for: .bookmark, state: .normal)
        searchController.searchBar.setLeftImage(UIImage(named: "Search"))
        searchController.searchBar.showsBookmarkButton = true

    }
    
    private func showError() {
        let alertController = UIAlertController(title: "", message: LocalizedString(key: "movies.load.error.body"), preferredStyle: .alert)
        let alertAction = UIAlertAction(title: LocalizedString(key: "movies.load.error.actionButton"), style: .default, handler: nil)
        alertController.addAction(alertAction)
        present(alertController, animated: true, completion: nil)
    }

    @objc private func refreshData() {
        viewModel.fetchData()
    }

    @objc private func textSizeChanged() {
        tableView.reloadData()
    }
}

// MARK: - UITableViewDataSource
extension MoviesViewController {

    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        let inSearchMode = self.viewModel.inSearchMode(searchController)
        return inSearchMode ? self.viewModel.filteredMovie.count : self.viewModel.state.movies.count
    }

    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        
        let cell: MovieCell = tableView.dm_dequeueReusableCellWithDefaultIdentifier()
        let inSearchMode = self.viewModel.inSearchMode(searchController)
        let movie = inSearchMode ? self.viewModel.filteredMovie[indexPath.row] : self.viewModel.state.movies[indexPath.row]
        cell.configure(movie)

        return cell
    }
}

// MARK: - UITableViewControllerDelegate
extension MoviesViewController {

    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 movies details view model.

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 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 = DetailsViewController(viewModel: viewModel)
        let svc = SmiliarViewController(viewModel: viewModel)
        viewController.addChild(dvc)
        viewController.addChild(svc)
        
    }
    @objc private func didTapBack(_ sender: UIBarButtonItem) {
        navigationController?.popViewController(animated: true)
    }
}

Here is the code DetailsViewController ..

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 displayViewController = MovieDetailsDisplayViewController(movieDetails: movieDetails)
        addChild(displayViewController)
        displayViewController.view.frame = view.bounds
        displayViewController.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        currentViewController?.willMove(toParent: nil)
        transition(
            from: currentViewController,
            to: displayViewController,
            duration: 0.25,
            options: [.transitionCrossDissolve],
            animations: nil
        ) { (_) in
            self.currentViewController.removeFromParent()
            self.currentViewController = displayViewController
            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)
    }
}

Here is the code for Display view controller for given sate .

import UIKit

final class MovieDetailsDisplayViewController: UIViewController {
    
    let movieDetails: MovieDetails
    
    init(movieDetails: MovieDetails) {
        self.movieDetails = movieDetails
        super.init(nibName: nil, bundle: nil)
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func loadView() {
        view = View()
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        (view as? View)?.configure(movieDetails: movieDetails)
    }
    
    private class View: UIView {
    
        let scrollView = UIScrollView()
        let backdropImageView = UIImageView()
        let titleLabel = UILabel()
        let overviewLabel = UILabel()
        let similarLabel = UILabel()
        private lazy var contentStackView = UIStackView(arrangedSubviews: [backdropImageView, titleLabel, overviewLabel, similarLabel])
        
        override init(frame: CGRect) {
            super.init(frame: frame)
            commonInit()
        }
        
        required init?(coder aDecoder: NSCoder) {
            super.init(coder: aDecoder)
            commonInit()
        }
        
        private func commonInit() {
            backgroundColor = .white
            
            backdropImageView.contentMode = .scaleAspectFill
            backdropImageView.clipsToBounds = true
            
            titleLabel.font = UIFont.Heading.medium
            titleLabel.textColor = UIColor.Text.charcoal
            titleLabel.numberOfLines = 0
            titleLabel.lineBreakMode = .byWordWrapping
            titleLabel.setContentHuggingPriority(.required, for: .vertical)
            
            overviewLabel.font = UIFont.Body.small
            overviewLabel.textColor = UIColor.Text.grey
            overviewLabel.numberOfLines = 0
            overviewLabel.lineBreakMode = .byWordWrapping
            
            similarLabel.font = UIFont.Body.smallSemiBold
            similarLabel.textColor = UIColor.Text.charcoal
            similarLabel.numberOfLines = 0
            similarLabel.lineBreakMode = .byWordWrapping
            
            contentStackView.axis = .vertical
            contentStackView.spacing = 24
            contentStackView.setCustomSpacing(8, after: titleLabel)
            
            setupViewsHierarchy()
            setupConstraints()
        }
        
        private func setupViewsHierarchy() {
            addSubview(scrollView)
            scrollView.addSubview(contentStackView)
        }
        
        private func setupConstraints() {
            scrollView.translatesAutoresizingMaskIntoConstraints = false
            backdropImageView.translatesAutoresizingMaskIntoConstraints = false
            contentStackView.translatesAutoresizingMaskIntoConstraints = false
            
            NSLayoutConstraint.activate(
                [
                    scrollView.topAnchor.constraint(equalTo: topAnchor),
                    scrollView.leadingAnchor.constraint(equalTo: leadingAnchor),
                    scrollView.bottomAnchor.constraint(equalTo: bottomAnchor),
                    scrollView.trailingAnchor.constraint(equalTo: trailingAnchor),
                    
                    backdropImageView.heightAnchor.constraint(equalTo: backdropImageView.widthAnchor, multiplier: 11 / 16, constant: 0),
                    
                    contentStackView.topAnchor.constraint(equalTo: scrollView.topAnchor, constant: 24),
                    contentStackView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 16),
                    contentStackView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -16),
                    contentStackView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor, constant: -24)
                ]
            )
            
            scrollView.layoutMargins = UIEdgeInsets(top: 24, left: 16, bottom: 24, right: 16)
            preservesSuperviewLayoutMargins = false
        }
        
        func configure(movieDetails: MovieDetails) {
            backdropImageView.dm_setImage(backdropPath: movieDetails.backdropPath)
            
            titleLabel.text = movieDetails.title
            
            overviewLabel.text = movieDetails.overview
            similarLabel.text = "Similar Movie"
        }
        
    }
}

Here is the screenshot when I clicked the table view cell .. screenshot


Solution

  • So, before this question gets closed again, here are some principles which you need to know about when building a "Custom Container" view controller:

    1. You want to have a parent view controller, embedding one or more child view controllers.

    2. Your parent view controller may create the child view controllers in its initialiser function.

    3. The parent view controller may want to keep references to the child view controllers, so that it can handle user intents on behalf of the child view controller which send those intents to the parent.

    4. Your parent view controller may want to define views for each child view controller where the views of the child view controllers get embedded. Thus you need IBOutlets for these views in the parent view controller.

    5. You should use Layout Constraints to layout your views! (note the exclamation mark!)

    6. Since you are also using "ViewModels" you may want to have some kind of factory function where you create the objects and put them together.

    7. It may be beneficial to hide the details of how you create the view controller by using a function that does that, so you can use storyboards or whatever to create it.

    8. You need an approach to let your child view controller and parent to communicate with each other.

    9. You are using a "ViewModel" - thus, I assume you want to implement a MVVM pattern? If so, you need to implement even more principles: the view is a function of state, and the view sends "commands" to the view model.

    So, lets start with a canonical implementation:

    First, use a way to implement the factory. Note, this is highly opinionated how you accomplish this:

    Given a view controller ParentViewController:

    
    extension ParentViewController {
        static func create(initialState: ParentViewController.State) -> ParentViewController {
            let viewModel = ViewModel<State, Event>.init(
                initialState: initialState
            )
            return UIStoryboard(name: "Parent", bundle: nil).instantiateViewController(
                identifier: "ParentViewController",
                creator: { coder in
                    let viewController = ParentViewController(coder: coder, viewModel: viewModel)
                    return viewController
                }
            )
        }
    }
    

    This uses storyboards to load and initialise the view controller. It also creates the largely opinionated view model and initialises the view model with an initial state and an "Environment" value.

    Note also, that it uses a not so common function to create the view controller from the storyboard. Please look it up in the documentation.

    Now create the parent view controller:

    Note that the parent view controller also creates the child view controllers, namely "Green" and "Mint" which serve as an example of the embedded child view controllers with a fancy name.

    final class ParentViewController: UIViewController {
           
        @IBOutlet private var greenView: UIView!
        @IBOutlet private var mintView: UIView!
        
        private var greenViewController: GreenViewController!
        private var mintViewController: MintViewController!
        
        private let viewModel: ViewModel<State, Event>
        private var cancellable: AnyCancellable!
        
        init(coder: NSCoder, viewModel: ViewModel<State, Event>) {
            state = viewModel.state
            self.viewModel = viewModel
            super.init(coder: coder)!
    
            greenViewController = GreenViewController.create(
                initialState: state.green,
                send: self.send(greenEvent:)
            )
            mintViewController = MintViewController.create(
                initialState: state.mint,
                send: self.send(mintEvent:)
            )
            
            self.cancellable = viewModel.$state.sink { [weak self] state in
                guard let self = self else { return }
                self.state = state
            }
        }
    
    
    

    Your child view controllers can be created in any way you want. Preferable you choose the same approach for all your view controllers. As you can see, there's a static function create(...) which is used to create it with all necessary parameters given.

    The send parameter in the static create(...) function is an opinionated approach to make the child view forward user intents to the parent view controller.

    In order to make this work, you need to add two functions to the parent view controller:

        private func send(greenEvent: GreenViewController.Event) {
            self.viewModel.send(.green(greenEvent))
        }
        
        private func send(mintEvent: MintViewController.Event) {
            self.viewModel.send(.mint(mintEvent))
        }
    

    You should realise, that it's only half done, but it should give you an idea how the child view controller sends "events" to the parent view controller!

    Note also, that I use an opinionated way to use a view model which I show later.

    Note also, that the parent view controller defines two views itself where the child view controller will embed their views - just to give then a place where to do this.

    You can embed a child view controller only after the parent has been loaded, since the views greenView and mintView need to be loaded already:

        override func viewDidLoad() {
            super.viewDidLoad()
    
            greenView.embed(greenViewController.view)
            self.addChild(greenViewController)
            greenViewController.didMove(toParent: self)
    
            mintView.embed(mintViewController.view)
            self.addChild(mintViewController)
            mintViewController.didMove(toParent: self)
    
            ...
    
        }
    

    Note that this above, is the central answer to your question! Unfortunately, there is so much more:

    You might want to use a helper function which embeds a view into another - given some assumptions how you want to layout the views. Mote, your mileage may vary.

    public extension UIView {
    
        func embed(_ view: UIView) {
            assert(view.superview == nil)
            view.translatesAutoresizingMaskIntoConstraints = false
            self.addSubview(view)
            NSLayoutConstraint.activate([
                view.leftAnchor.constraint(equalTo: self.leftAnchor),
                view.topAnchor.constraint(equalTo: self.topAnchor),
                view.rightAnchor.constraint(equalTo: self.rightAnchor),
                view.bottomAnchor.constraint(equalTo: self.bottomAnchor)
                ])
        }
    
    }
    

    Since you are using a "ViewModel" you should implement the view controllers as the "View" in a MVVM pattern. It also means, your "View" is a "function of state" lets, do this:

    First define the state, which should be rendered. Note, that this data is a full representation of the view (aka view controller and its subviews). In other words, it fully describes what will get rendered.

    extension GreenViewController {
    
        struct State: Equatable {
            var receivedHello: Bool = false
        }
    
        ...
    
    

    In our View Controllers (all of them) you might want to define what happens when you set the "state":

    
    final class GreenViewController: UIViewController {
        
        var state: State {
            didSet {
                if self.isViewLoaded {
                    update(with: self.state, oldState: oldValue, animated: true)
                }
            }
    
            ...
        }
    

    So, what that above means, is an highly opinionated approach which means: "update the thing, when the state changed."

    The View (aka View Controller) also needs a way to communicate "user intents back to the View Model. You purposefully use an Enum to define all the user intents which can happen in this ViewController local the ViewController:

    extension GreenViewController {
        enum Event {
            case didAppear
            case hello
        }
    
        ...
    
    

    The easiest way to accomplish to send an event to the view model is via a function variable, that will be initialised by the factory or a parent view controller, which is of course again a highly opinionated way to accomplish this:

        private let send: (Event) -> Void
    

    Use it like this:

        @IBAction func helloButtonAction(_ sender: Any) {
            send(.hello)
        }
    

    Finally you need to implement the state changes, i.e. implement how your view will render the given state:

    
        private func update(with newState: State, oldState: State, animated: Bool) {
            // render state
        }
    

    Conclusion

    So, now since you have read up to there, you probably know now why your question was not a good fit for SO ;)

    Caveat I probably missed something important.

    See this gist for the full code: https://gist.github.com/couchdeveloper/a6ba405a341fe1c9a4f1cbec85f05642