首页 > 解决方案 > 如何在没有故事板的容器视图中打开视图控制器

问题描述

我想通过按钮单击在现有视图控制器上打开视图控制器,而不使用情节提要。我该怎么做呢?这就是我的意思:

假设我们有三个可以滚动的视图控制器:

“zeroVC”、“oneVC”和“twoVC”

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

“zeroVC”、“oneVC”和“threeVC”

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

标签: iosswiftcocoa-touch

解决方案


假设我们有四个视图控制器:RedViewControllerGreenViewControllerBlueViewController和一个包含它们的视图控制器,ContainerViewController

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

我们RedViewController是 7 行长:

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

在继续讨论GreenViewControllerand之前BlueViewController,我们将定义protocol SwapViewControllerDelegate

protocol SwapViewControllerDelegate: AnyObject {
  func swap()
}

GreenViewController并且BlueViewController将有一个delegate符合此协议的协议,它将处理交换。我们将ContainerViewController遵守此协议。

请注意,在其继承列表中SwapViewControllerDelegateAnyObject,使其成为仅类协议——因此我们可以使委托变弱,以避免内存保留循环。

以下是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'sbackgroundColor和 the button's titleColor

最后,我们来看看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是将包含子视图控制器、redVCgreenVC和的视图blueVC。我们将使用自动布局,所以不要忘记标记translatesAutoresizingMaskIntoConstraintsfalse.

现在,设置自动布局约束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,请参阅技术说明 TN2154:UIScrollView 和自动布局

现在最重要的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子视图控制器的 's 添加到scrollView. 将子视图控制器的 and 设置为widthAnchorand ,以使它们全屏显示。此外,这也将在屏幕旋转时起作用。heightAnchorview.widthAnchorview.heightAnchor

使用views字典,我们使用 VFL 来设置自动布局约束。我们将放在:greenVC.view的右边,同样放在:上。要固定 和 的垂直位置,请在约束中添加选项。然后应用垂直布局,并设置和的高度:。正如我们在设置水平约束时使用的那样,设置了垂直位置。redVC.viewH:|[redVC][greenVC]|blueVC.viewH:|[redVC][blueVC]|greenVC.viewblueVC.view.alignAllTopredVC.viewgreenVC.viewblueVC.view"V:|[redVC(==greenVC,==blueVC)]|.alignAllTop

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

最后,隐藏greenVC.view,设置greenVC.delegateblueVC.delegateself。那么当然,我们需要ContainerViewController符合SwapViewControllerDelegate

extension ContainerViewController: SwapViewControllerDelegate {
  func swap() {
    greenVC.view.isHidden.toggle()
    blueVC.view.isHidden.toggle()
  }
}

而已!整个项目在这里上传。

我推荐阅读Implementing a Container View Controller,Apple 有详细的文档。(它是用 Objective-C 编写的,但实际上翻译成 Swift 很简单)


推荐阅读