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 ..
So, before this question gets closed again, here are some principles which you need to know about when building a "Custom Container" view controller:
You want to have a parent view controller, embedding one or more child view controllers.
Your parent view controller may create the child view controllers in its initialiser function.
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.
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 IBOutlet
s for these views in the parent view controller.
You should use Layout Constraints to layout your views! (note the exclamation mark!)
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.
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.
You need an approach to let your child view controller and parent to communicate with each other.
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