Container View Controllers Redux
In a previous post I discussed the use of container view controllers in iOS as they relate to composition. In this post I would like to expand on that concept through illustration in a more complex app with coordinators and multiple content screens. Let’s get started!
Just in case you did not read the previous post or just need a quick refresher on view controller containment we’ll review the basics. Custom container view controllers have been around since iOS 5 and were provided by Apple, in part, to prevent what they saw as abuse of view hierarchies in apps. This occurs when the view hierarchy from one view controller is “ripped” out and placed in another controller’s view hierarchy. Apple mentions that this approach leads to inconsistent hierarchies. The inconsistent hierarchy results in the appearance and rotation methods not to being properly forwarded to parts of the hierarchy, which in turn leads to unexpected behavior and a bad user experience. Apple’s talk can be found here: WWDC 2011 — Session 102. The basic code for adding a child controller can be found in the previous post.
In order to better demonstrate the uses of container view controllers, I have created a sample app that implements a variety of containment approaches. This app is meant to simulate a real-world app rather than just focus on view controller containment. Though the view controller examples are a little contrived the app uses view controller containment along with flow coordinators, protocols, class extensions, dependency injection, and several other app architecture techniques to attempt to provide a more extensive context for view controller containment. We’ll go through each screen and explore the components that comprise that screen.
The root view controller for the app is a
UITabBarController consisting of four content view controllers. The first tab is an example of a custom
ContainerViewController, which is a standard view controller that manages the content of two other view controllers. This is meant to illustrate two concepts. First an image loading view controller that simply loads an image into a view controller (a concept borrowed from Dave DeLong). The
ImageViewController is initialized with an
ImageLoader object that makes the network call to retrieve an image. That’s it, it does nothing else, short and simple.
Second, the lower half of the screen is a
ListViewController, which is just a
UIViewController containing a
UITableView that loads some
JSON data from the web. The
ListViewController is initialized with a
ListLoader object to make the network call to retrieve the data. The
DataSource is separated out into separate
ListDataSource, which is just a Swift object, to manage the sections, row counts, and cell creation. Again, very simple and straightforward.
Each of these controllers uses containment to add a
LoadingViewController which is a view controller that contains only an activity indicator. I have used this concept in several apps, and it is quite useful, especially when you have a custom activity indicator. Once the data is loaded the loading view controller is removed. Selecting a row in the
ListViewController will transition to a new
ListDetailViewController. This controller takes an
ImageLoader object like the previous view controller to load a larger version the image. We could have used the image loading view controller here as well, but I wanted show how you can override
loadView to provide a custom view in code for a view controller. This approach is used again in subsequent view controllers.
ListViewController also implements peek and pop actions which are becoming expected user experiences in apps. An issue with coordinators is revealed when implementing this behavior. The coordinator is used for the pop action just as it is used for any other navigation flow. However, peek is implemented directly in the
ListViewController. This is contrary to how the coordinator pattern would be expected to operate since a view controller should not know about any other view controller. The question becomes who should handle vending this view controller? The list view controller knows nothing about the existence of a coordinator. Should we move this into the
ListViewController coordinator and inject the coordinator so the list can ask for the peek view controller? Should we consider making a factory object to vend view controllers for peek instances? Its a question I still need to answer, since ideally we would want the
ListViewController to be able to display different detail screens based on list content. This is not possible in the current implementation.
The second tab of the app is partially based on instances I encountered at my work where there were multiple components such as a user’s profile view, the ability to add new items, and a list of items that were being displayed using
UITableView and custom cells. The problem was the profile view and action item views were used in multiple places and were being recreated in Storyboard rather than reused.
We want to display to the content of two view controllers on a single screen. We can do this by creating a
VerticalScrollingViewController which is a view controller whose main view has been replaced by a
UIScrollView. The container view is initialized with an array of view controllers, those content controller views are added as subviews to the scrollview. To illustrate we create a
ProfileViewController and a
StoryViewController. These are just examples based on my past experience. Profile views are common in many apps and often need to be displayed in multiple locations. Making a separate view controller for this feature allows it to be reused in multiple places and encapsulates the logic for editing the profile or updating the photo. The story view controller is something you might encounter in a social media app or dating app. The
ProfileViewController and the
StoryViewController both override
loadView to provide custom views for the view controllers which are created in code. The
ProfileViewController also uses a custom
CircularImageView to create the round profile image. The purpose is to show how we can distribute code into small manageable bundles, and then through composition, combine them into a meaningful and complex objects.
If you had multiple view controllers being used else where in an app, but then needed them to appear together in single screen, this is an example of how you might accomplish that using view controller containment. This is obviously not be suitable for paginated data or infinite scrolling since the view controllers are created up front not on demand like a tableview’s data source and reuse pool provide. Dave DeLong’s MVCToDo app has a more extensive example of where he uses view controllers in inside
The orientation of the scrolling view controller is only vertical and we use extensions on
UITabBarController to override the controllers orientation settings to prevent rotation. Having a scrolling view controller is really useful when creating login screens or forms. Often when you initially develop the screen you overlook the keyboard and need to manage moving the content up when the keyboard is displayed. Being able to inject your content into a scrolling view controller lets you consolidate the logic for keyboard handling as well. In fact you can even create a separate object to handle keyboard notifications and scrolling content (see Soroush Khanlou’s blog). You can take this a step further by extracting the main aspects (notifications, active field, etc.) into a protocol that can be adopted by a single class.
The third tab is an alternative implementation of a scrolling view controller. On this screen we create a
StackViewController, this is a view controller who’s base view is replaced by a
UIStackView, again by overriding
loadView. Constraints are setup when the contents are added to the
StackViewController. We base the constraints on the content view controller’s view size or preferred content size (if one is provided). When the device is rotated the axis of the stack view is changed from vertical to horizontal allowing the content to be displayed differently in a different orientation. The stack view controller is then placed in a
ScrollingContentViewController, an example borrowed from Dave DeLong. This container expects a single view controller as content and does not restrict the orientation on rotation. The focus here is on composition of view controllers as a way to separate concerns (layout for the stack view) and enhance functionality (scrolling view controller).
I did encounter some problems with this approach when building the app. In particular when using Storyboard the xib will be resized after the initialization of the
StackViewController. In order to get the correct size of the content controllers I had to add them in
viewWillAppear instead of at initialization to ensure the xib was already resized. Even then it still felt like some parts of the content views did not correctly match their layout. In the end I opted to do everything in code as the solution was simpler.
Apple provides some sample code which takes a different approach, that of using different size view controllers depending on the orientation of the device. The idea being to utilize all the available screen space on each device type (iPhone & iPad) while at the same time making the content visually pleasing. From the Adaptive Elements sample code readme file:
This sample shows how to use UIKit to lay out your app’s elements in different sizes, from full-screen on the smallest iPhone to Multitasking on the biggest iPad. It shows how to make smart decisions about implementing your own design. It also demonstrates how to reuse elements in different sizes, so you can take advantage of all the available space without having to rewrite your entire app.
The final tab is the
CardContainerView controller. This combines two view controllers, one to represent the main content, and the other to be the interactive content. Sliding panels are an app feature you see increasingly nowadays, for example in Apple’s maps on iPhone you have a panel you can slide up/down from the bottom of the screen to search for locations on the map. In our example, the card container acts as the parent view controller, holding a main view controller and a card view controller. The card view controller needs to conform to a protocol in order to handle the interactive transitions. This is our biggest container view controller and it is only 200 lines, 100 lines of which is gesture recognizer code to handle the interactive animation which could be moved into a custom delegate object.
This concept was suggested Ben Sandofsky (of the Halide app) and Dave DeLong as an alternative to using coordinators for application flow. The idea is to use container view controllers as a way to encapsulate application flow. As an example lets consider an image picking flow. Such a flow could consist of multiple steps such as presenting the picker/camera, capturing the image, applying a filter, and then saving or posting the image. This could involve three view controllers and all three would be contained in a single container view controller. The application would only need to present a single view controller with the management of the flow being handled in the container view controller. Essentially the container controller acts as the coordinator for the image picking flow. This eliminates the boilerplate code and communication that can come with coordinators.
There is a lot going on in this sample app and that’s kind of the point. I wanted to show how view controller containment could be used in an real app, instead of focusing on a single screen. Many of the concepts covered were learned from other authors and speakers. View controller containment, tiny networking (from objc.io), flow coordinators (see my previous post). The ability to break problems into small manageable, solvable components, and then re-assemble them into complex solutions is one of the most important aspects of programming. A view controller does not need to manage an entire screen of content, it can manage only a portion of a screen or even a single element if the requirements are complex. Experimenting with new approaches and ideas allows us to validate (or invalidate) our assumptions. The more things we try when we build apps the more we learn.
The final project can be found here: ViewControllerContainment
If you are interested in any of the authors mentioned in this post you can follow them on Twitter at @davedelong, @johnsundell, @twostraws (Paul Hudson), and @khanlou( Soroush Khanlou)
How to move view code out of your view controllers
Using child view controllers as plugins in Swift
Custom container view controllers in Swift
How to use view controller containment
UIKonf18 — MVC is Not Your Problem
Swiss Mobile Developers Association — A Better MCV
AdaptiveElements: Implementing Your Own Adaptive Design with UIKit
Free JSON Site
Network Link Conditioner (simulate slow network)
Socrates Wikipedia Commons
Port Hercule Image
Taj Mahal by Raghu Nayyar on Unsplash
Woman/boots by Dmitriy Ilkevich on Unsplash