Avoiding Massive View Controller using Containment & Child View Controller
View Controller is the component that provides basic building block that we use as a foundation to build application in iOS development. In Apple MVC world, it sits as a middle man between the View and Model, acting as an orchestrator between both of them. It starts with the controller acting as an observer that reacts to Model changes, updates the View, accepts user interaction from the view using the Target Action, and then updates the Model.
As an iOS developer, there are many times that we will face an issue of handling Massive View Controller, even if we use architectures like MVVM, MVP, or VIPER. Sometimes, the View Controller has too many responsibilities to handle in one single screen. It’s violating the SRP (Single Responsibility Principle), creating tight coupling between modules, and making it hard to reuse and test each of the components.
We can take the app screenshot below as an example. You can see there are at least 3 responsibilities in one single screen:
- Display a list of movies;
- Display a filter list that can be selected to be applied for the list of movies;
- Clear selection of the selected filters.
If we are going to build this screen using Single View Controller, it is guaranteed that the view controller will become very massive and bloated as it handles too many responsibilities in one single view controller.
How can we solve this problem? One of the solution is using View Controller Containment and Child View Controller. Here are the benefits of using this solution:
- Encapsulate the listing of movies into
MovieListViewControllerthat has single responsibility to just display list of movies and reacts to changes in
MovieModel. We can also reuse this
MovieListViewControllerin another screen if we want to just display list of movies without filter.
- Encapsulate the listing and selection of filters logic into
FilterListViewControllerthat has single responsibility to just display and handles the selection of filters. We can use delegation to communicate with the parent View Controller when user select and deselect filters.
- Slimming down the main View Controller into one
ContainerViewControllerthat just has the responsibility to apply the selected filters from the Filter List to the
MovieListViewController. It also sets up the layout and adds the child view controllers using the container views.
You can view the complete project source code in the GitHub Repository below.
Using Child View Controllers for encapsulation, reusability, and avoid Massive View Controller …github.com
Composition of the View Controllers using Storyboard
According to the above storyboard, here are the View Controllers that we use to build our Filter screen:
ContainerViewController: The Containment View Controller provides 2 container views to embed the Child View Controller inside a horizontal
UIStackView. It provides a single
UIButtonto clear the selected filters as well. It also embedded in a
UINavigationControllerthat acts as the initial View Controller.
FilterListMovieController: The View Controller that is the subclass of the
UITableViewControllerwith Grouped style and one prototype standard Cell to display the name of the filter. It also has its Storyboard ID assigned so it can be instantiated from the
MovieListViewController: The View Controller that is the subclass of the
UITableViewControllerwith Plain style and one prototype subtitle Cell to display the attributes of the
Movie. It also has its Storyboard ID assigned like the
The Movie List View Controller
This view controller has the responsibility to display the list of
Movie model that is exposed as an instance property. We are using Swift
didSet property observer to react to changes in model, and then reload the
UITableView. The cell displays the title, duration, rating, and genre for the
Movie using default subtitle
The Filter List View Controller
The Filter List displays the
MovieFilter enum in 3 separate sections: genre, rating, and duration. The
MovieFilter enum itself conforms to
Hashable protocol so it can be stored inside a
Set uniquely using the hash value of each enum and its properties. The selections of the filters are stored under an instance property with
Set containing the
To communicate with other object, a
delegate pattern is used using the
FilterListControllerDelegate. There are 3 methods for the delegate to implement:
- Selection of a filter.
- Deselection of a filter.
- Clear all selected filters.
Integrating inside the Container View Controller
ContainerViewController, we have several instance properties:
MovieListContainerView: The container views that will be used to add the child view controllers.
MovieListViewController: The reference to Movie List and Filter List View Controllers that will be instantiated using the Storyboard ID.
Moviearray that is instantiated using default hardcoded Movies.
viewDidLoad is invoked, we call the method to setup the Child View Controllers. Here are several tasks it performs:
- Instantiate the
MovieListViewControllerusing the Storyboard ID;
- Assign them to the instance properties;
- Assign the
MovieListViewControllerthe movies array;
- Assign the
FilterListViewControllerso it can respond to the filter selection;
- Set Child Views frames and add them as the Child View Controller using the helper method extension.
FilterListViewControllerDelegate implementation, when filter is selected or deselected, the default Movies data is filtered for each respective genre, rating, and duration. Then, the result of the filter is assigned to the
movies property. For the deselection of all filters, it just assigns the default movies data.
By looking at the sample project, we can see the benefit of using View Controller Containment and Child View Controller in our app. We can divide the responsibilities of single View Controller into separate View Controllers that only has single responsibility (SRP). We also need to make sure that the Child View Controller does not know anything about its parent. For the Child View Controller to communicate back to the parent, we can use the Delegation pattern.
This approach also provides the benefit of loosely coupled modules that can lead to better reusability and testing of each components. It really helps us scale our app as it grow larger and more complex. Let’s keep learning 📖, Merry Christmas🎄, and Happy New Year🎊 to all of you! Keep on Swifting with Cocoa!!😋