如何在没有情节提要的情况下在容器视图中打开视图控制器

How to open view controllers in a container view without storyboard

我想在不使用故事板的情况下通过单击按钮在现有视图控制器上打开一个视图控制器。我该怎么做呢?这就是我的意思:

假设我们有三个视图控制器,我可以在它们之间滚动:

"zeroVC"、"oneVC" 和 "twoVC"

当我按下 "twoVC" 上的按钮时,我现在想在以下之间滚动:

"zeroVC"、"oneVC" 和 "threeVC"

我尝试通过堆栈溢出查看所有内容,但它们都使用故事板。

假设我们有四个视图控制器:RedViewControllerGreenViewControllerBlueViewController,以及一个包含所有视图控制器的 ContainerViewController.

虽然您提到了一个包含三个子项的滚动视图控制器,但我们会将其设置为两个屏幕以保持简单。 以下方法是可扩展的,因此您可以轻松地将其用于任意数量的视图控制器。

我们的 RedViewController 有 7 行:

class RedViewController: UIViewController {
  override func loadView() {
    let view = UIView()
    view.backgroundColor = .red
    self.view = view
  }
}

在我们继续 GreenViewControllerBlueViewController 之前,我们将定义 protocol SwapViewControllerDelegate:

protocol SwapViewControllerDelegate: AnyObject {
  func swap()
}

GreenViewControllerBlueViewController 将有一个符合此协议的 delegate,它将处理交换。 我们会让ContainerViewController遵守这个协议。

请注意 SwapViewControllerDelegate 在其继承列表中有 AnyObject 使其成为一个 class-only 协议——因此我们可以使委托弱化,以避免内存保留循环。

以下为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()
  }
}

唯一的区别是 viewbackgroundColorbuttontitleColor

最后,我们来看看ContainerViewControllerContainerViewController 有四个属性:

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 是将包含子视图控制器的视图,redVCgreenVCblueVC。 我们将使用自动布局,所以不要忘记将 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([=15=]) }
  }
...
}

我使用了 VFL,但您可以像我们为上面的按钮所做的那样手动设置自动布局约束。 使用自动布局,我们不必自己设置 scrollView 的 contentSize。 有关将自动布局与 UIScrollView 一起使用的更多信息,请参阅 Technical Note TN2154: UIScrollView And Autolayout

现在最重要的setupChildViewControllers():

class ContainerViewController: UIViewController {
...
  private func setupChildViewControllers() {
    [redVC, greenVC, blueVC].forEach { addChild([=16=]) }

    let views = [
      "redVC": redVC.view!,
      "greenVC": greenVC.view!,
      "blueVC": blueVC.view!,
    ]
    views.values.forEach {
      scrollView.addSubview([=16=])
      [=16=].translatesAutoresizingMaskIntoConstraints = false
      [=16=].widthAnchor.constraint(equalTo: view.widthAnchor).isActive = true
      [=16=].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([=16=]) }

    [redVC, greenVC, blueVC].forEach { [=16=].didMove(toParent: self) }

    greenVC.view.isHidden = true

    greenVC.delegate = self
    blueVC.delegate = self
  }
...
}

我们首先将每个 [redVC, greenVC, blueVC] 添加为 ContainerViewController 的子视图控制器。 然后将 view 的子视图控制器添加到 scrollView。 将子视图控制器的 widthAnchorheightAnchor 设置为 view.widthAnchorview.heightAnchor,以使它们全屏显示。 此外,这在屏幕旋转时也有效。

使用views字典,我们使用VFL来设置自动布局约束。 我们将 greenVC.view 放在 redVC.view 的右边:H:|[redVC][greenVC]|blueVC.view 的右边也是如此:H:|[redVC][blueVC]|。 要固定 greenVC.viewblueVC.view 的垂直位置,请将 .alignAllTop 选项添加到约束中。 然后为redVC.view应用垂直布局,并设置greenVC.viewblueVC.view的高度:"V:|[redVC(==greenVC,==blueVC)]|。 设置垂直位置,因为我们在设置水平约束时使用 .alignAllTop

添加 then 作为子视图控制器后,我们应该在子视图控制器上调用 didMove(toParent:) 方法。 (如果您想知道 didMove(toParent:)addChild(_:) 方法做了什么,显然它们做的很少;请参阅 What does addChildViewController actually do? and 。)

最后隐藏greenVC.view,将greenVC.delegateblueVC.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)