这篇文章可以追溯到现代 iOS 的早期。它使用当前信息和当前 Swift 语法进行更新。
在今天的 iOS 中,“一切都是容器视图”。这是您今天制作应用程序的基本方式。
一个应用程序可能非常简单,以至于它只有一个屏幕。但即使在这种情况下,屏幕上的每个“事物”都是一个容器视图。
就这么容易...
版本说明
2020 年。如今,您通常只需从单独的故事板加载容器视图,这非常容易。在这篇文章的底部有解释。如果您不熟悉容器视图,不妨先熟悉“经典风格”(“相同故事板”)容器教程。
2021. 更新语法。使用 SO 的新 '###' 漂亮的头条新闻。有关从代码加载的更多详细信息。
(A) 将容器视图拖入您的场景...
将容器视图拖到场景视图中。(就像您拖入任何元素,例如 UIButton。)
容器视图是此图像中的棕色物体。它实际上在您的场景视图中。
当您将容器视图拖入场景视图时,Xcode 会自动为您提供两件事:
您在场景视图中获得容器视图,并且,
你会得到一个全新的UIViewController
,它就在你的故事板白色的某个地方。
两者都与“共济会符号事物”有关 - 解释如下!
(B) 点击那个新的视图控制器。(所以这是 Xcode 在白色区域的某个地方为您制作的新东西,而不是您场景中的东西。)......并且,改变类!
真的就是这么简单。
你完成了。
这是视觉上解释的同一件事。
注意容器(A)
视图。
请注意 处的控制器(B)
。
点击 B。(那是 B - 不是 A!)
转到右上角的检查员。注意它说“UIViewController”
将其更改为您自己的自定义类,即 UIViewController。
所以,我有一个 Swift 类Snap
,它是一个UIViewController
.
因此,在检查器中显示“UIViewController”的地方我输入了“Snap”。
(像往常一样,当你开始输入“Snap...”时,Xcode 会自动完成“Snap”。)
这就是它的全部 - 你已经完成了。
如何更改容器视图 - 例如,更改为表格视图。
因此,当您单击添加容器视图时,Apple 会自动为您提供一个链接视图控制器,它位于情节提要上。
目前(2019 年)它恰好将其设为UIViewController
默认值。
这很愚蠢:它应该询问您需要哪种类型。例如,通常您需要一个表格视图。
以下是如何将其更改为不同的内容:
在撰写本文时,XcodeUIViewController
默认为您提供了一个。假设您想要一个UICollectionViewController
:
(i) 将容器视图拖到您的场景中。查看 Xcode 默认为您提供的故事板上的 UIViewController。
(ii) 将一个新UICollectionViewController
的拖到情节提要的主要白色区域的任何地方。
(iii) 单击场景内的容器视图。单击连接检查器。请注意,有一个“触发的 Segue”。 将鼠标悬停在“Triggered Segue”上,注意 Xcode突出显示了所有不需要的 UIViewController。
(iv) 单击“x”以实际删除触发的 Segue。
(v)从触发的 Segue 中拖动(viewDidLoad 是唯一的选择)。将故事板拖到新的 UICollectionViewController 中。放开并出现一个弹出窗口。您必须选择embed。
(vi) 只需删除所有不需要的 UIViewController。你完成了。
简洁版本:
就这么容易。
输入文本标识符...
您将拥有这些“正方形中的正方形”共济会符号事物之一:它位于连接容器视图和视图控制器的“弯曲线”上。
“共济会符号”是segue。
通过单击“共济会符号”来选择segue。
向右看。
您必须输入 segue 的文本标识符。
你决定名字。它可以是任何文本字符串。一个好的选择通常是“segueClassName”。
如果你遵循这个模式,你所有的 segue 将被称为 segueClockView、seguePersonSelector、segueSnap、segueCards 等等。
接下来,您在哪里使用该文本标识符?
如何连接“到”子控制器...
然后,在代码中,在整个场景的 ViewController 中执行以下操作。
假设您在场景中有三个容器视图。每个容器视图都有一个不同的控制器,比如“Snap”、“Clock”和“Other”。
最新语法
var snap:Snap?
var clock:Clock?
var other:Other?
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if (segue.identifier == "segueSnap")
{ snap = (segue.destination as! Snap) }
if (segue.identifier == "segueClock")
{ clock = (segue.destination as! Clock) }
if (segue.identifier == "segueOther")
{ other = (segue.destination as! Other) }
}
就是这么简单。prepareForSegue
您使用调用连接一个变量以引用控制器。
如何在“其他方向”上连接,直到父...
假设您在“放入”容器视图中的控制器(示例中为“Snap”)。
到达您上方的“boss”视图控制器(示例中的“Dash”)可能会令人困惑。幸运的是,它很简单:
// Dash is the overall scene.
// Here we are in Snap. Snap is one of the container views inside Dash.
class Snap {
var myBoss:Dash?
override func viewDidAppear(_ animated: Bool) { // MUST be viewDidAppear
super.viewDidAppear(animated)
myBoss = parent as? Dash
}
关键:仅适用于viewDidAppear
或以后。不会在viewDidLoad
.
你完成了。
重要提示:这只适用于容器视图。
提示,不要忘记,这仅适用于容器视图。
现在有了故事板标识符,在屏幕上弹出新视图是司空见惯的(就像在 Android 开发中一样)。所以,假设用户想要编辑一些东西......
// let's just pop a view on the screen.
// this has nothing to do with container views
//
let e = ...instantiateViewController(withIdentifier: "Edit") as! Edit
e.modalPresentationStyle = .overCurrentContext
self.present(e, animated: false, completion: nil)
使用容器视图时,保证Dash 将成为 Snap 的父视图控制器。
但是,当您使用 instantiateViewController 时,情况并非如此。
非常令人困惑的是,在 iOS 中,父视图控制器与实例化它的类无关。(可能相同,但通常不一样。)该self.parent
模式仅适用于容器视图。
(对于 instanceViewController 模式中的类似结果,您必须使用协议和委托,记住委托将是一个薄弱环节。)
请注意,现在从另一个故事板动态加载容器视图非常容易 - 请参阅下面的最后一节。这通常是最好的方法。
prepareForSegue 名字不好...
值得注意的是,“prepareForSegue”是一个非常糟糕的名字!
“prepareForSegue”用于两个目的:加载容器视图,以及在场景之间进行 segue。
但在实践中,你很少在场景之间切换!而几乎每个应用程序都有很多很多的容器视图,这是理所当然的。
如果将“prepareForSegue”称为“loadingContainerView”之类的名称会更有意义。
超过一个...
一种常见的情况是:您在屏幕上有一个小区域,您想在其中显示许多不同的视图控制器中的一个。例如,四个小部件之一。
最简单的方法是:让四个不同的容器视图都位于同一个区域内。在您的代码中,只需隐藏所有四个并打开您想要显示的那个。
简单的。
容器视图“来自代码”...
...动态地将故事板加载到容器视图中。
2019+ 语法
假设您有一个情节提要文件“Map.storyboard”,情节提要 ID 是“MapID”,情节提要是您Map
班级的视图控制器。
let map = UIStoryboard(name: "Map", bundle: nil)
.instantiateViewController(withIdentifier: "MapID")
as! Map
在你的主场景中有一个普通的 UIView:
@IBOutlet var dynamicContainerView: UIView!
苹果在这里解释了添加动态容器视图必须做的四件事
addChild(map)
map.view.frame = dynamicContainerView.bounds
dynamicContainerView.addSubview(map.view)
map.didMove(toParent: self)
(以该顺序。)
并删除该容器视图:
map.willMove(toParent: nil)
map.view.removeFromSuperview()
map.removeFromParent()
(也按这个顺序。)就是这样。
但是请注意,在该示例中,这dynamicContainerView
只是一个固定视图。它不会更改或调整大小。这仅在您的应用从不旋转或其他任何情况下才有效。通常,您必须添加四个常用约束,以便在调整大小时将 map.view 简单地保留在 dynamicContainerView 中。事实上,这是任何 iOS 应用程序都需要的“世界上最方便的扩展”,
extension UIView {
// it's basically impossible to make an iOS app without this!
func bindEdgesToSuperview() {
guard let s = superview else {
preconditionFailure("`superview` nil in bindEdgesToSuperview")
}
translatesAutoresizingMaskIntoConstraints = false
leadingAnchor.constraint(equalTo: s.leadingAnchor).isActive = true
trailingAnchor.constraint(equalTo: s.trailingAnchor).isActive = true
topAnchor.constraint(equalTo: s.topAnchor).isActive = true
bottomAnchor.constraint(equalTo: s.bottomAnchor).isActive = true
}
}
因此,在任何真正的应用程序中,上面的代码都是:
addChild(map)
dynamicContainerView.addSubview(map.view)
map.view.bindEdgesToSuperview()
map.didMove(toParent: self)
(有些人甚至进行扩展.addSubviewAndBindEdgesToSuperview()
以避免出现一行代码!)
提醒订单必须是
删除其中之一?
您已map
动态添加到持有人,现在您想将其删除。正确且唯一的顺序是:
map.willMove(toParent: nil)
map.view.removeFromSuperview()
map.removeFromParent()
通常你会有一个持有者视图,并且你想要交换不同的控制器。所以:
var current: UIViewController? = nil
private func _install(_ newOne: UIViewController) {
if let c = current {
c.willMove(toParent: nil)
c.view.removeFromSuperview()
c.removeFromParent()
}
current = newOne
addChild(current!)
holder.addSubview(current!.view)
current!.view.bindEdgesToSuperview()
current!.didMove(toParent: self)
}