本帖子发布于现代 iOS 的早期阶段。它已更新为最新的信息和当前的 Swift 语法。
在今天的 iOS 中,“一切都是容器视图”。这是制作应用程序的基本方式。
一个应用程序可能非常简单,只有一个屏幕。但即使在这种情况下,屏幕上的每个“东西”也都是一个容器视图。
这很容易......
版本说明
2020年。现在,您通常只需从单独的 storyboard 中加载容器视图,这非常简单。它在本文底部进行了解释。如果您是容器视图的新手,请先熟悉“经典样式”(同一 storyboard)容器教程。
2021年。更新语法。使用 Stack Overflow 的新“###”漂亮标题。有关从代码加载的更多详细信息。
(A)将容器视图拖入场景中......
将容器视图拖入场景视图中。(就像您拖入任何元素一样,例如 UIButton。)
容器视图是此图像中的棕色物体。它实际上在您的场景视图内部。
当您将容器视图拖入场景视图中时,Xcode会自动为您提供两件东西:
您在场景视图中获得了容器视图,
您获得了一个全新的 UIViewController
,它只是闲置在白色 Storyboard 的某个地方。
这两者连接在一起,使用下面解释的“共济会符号”!
(B)单击该新视图控制器。(也就是 Xcode 为您在白色区域某处创建的新对象,而不是场景中的对象。)......并更改类别!
就是这么简单。
您完成了。
这里是相同的内容可视化解释。
请注意,在 (A)
处的容器视图。
请注意,在 (B)
处的控制器。
单击 B。(这是 B,而不是 A!)
转到右上方的 Inspector。请注意,它说“UIViewController”。
将其更改为您自己的自定义类,该类是UIViewController。
我有一个Swift类Snap
,它是一个UIViewController
。
在检查器中显示“UIViewController”的位置上键入“Snap”即可替换为Snap。
(像往常一样,当你开始输入“Snap...”时,Xcode会自动完成“Snap”)
就是这样 - 完成了。
如何更改容器视图-比如说,更改为表视图。
因此,当您单击添加容器视图时,Apple会在Storyboard上提供链接的视图控制器。
当前(2019年),默认情况下它会变成UIViewController。
这太傻了:它应该问您需要哪种类型。例如,通常需要表视图。
以下是如何将其更改为其他内容:
在撰写本文时,Xcode默认情况下为您提供了一个UIViewController。假设你想要一个UICollectionViewController而不是它:
(i) 将容器视图拖到场景中。查看Xcode默认提供的Storyboard上的UIViewController。
(ii) 在Storyboard的主白色区域中任意拖动新的UICollectionViewController
。
(iii) 单击场景内的容器视图。单击连接检查器。注意有一个“触发的Segue”。鼠标悬停在“触发的Segue”上,并注意Xcode突出显示所有不需要的UIViewController。
(iv) 单击“x”以实际删除该触发的Segue。
(v) 从那个触发的Segue (viewDidLoad是唯一的选择)开始拖动。拖动跨越Storyboard到新的UICollectionViewController。松开,弹出一个弹出窗口。您必须选择嵌入。
(vi) 简单地删除所有不需要的UIViewController。完成了。
简短版:
删除不需要的UIViewController。
在任何一个Storyboard中放置一个新的UICollectionViewController
。
控制拖动从容器视图的连接 - 触发Segue - viewDidLoad,到你的新控制器。
确保在弹出窗口中选择“嵌入”。
就这么简单。
输入文本标识符...
您将拥有其中一种这些“正方形内外都是正方形”的共济会符号:它位于连接容器视图和视图控制器的曲线线上。
“共济会符号”就是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”)。
要从下面找到位于您上方的“老板”视图控制器(例如示例中的“Dash”),可能会有些困惑。幸运的是,这很简单:
class Snap {
var myBoss:Dash?
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
myBoss = parent as? Dash
}
注意:仅在viewDidAppear
或更晚的时间点工作,不会在viewDidLoad
中工作。
完成了。
重要提示:仅适用于容器视图。
提示:别忘了,它只适用于容器视图。
如今,使用Storyboard标识符在屏幕上弹出新视图已经很常见(就像在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模式仅适用于容器视图。为了在instantiateViewController模式下获得类似的结果,您必须使用协议和委托,并记住委托将是弱链接。
请注意,现在很容易从另一个storyboard动态加载容器视图-请参见下面的最后一节。这通常是最好的方法。
prepareForSegue命名不好...
值得注意的是,"prepareForSegue"是一个非常糟糕的名称! "prepareForSegue"用于两个目的:加载容器视图和在场景之间切换。但实际上,你很少在场景之间进行segue!然而,几乎每个应用程序作为一个常规事项都有许多容器视图。
如果"prepareForSegue"被称为"loadingContainerView"等等,那么它会更有意义。
不止一个...
常见情况是:您在屏幕上有一个小区域,希望显示多个不同的视图控制器之一。例如,四个小部件之一。最简单的方法是:有四个不同的容器视图坐落在同一个相同的区域内。在您的代码中,只需隐藏所有四个并打开您想要可见的那个。
容器视图"from code"...动态将Storyboard加载到容器视图中。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
只是一个固定的视图。它不会改变或调整大小。只有你的应用程序永远不会旋转或发生其他任何变化,才会起作用。通常情况下,您需要添加四个普通约束条件,以便保持地图视图在dynamicContainerView
内,因为它会调整大小。实际上,以下是“世界上最方便的扩展”,任何iOS应用程序都需要使用它:
extension UIView {
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)
}
nil
。特别需要检查self.aboutVC、self.utilityView和self.aboutVC.aboutView这三个对象是否为nil
。 - Abhi Beckert