58

我正在与有自己的子视图作斗争UIViewControllers。我有一个UIViewController带视图(浅粉色)和两个按钮toolbar。我希望在按下第一个按钮时显示蓝色视图,并在按下第二个按钮时显示黄色视图。如果我只是想显示一个视图应该很容易。但是蓝色视图将包含一个表,因此它需要它自己的控制器。那是我的第一堂课。我从这个 SO question开始,在那里我了解到我需要一个用于桌子的控制器。

所以,我要备份并在这里采取一些婴儿步骤。ViewController下面是我的实用程序(主视图控制器)和其他两个控制器(蓝色和黄色)的简单起点图片。想象一下,当实用程序ViewController(主视图)首次显示时,蓝色(默认)视图将显示在粉红色视图所在的位置。用户将能够单击这两个按钮来回移动,并且永远不会显示粉红色的视图。我只希望蓝色视图去粉色视图所在的位置,黄色视图去粉色视图所在的位置。我希望这是有道理的。

简单的故事板图像

我正在尝试使用addChildViewController. 据我所见,有两种方法可以做到这一点:storyboard或以addChildViewController编程方式使用容器视图。我想以编程方式进行。我不想使用 aNavigationController或 Tab 栏。我只想添加控制器并在按下相关按钮时将正确的视图推入粉红色视图。

以下是我到目前为止的代码。我要做的就是在粉红色视图所在的位置显示蓝色视图。从我所见,我应该能够添加addChildViewControllerSubView。这段代码不是为我做的。我的困惑越来越好。有人可以帮我在粉红色视图的位置显示蓝色视图吗?

除了在 viewDidLoad 中显示蓝色视图之外,此代码不打算做任何事情。

IDUtilityViewController.h

#import <UIKit/UIKit.h>

@interface IDUtilityViewController : UIViewController
@property (strong, nonatomic) IBOutlet UIView *utilityView;
@end

IDUtilityViewController.m

#import "IDUtilityViewController.h"
#import "IDAboutViewController.h"

@interface IDUtilityViewController ()
@property (nonatomic, strong) IDAboutViewController *aboutVC;
@end

@implementation IDUtilityViewController

- (void)viewDidLoad
{
    [super viewDidLoad];

    self.aboutVC = [[IDAboutViewController alloc]initWithNibName:@"AboutVC" bundle:nil];
    [self addChildViewController:self.aboutVC];
    [self.aboutVC didMoveToParentViewController:self];
    [self.utilityView addSubview:self.aboutVC.aboutView];
}

@end

- - - - - - - - - - - - - 编辑 - - - - - - - - - - - - --------

self.aboutVC.aboutView 为零。但我把它连接到storyboard. 我还需要实例化它吗?

在此处输入图像描述

4

2 回答 2

142

这篇文章可以追溯到现代 iOS 的早期。它使用当前信息和当前 Swift 语法进行更新。

在今天的 iOS 中,“一切都是容器视图”。这是您今天制作应用程序的基本方式。

一个应用程序可能非常简单,以至于它只有一个屏幕。但即使在这种情况下,屏幕上的每个“事物”都是一个容器视图。

就这么容易...

版本说明

2020 年。如今,您通常只需从单独的故事板加载容器视图,这非常容易。在这篇文章的底部有解释。如果您不熟悉容器视图,不妨先熟悉“经典风格”(“相同故事板”)容器教程。

2021. 更新语法。使用 SO 的新 '###' 漂亮的头条新闻。有关从代码加载的更多详细信息。


(A) 将容器视图拖入您的场景...

将容器视图拖到场景视图中。(就像您拖入任何元素,例如 UIButton。)

容器视图是此图像中的棕色物体。它实际上您的场景视图中。

在此处输入图像描述

当您将容器视图拖入场景视图时,Xcode 会自动为您提供两件事

  1. 您在场景视图中获得容器视图,并且,

  2. 你会得到一个全新的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。你完成了。

简洁版本:

  • 删除不需要的 UIViewController。

  • 在故事板上的任何地方放置一个新UICollectionViewController的。

  • 容器视图的Connections - Trigger Segue - viewDidLoad按住 Control 并拖动到您的新控制器。

  • 请务必在弹出窗口中选择“嵌入”。

就这么容易。


输入文本标识符...

您将拥有这些“正方形中的正方形”共济会符号事物之一:它位于连接容器视图和视图控制器的“弯曲线”上。

“共济会符号”是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()以避免出现一行代码!)

提醒订单必须是

  • 添加孩子
  • 添加实际视图
  • 调用 didMove

删除其中之一?

您已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)
}
于 2014-05-01T07:49:27.927 回答
5

我看到两个问题。首先,由于您在情节提要中制作控制器,因此您应该使用 而instantiateViewControllerWithIdentifier:不是来实例化它们initWithNibName:bundle:。其次,当您将视图添加为子视图时,您应该给它一个框架。所以,

- (void)viewDidLoad
{
    [super viewDidLoad];

    self.aboutVC = [self.storyboard instantiateViewControllerWithIdentifier:@"aboutVC"]; // make sure you give the controller this same identifier in the storyboard
    [self addChildViewController:self.aboutVC];
    [self.aboutVC didMoveToParentViewController:self];
    self.aboutVC.view.frame = self.utilityView.bounds;
    [self.utilityView addSubview:self.aboutVC.aboutView];
}
于 2014-05-01T04:09:24.500 回答