假设我们有四个视图控制器:RedViewController、GreenViewController、BlueViewController,还有一个包含所有视图控制器,ContainerViewController。
虽然您提到了一个包含三个子项的滚动视图控制器,但我们将使其设置为两个屏幕以保持简单。
以下方法是可扩展的,因此您可以轻松地采用任意数量的视图控制器。
我们的RedViewController 有 7 行:
class RedViewController: UIViewController {
override func loadView() {
let view = UIView()
view.backgroundColor = .red
self.view = view
}
}
在我们继续讨论GreenViewController 和BlueViewController 之前,我们将定义protocol SwapViewControllerDelegate:
protocol SwapViewControllerDelegate: AnyObject {
func swap()
}
GreenViewController 和 BlueViewController 将有一个符合此协议的 delegate,它将处理交换。
我们将使ContainerViewController符合这个协议。
请注意,SwapViewControllerDelegate 在其继承列表中有 AnyObject 以使其成为仅限类的协议——因此我们可以使委托变弱,以避免内存保留循环。
以下是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()
}
}
它有weak var delegate: SwapViewControllerDelegate?,它会在viewDidLoad中添加的按钮被触摸时处理交换,触发swapButtonWasTouched方法。
BlueViewController 同样实现:
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()
}
}
唯一的区别是view 的backgroundColor 和button 的titleColor。
最后,我们来看看ContainerViewController。
ContainerViewController 有四个属性:
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 是包含子视图控制器redVC、greenVC 和blueVC 的视图。
我们将使用自动布局,所以不要忘记将translatesAutoresizingMaskIntoConstraints 标记为false。
现在,设置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) }
}
...
}
我使用了 VFL,但您可以手动设置自动布局约束,就像我们对上面的按钮所做的那样。
使用自动布局,我们不必自己设置滚动视图的contentSize。
有关在 UIScrollView 中使用自动布局的更多信息,请参阅 Technical Note TN2154: UIScrollView And Autolayout。
现在最重要的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
}
...
}
我们首先将每个[redVC, greenVC, blueVC] 添加为ContainerViewController 的子视图控制器。
然后将子视图控制器的view 添加到scrollView。
将子视图控制器的widthAnchor 和heightAnchor 设置为view.widthAnchor 和view.heightAnchor,以使它们全屏显示。
此外,这在屏幕旋转时也可以使用。
使用views 字典,我们使用VFL 来设置自动布局约束。
我们将greenVC.view 放在redVC.view:H:|[redVC][greenVC]| 的右边,blueVC.view:H:|[redVC][blueVC]| 也是如此。
要修复greenVC.view 和blueVC.view 的垂直位置,请将.alignAllTop 选项添加到约束中。
然后为redVC.view应用垂直布局,并设置greenVC.view和blueVC.view的高度:"V:|[redVC(==greenVC,==blueVC)]|。
设置了垂直位置,因为我们在设置水平约束时使用了.alignAllTop。
在添加 then 作为子视图控制器之后,我们应该在子视图控制器上调用 didMove(toParent:) 方法。
(如果您想知道didMove(toParent:) 和addChild(_:) 方法的作用,显然它们作用很小;请参阅What does addChildViewController actually do? 和didMoveToParentViewController and willMoveToParentViewController。)
最后,隐藏greenVC.view,并将greenVC.delegate 和blueVC.delegate 设置为self。
那么当然,我们需要ContainerViewController符合SwapViewControllerDelegate:
extension ContainerViewController: SwapViewControllerDelegate {
func swap() {
greenVC.view.isHidden.toggle()
blueVC.view.isHidden.toggle()
}
}
就是这样!
整个项目上传here。
我建议阅读Implementing a Container View Controller,Apple 对此进行了详细记录。 (它是用 Objective-C 编写的,但实际上翻译成 Swift 很简单)