Let's assume we have four view controllers: RedViewController
, GreenViewController
, BlueViewController
, and the one to contain them all, ContainerViewController
.
Although you mentioned a scrolling view controller with three children within, we'll make it a two screen setup to keep it simple.
The following approach is scalable, so you would easily adopt it with an arbitrary number of view controllers.
Our RedViewController
is 7 lines long:
class RedViewController: UIViewController {
override func loadView() {
let view = UIView()
view.backgroundColor = .red
self.view = view
}
}
Before we move on to GreenViewController
and BlueViewController
, we will define protocol SwapViewControllerDelegate
:
protocol SwapViewControllerDelegate: AnyObject {
func swap()
}
GreenViewController
and BlueViewController
will have a delegate
that conforms to this protocol, which will handle the swapping.
We will make ContainerViewController
conform to this protocol.
Note that SwapViewControllerDelegate
has the AnyObject
in its inheritance list to make it a class-only protocol–we can thus make the delegate weak, to avoid memory retain cycle.
The following is GreenViewController
:
class GreenViewController: UIViewController {
weak var delegate: SwapViewControllerDelegate?
override func loadView() {
let view = UIView()
view.backgroundColor = .green
self.view = view
}
override func viewDidLoad() {
super.viewDidLoad()
let button = UIButton()
button.setTitle("Swap Me!", for: .normal)
button.setTitleColor(.black, for: .normal)
button.titleLabel?.font = .boldSystemFont(ofSize: 50)
button.addTarget(
self,
action: #selector(swapButtonWasTouched),
for: .touchUpInside)
view.addSubview(button)
// Put button at the center of the view
button.translatesAutoresizingMaskIntoConstraints = false
button.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
button.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
}
@objc private func swapButtonWasTouched(_ sender: UIButton) {
delegate?.swap()
}
}
It has weak var delegate: SwapViewControllerDelegate?
which will handle the swap when the button added in viewDidLoad
is touched, triggering the swapButtonWasTouched
method.
BlueViewController
is implemented likewise:
class BlueViewController: UIViewController {
weak var delegate: SwapViewControllerDelegate?
override func loadView() {
let view = UIView()
view.backgroundColor = .blue
self.view = view
}
override func viewDidLoad() {
super.viewDidLoad()
let button = UIButton()
button.setTitle("Swap Me!", for: .normal)
button.setTitleColor(.white, for: .normal)
button.titleLabel?.font = .boldSystemFont(ofSize: 50)
button.addTarget(
self,
action: #selector(swapButtonWasTouched),
for: .touchUpInside)
view.addSubview(button)
// Put button at the center of the view
button.translatesAutoresizingMaskIntoConstraints = false
button.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
button.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
}
@objc private func swapButtonWasTouched(_ sender: UIButton) {
delegate?.swap()
}
}
The only difference is the view
's backgroundColor
and the button
's titleColor
.
Finally, we'll take a look at ContainerViewController
.
ContainerViewController
has four properties:
class ContainerViewController: UIViewController {
let redVC = RedViewController()
let greenVC = GreenViewController()
let blueVC = BlueViewController()
private lazy var scrollView: UIScrollView = {
let scrollView = UIScrollView()
scrollView.translatesAutoresizingMaskIntoConstraints = false
scrollView.bounces = false
scrollView.isPagingEnabled = true
return scrollView
}()
...
}
scrollView
is the view that will contain child view controllers, redVC
, greenVC
, and blueVC
.
We will use autolayout, so don't forget to mark translatesAutoresizingMaskIntoConstraints
as false
.
Now, setup autolayout constraints of the scrollView
:
class ContainerViewController: UIViewController {
...
private func setupScrollView() {
view.addSubview(scrollView)
let views = ["scrollView": scrollView]
[
NSLayoutConstraint.constraints(
withVisualFormat: "H:|[scrollView]|",
metrics: nil,
views: views),
NSLayoutConstraint.constraints(
withVisualFormat: "V:|[scrollView]|",
metrics: nil,
views: views),
]
.forEach { NSLayoutConstraint.activate($0) }
}
...
}
I used VFL, but you can manually set autolayou constraints as we did for the button above.
Using autolayout, we don't have to set contentSize
of the scrollView ourselves.
For more information about using autolayout with UIScrollView
, see Technical Note TN2154: UIScrollView And Autolayout.
Now the most important setupChildViewControllers()
:
class ContainerViewController: UIViewController {
...
private func setupChildViewControllers() {
[redVC, greenVC, blueVC].forEach { addChild($0) }
let views = [
"redVC": redVC.view!,
"greenVC": greenVC.view!,
"blueVC": blueVC.view!,
]
views.values.forEach {
scrollView.addSubview($0)
$0.translatesAutoresizingMaskIntoConstraints = false
$0.widthAnchor.constraint(equalTo: view.widthAnchor).isActive = true
$0.heightAnchor.constraint(equalTo: view.heightAnchor).isActive = true
}
[
NSLayoutConstraint.constraints(
withVisualFormat: "H:|[redVC][greenVC]|",
options: .alignAllTop,
metrics: nil,
views: views),
NSLayoutConstraint.constraints(
withVisualFormat: "H:|[redVC][blueVC]|",
options: .alignAllTop,
metrics: nil,
views: views),
NSLayoutConstraint.constraints(
withVisualFormat: "V:|[redVC(==greenVC,==blueVC)]|",
metrics: nil,
views: views),
]
.forEach { NSLayoutConstraint.activate($0) }
[redVC, greenVC, blueVC].forEach { $0.didMove(toParent: self) }
greenVC.view.isHidden = true
greenVC.delegate = self
blueVC.delegate = self
}
...
}
We first add each of [redVC, greenVC, blueVC]
as child view controllers of ContainerViewController
.
Then add the view
's of child view controllers to scrollView
.
Set widthAnchor
and heightAnchor
of the child view controllers to be view.widthAnchor
and view.heightAnchor
, in order to make them fullscreen.
Moreover, this will also work when the screen rotates.
Using views
dictionary, we use VFL to set autolayout constraints.
We will put greenVC.view
on the right of redVC.view
: H:|[redVC][greenVC]|
, and similarly for the blueVC.view
: H:|[redVC][blueVC]|
.
To fix the vertical position of greenVC.view
and blueVC.view
, add .alignAllTop
option to the constraints.
Then apply vertical layout for redVC.view
, and set the height of the greenVC.view
and blueVC.view
: "V:|[redVC(==greenVC,==blueVC)]|
.
The vertical position is set, as we used .alignAllTop
while setting the horizontal constraints.
We should call didMove(toParent:)
methods on the child view controllers after we add then as child view controllers.
(If you are wondering about what didMove(toParent:)
and addChild(_:)
methods do, apparently they do very little; see What does addChildViewController actually do? and didMoveToParentViewController and willMoveToParentViewController.)
Finally, hide greenVC.view
, and set greenVC.delegate
and blueVC.delegate
to self
.
Then of course, we need ContainerViewController
to conform to SwapViewControllerDelegate
:
extension ContainerViewController: SwapViewControllerDelegate {
func swap() {
greenVC.view.isHidden.toggle()
blueVC.view.isHidden.toggle()
}
}
That's it!
The entire project is uploaded here.
I recommend reading Implementing a Container View Controller, which is well-documented by Apple. (It is written in Objective-C, but it is actually straightforward to translate into Swift)