如何在Objective-C中添加一个具有自己UIViewController的子视图?

59

我正在努力解决具有自己的UIViewControllers的子视图问题。我有一个带有视图(浅粉色)和两个toolbar上按钮的UIViewController。当按下第一个按钮时,我希望蓝色视图显示,当按下第二个按钮时,黄色视图显示。如果我只想显示一个视图,这应该很容易。但是蓝色视图将包含一个表格,因此它需要它自己的控制器。那就是我的第一课。我从这个SO问题开始,在那里我学到了我需要一个表格的控制器。

所以,我要退后几步。以下是我实用程序ViewController(主视图控制器)和其他两个控制器(蓝色和黄色)的简单起点图片。想象一下,当实用程序ViewController(主视图)首次显示时,蓝色(默认)视图将显示在粉色视图所在的位置。用户将能够单击这两个按钮来前后移动,并且永远不会显示粉色视图。我只想让蓝色视图进入粉红色视图的位置,黄色视图进入粉红色视图的位置。我希望这讲得通。

Simple Storyboard image

我正在尝试使用addChildViewController。从我看到的情况来看,有两种方法可以做到这一点:在storyboard中使用容器视图或以编程方式使用addChildViewController。我想以编程方式做到这一点。我不想使用NavigationControllerTab bar。我只是想添加控制器,并在单击相关按钮时将正确的视图插入粉色视图。

以下是我目前拥有的代码。我只想在粉色视图的位置显示蓝色视图。从我所看到的情况来看,我应该能够addChildViewController和addSubView。但是这段代码对我来说并没有这样做。我的困惑掩盖了我。有人能帮我将蓝色视图显示在粉色视图的位置吗?

这段代码的目的不是做任何其他事情,只是在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中将其连接了,我还需要实例化它吗?

图片描述


你遇到的实际问题是什么? - Abhi Beckert
当视图加载时,我看到了粉色的视图。我猜我一定没有编写正确的代码。 - Patricia
好的,首先,逐行查看代码,并确保没有任何对象是nil。特别需要检查self.aboutVC、self.utilityView和self.aboutVC.aboutView这三个对象是否为nil - Abhi Beckert
你是正确的。self.aboutVC.aboutView 是空的。但是我在故事板中已经将所有东西连接好了。我会更新我的问题并附上图片。 - Patricia
你的第一个按钮是什么?你的第二个按钮呢?当你说“蓝色视图在粉色视图上方”时,你指的是蓝色视图出现在屏幕上时,“X”和“?”仍然悬挂在视图的顶部吗? - mfaani
2个回答

147

本帖子发布于现代 iOS 的早期阶段。它已更新为最新的信息和当前的 Swift 语法。

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

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

这很容易......

版本说明

2020年。现在,您通常只需从单独的 storyboard 中加载容器视图,这非常简单。它在本文底部进行了解释。如果您是容器视图的新手,请先熟悉“经典样式”(同一 storyboard)容器教程。

2021年。更新语法。使用 Stack Overflow 的新“###”漂亮标题。有关从代码加载的更多详细信息。


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

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

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

enter image description here

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

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

  2. 您获得了一个全新的 UIViewController,它只是闲置在白色 Storyboard 的某个地方

这两者连接在一起,使用下面解释的“共济会符号”!


(B)单击该新视图控制器。(也就是 Xcode 为您在白色区域某处创建的新对象,而不是场景中的对象。)......并更改类别!

就是这么简单。

您完成了。


这里是相同的内容可视化解释。

请注意,在 (A) 处的容器视图

请注意,在 (B) 处的控制器

shows a container view and the associated view controller

单击 B。(这是 B,而不是 A!)

转到右上方的 Inspector。请注意,它说“UIViewController”。

enter image description here

将其更改为您自己的自定义类,该类是UIViewController。

enter image description here

我有一个Swift类Snap,它是一个UIViewController

enter image description here

在检查器中显示“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

enter image description here

通过单击“共济会符号”来选择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”),可能会有些困惑。幸运的是,这很简单:

// 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中工作。

完成了。


重要提示:仅适用于容器视图。

提示:别忘了,它只适用于容器视图。

如今,使用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 {
    
    // 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)
}

我到办公室后会尝试这个。谢谢。我仍然认为你很棒!!! :-) - Patricia
你真有趣,乔。使用容器视图解决了我该死的问题。:-) 我想以代码方式完成它,因为我曾经用过代码(一次......并且已经一年多了)。我像其他人一样开始使用Storyboard。我的前同事是高级iOS开发人员,他们不想使用storyboards,因为我们有太多的窗口。因此,我的故事板和segue经验非常薄弱。最近,我一直在处理AVFoundation和相机控件,而不是UI方面的内容。当你被甩来甩去时,很容易忘记基础知识。 - Patricia
嘿,乔,我有一个新的最爱。它让我想起了你! :-) 这里:https://dev59.com/vWsy5IYBdhLWcg3wyw-i - Patricia
在我看来,如果您想使用IB,则这是最清晰的方法。比尝试通过nibs实现要好得多。 - abc123
2
iOS 9的Storyboard References使容器视图变得更加强大。您可以在任何地方定义可重用的视图(控制器),并从多个模块化storyboard中的任何容器视图引用它。 - SimplGy
显示剩余4条评论

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];
}

2
设置框架(frame)是必要的吗?还是根据故事板会给视图默认的框架? - Brian
2
@Brian,如果你添加的视图是全屏的话,可能不需要这样做,但我通常会这样做以确保。此外,在这种情况下,我不确定self.utilityView是否是完整的视图还是工具栏下面的部分视图,如果是后者,则可能需要设置框架。 - rdelmar
@rdelmar - 谢谢您的信息。我想知道是否应该使用instantiateViewControllerWithIdentifier,但有点困惑。另外,是的,self.utilityView不是完整的视图。所有3个子视图的大小都相同,所以我认为不需要设置框架大小。当我到办公室时,我会尝试您的建议。 - Patricia
2
@JoeBlow,OP提到在Storyboard中使用容器视图,但是他们想要在代码中实现。 - rdelmar

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接