Containment of view controllers is an often used technique in UIKit.
It allows to drastically reduce the complexity of a view controller, very easily, simply by splitting it into multiple smaller ones.
Having smaller view controllers, with a reduced functional scope, makes testing easier, so as reusability across your codebase.
Pratical example
Let’s say you have a video player in your app. The first third of the screen would be used for the actual video player, with the controls and everything (play/pause, time indicator), and the rest of the screen would display the playlist. A very common layout - the exact one you would find on the YouTube app.
I would be very suitable to split this PlayerViewController into :
- VideoPlayerViewController
- PlaylistViewController
The two smaller view controller would have simple interfaces like delegates :
func playerDidEndPlaying(player: Player, media: Media)
func playlistDidSelect(playlist: Playlist, media: Media)
Allowing you to build a “main” view controller that would implement these methods and be used as data source for your two child view controller.
The best part is if you wish you swap the playlist for a comment section - say, after a user action, you would just replace the Playlist child view controller by a CommentViewController.
You would then be able to test independently the comment, player and playlists view controllers.
As you can see, view controller containment is great for reusability, a better code organization, reducing complexity and better testing.
Here is a very helpful extension to add and remove a child view controller.
extension UIViewController {
/// Add a child UIViewController to a given UIView. If `view` is set to nil or not set, it will add the child the ViewController's view.
/// - Parameters:
/// - child: Child view controller as a UIViewController
/// - view: Target UIVew that will contain the child
func add(_ child: UIViewController, view: UIView? = nil) {
let view: UIView = view ?? self.view
addChild(child)
child.view.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(child.view)
NSLayoutConstraint.activate([
child.view.topAnchor.constraint(equalTo: view.topAnchor),
child.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
child.view.leftAnchor.constraint(equalTo: view.leftAnchor),
child.view.rightAnchor.constraint(equalTo: view.rightAnchor),
])
child.didMove(toParent: self)
}
/// Remove a child viewController from it's container
func remove() {
guard parent != nil else {
return
}
willMove(toParent: nil)
view.removeFromSuperview()
removeFromParent()
}
}
A simple use-case based on the given example with a PlayerViewController would render as follow :
class PlayerViewController: UIViewController {
let player = VideoPlayerViewController()
let playlist = PlaylistViewController()
let comment = CommentViewController()
let playerViewContainer = UIView()
let bottomViewContainer = UIView()
override func viewDidLoad() {
super.viewDidLoad()
self.add(self.player, view: self.playerViewContainer)
self.add(self.playlist, view: self.bottomViewContainer)
}
func showComment() {
self.playlist.remove()
self.add(self.comment, view: self.bottomViewContainer)
}
}
As you can see, it is very simple and makes the "main" view controller clean, and free from player, playlist or comment related business logic!
Aside of the reusability and cleanness of view controllers containment, the main benefit is that the child view controllers can interact with the parent's life cycle (didLoad
, didAppear
...).
I hope you enjoyed this little post. I like sharing about my way of coding, if you have any question, feel free to hit me on Twitter or use the comment section :)
Happy coding!
Top comments (0)