如何在 Objective-C 中添加一个拥有自己的 UIViewController 的子视图? [英] How to add a subview that has its own UIViewController in Objective-C?

查看:20
本文介绍了如何在 Objective-C 中添加一个拥有自己的 UIViewController 的子视图?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在为拥有自己的 UIViewControllers 的子视图而苦苦挣扎.我有一个 UIViewController 带有视图(浅粉色)和 toolbar 上的两个按钮.我希望在按下第一个按钮时显示蓝色视图,并在按下第二个按钮时显示黄色视图.如果我只想显示视图应该很容易.但是蓝色视图将包含一个表,因此它需要它自己的控制器.那是我的第一堂课.我从

点击 B.(那是 B - 不是 A!)

转到右上角的检查员.注意它说UIViewController"

将其更改为您自己的自定义类,即 UIViewController.

所以,我有一个 Swift 类 Snap,它是一个 UIViewController.

所以它说UIViewController"的地方在 Inspector 中,我输入了Snap".

(像往常一样,当您开始输入Snap..."时,Xcode 会自动完成Snap".)

这就是全部 - 你已经完成了.


如何将容器视图更改为表格视图.

因此,当您单击添加容器视图时,Apple 会自动为您提供一个链接的视图控制器,位于故事板上.

目前(2019 年)它碰巧默认为 UIViewController.

这很愚蠢:它应该询问您需要哪种类型.例如,你经常需要一个表格视图.

以下是如何更改为不同的内容:

<块引用>

在撰写本文时,Xcode 默认为您提供一个 UIViewController.假设你想要一个 UICollectionViewController 来代替:

(i) 将容器视图拖到您的场景中.查看 Xcode 默认为您提供的故事板上的 UIViewController.

(ii) 将一个新的 UICollectionViewController 拖到故事板的主要白色区域的任何位置.

(iii) 单击场景内的容器视图.单击连接检查器.请注意,有一个触发转场".鼠标悬停触发转场"并注意 Xcode 突出显示所有不需要的 UIViewController.

(iv) 点击x"实际上删除那个触发的转场.

(v) DRAG 从那个 Triggered Segue(viewDidLoad 是唯一的选择).拖过故事板到你的新 UICollectionViewController.放手,会出现一个弹出窗口.您必须选择嵌入.

(vi) 只需删除所有不需要的 UIViewController.大功告成.

简短版本:

  • 删除不需要的 UIViewController.

  • 在情节提要的任意位置放置一个新的 UICollectionViewController.

  • Control-drag容器视图的 Connections - Trigger Segue - viewDidLoad,到您的新控制器.

  • 一定要选择嵌入";在弹出窗口中.

就这么简单.


正在输入文本标识符...

您将拥有其中一个正方形中的正方形"共济会象征物:它位于弯曲线"上;将您的容器视图与视图控制器连接起来.

共济会符号"事情是segue.

通过点击共济会符号"来选择转场东西.

向右看.

必须为转场输入文本标识符.

名称由您决定.它可以是任何文本字符串.一个不错的选择通常是segueClassName".

如果你遵循这个模式,你所有的 segue 都将被称为 segueClockView、seguePersonSelector、segueSnap、segueCards 等等.

接下来,您在哪里使用该文本标识符?


如何连接到子控制器...

然后,在代码中,在整个场景的ViewController中执行以下操作.

假设场景中有三个容器视图.每个容器视图都有一个不同的控制器,比如Snap"、Clock"等等.和其他".

最新语法

var snap:Snap?var 时钟:时钟?var other:其他?覆盖func准备(对于segue:UIStoryboardSegue,发件人:任何?){if (segue.identifier == "segueSnap"){ snap = (segue.destination as! Snap) }if (segue.identifier == "segueClock"){时钟=(segue.destination as!时钟)}if (segue.identifier == "segueOther"){ 其他 = (segue.destination as! 其他) }}

就是这么简单.使用 prepareForSegue 调用连接一个变量以引用控制器.


如何在另一个方向"上连接到父级...

说你在"放置在容器视图中的控制器(在示例中为Snap").

找到老板"可能会令人困惑您上方的视图控制器(示例中的Dash").幸运的是,就是这么简单:

//Dash 是整体场景.//这里我们在 Snap 中.Snap 是 Dash 中的容器视图之一.类捕捉{var myBoss:破折号?覆盖 func viewDidAppear(_ animation: Bool) {//必须是 viewDidAppearsuper.viewDidAppear(动画)myBoss = 父母身份?短跑}

严重:仅适用于 viewDidAppear 或更高版本.在 viewDidLoad 中不起作用.

大功告成.


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

提示,不要忘记,这只适用于容器视图.

现在有了故事板标识符,在屏幕上弹出新视图是司空见惯的(而不是在 Android 开发中).因此,假设用户想要编辑某些内容...

//让我们在屏幕上弹出一个视图.//这与容器视图无关//让 e = ...instantiateViewController(withIdentifier: "Edit") as!编辑e.modalPresentationStyle = .overCurrentContextself.present(e, 动画: false, 完成: nil)

使用容器视图时,保证 Dash 将成为 Snap 的父视图控制器.

但是,当您使用 instantiateViewController 时,不一定是这种情况.

令人困惑的是,在 iOS 中,父视图控制器与实例化它的类无关.(它可能相同,但通常不相同.)self.parent 模式用于容器视图.>

(对于instantiateViewController 模式中的类似结果,您必须使用协议和委托,记住委托将是一个薄弱环节.)

请注意,如今从另一个故事板动态加载容器视图非常容易 - 请参阅下面的最后一节.这通常是最好的方法.


prepareForSegue 名字不好...

值得注意的是prepareForSegue"这是一个非常糟糕的名字!

prepareForSegue"用于两个目的:加载容器视图,以及在场景之间切换.

但在实践中,您很少在场景之间切换!而几乎每个应用程序都有很多很多容器视图,这是理所当然的.

如果prepareForSegue"更有意义被称为loadingContainerView"之类的东西.


不止一个...

一种常见的情况是:您在屏幕上有一个小区域,您想在其中显示多个不同视图控制器中的一个.例如,四个小部件之一.

最简单的方法是:让四个不同的容器视图都位于相同的区域.在您的代码中,只需隐藏所有四个并打开您希望可见的一个.

简单.


容器视图来自代码"...

... 将 Storyboard 动态加载到容器视图中.

2019+ 语法

假设您有一个故事板文件Map.storyboard",故事板 ID 是MapID",而故事板是您的 Map 类的视图控制器.

let map = UIStoryboard(name: "Map", bundle: nil).instantiateViewController(withIdentifier: "MapID")作为!地图

在你的主场景中有一个普通的 UIView:

@IBOutlet var dynamicContainerView:UIView!

Apple 在此处解释了您必须要做的四件事添加动态容器视图

addChild(map)map.view.frame = dynamicContainerView.boundsdynamicContainerView.addSubview(map.view)map.didMove(toParent: self)

(按那个顺序.)

并删除该容器视图:

map.willMove(toParent: nil)map.view.removeFromSuperview()map.removeFromParent()

(也是按这个顺序.)就是这样.

但是请注意,在该示例中,dynamicContainerView 只是一个固定视图.它不会改变或调整大小.这仅在您的应用程序从不旋转或其他任何东西时才有效.通常,您必须添加四个常用约束,以便在调整大小时将 map.view 保持在 dynamicContainerView 中.事实上,这里是世界上最方便的扩展"在任何 iOS 应用中都需要哪一个,

扩展 UIView {//没有这个,基本上不可能制作一个 iOS 应用程序!func bindEdgesToSuperview() {守卫让 s = superview else {preconditionFailure("`superview` nil in bindEdgesToSuperview")}translatesAutoresizingMaskIntoConstraints = falseLeadershipAnchor.constraint(equalTo: s.leadingAnchor).isActive = truetrailingAnchor.constraint(equalTo: s.trailingAnchor).isActive = truetopAnchor.constraint(equalTo: s.topAnchor).isActive = truebottomAnchor.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?= 零私有 func _install(_ newOne: UIViewController) {如果让 c = 当前 {c.willMove(toParent: nil)c.view.removeFromSuperview()c.removeFromParent()}当前 = 新的addChild(当前!)holder.addSubview(current!.view)当前!.view.bindEdgesToSuperview()当前!.didMove(toParent: self)}

I am struggling with subviews that have their own UIViewControllers. I have a UIViewController with a view (light pink) and two buttons on a toolbar. I want blue view to display when the first button is pressed and the yellow view to display with the second button is pressed. Should be easy if I just wanted to display a view. But the blue view will contain a table, so it needs it's own controller. That was my first lesson. I started off with this SO question where I learned I needed a controller for the table.

So, I am going to back up and take some baby steps here. Below is a picture of a simple starting point with my Utility ViewController (the main view controller) and the other two controllers (blue and yellow). Imagine that when the Utility ViewController (the main view) is first displayed the blue (default) view will be displayed where the pink view is located. Users will be able to click the two buttons to go back and forth and the pink view will NEVER be displayed. I just want the blue view to go where the pink view is and the yellow view to go where the pink view is. I hope this makes sense.

I'm trying to use addChildViewController. From what I have seen, there are two ways to do this: The Container View in the storyboard or addChildViewController programmatically. I want to do it programmatically. I don't want to use a NavigationController or a Tab bar. I just want to add the controllers and shove the correct view into the pink view when the associated button is pressed.

Below is the code I have so far. All I want to do is display the blue view where the pink view is. From what I have seen I should be able to just addChildViewController and addSubView. This code is not doing that for me. My confusion is getting the better of me. Can somebody help me get the blue view displayed where the pink view is?

This code is not intended to do anything other than display the blue view in 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

--------------------------EDIT------------------------------

The self.aboutVC.aboutView is nil. But I wired it up in the storyboard. Do I still need to instantiate it?

解决方案

This post dates from the early days of modern iOS. It is updated with current information and the current Swift syntax.

In iOS today "everything is a container view". It is the basic way you make apps today.

An app may be so simple that it has just the one screen. But even in that case, each "thing" on the screen is a container view.

It's this easy...

version notes

2020. These days you usually just load a container view from a separate storyboard, which is dead easy. It's explained at the bottom of this post. If you are new to container views, maybe familiarize with the 'classic style' ('same storyboard') container tutorial first.

2021. Updated syntax. Used SO's new '###' pretty headlines. More detail on loading from code.


(A) Drag a container view in to your scene...

Drag a container view into your scene view. (Just as you would drag in any element such as a UIButton.)

The container view is the brown thing in this image. It is actually inside your scene view.

When you drag a container view into your scene view, Xcode automatically gives you two things:

  1. You get the container view inside your scene view, and,

  2. you get a brand-new UIViewController which is just sitting around somewhere on the white of your storyboard.

The two are connected with the "Masonic Symbol Thing" - explained below!


(B) Click on that new view controller. (So that's the new thing Xcode made for you somewhere on the white area, not the thing inside your scene.) ... and, change the class!

It's really that simple.

You're done.


Here's the same thing explained visually.

Notice the container view at (A).

Notice the controller at (B).

Click on B. (That's B - not A!)

Go to the inspector at the top right. Notice it says "UIViewController"

Change it to your own custom class, which is a UIViewController.

So, I have a Swift class Snap which is a UIViewController.

So where it says "UIViewController" in the Inspector I typed in "Snap".

(As usual, Xcode will auto-complete "Snap" as you start typing "Snap...".)

That's all there is to it - you're done.


How to change the container view - say, to a table view.

So when you click to add a container view, Apple automatically gives you a linked view controller, sitting on the storyboard.

Currently (2019) it happens to make it a UIViewController by default.

That's silly: it should ask which type you need. For example, often you need a table view.

Here's how to change it to something different:

At the time of writing, Xcode gives you a UIViewController by default. Let's say you want a UICollectionViewController instead:

(i) Drag a container view in to your scene. Look at the UIViewController on the storyboard which Xcode gives you by default.

(ii) Drag a new UICollectionViewController to anywhere on the main white area of the storyboard.

(iii) Click the container view inside your scene. Click the connections inspector. Notice there is one "Triggered Segue". Mouse over the "Triggered Segue" and notice that Xcode highlights all of the unwanted UIViewController.

(iv) Click the "x" to actually delete that Triggered Segue.

(v) DRAG from that Triggered Segue (viewDidLoad is the only choice). Drag across the storyboard to your new UICollectionViewController. Let go and a pop-up appears. You must select embed.

(vi) Simply delete all of the unwanted UIViewController. You're done.

Short version:

  • delete the unwanted UIViewController.

  • Put a new UICollectionViewController anywhere on the storyboard.

  • Control-drag from the container view's Connections - Trigger Segue - viewDidLoad, to, your new controller.

  • Be sure to select "embed" on the popup.

It's that easy.


Entering the text identifier...

You will have one of these "square in a square" Masonic symbol things: it is on the "bendy line" connecting your container view with the view controller.

The "masonic symbol" thing is the segue.

Select the segue by clicking on the "masonic symbol" thing.

Look to your right.

You MUST type in a text identifier for the segue.

You decide on the name. It can be any text string. A good choice is often "segueClassName".

If you follow that pattern, all your segues will be called segueClockView, seguePersonSelector, segueSnap, segueCards and so on.

Next, where do you use that text identifier?


How to connect 'to' the child controller...

Then, do the following, in code, in the ViewController of the whole scene.

Let's say you have three container views in the scene. Each container view holds a different controller, say "Snap", "Clock" and "Other".

Latest syntax

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) }
}

It's that simple. You connect a variable to refer to the controllers, using the prepareForSegue call.


How to connect in the 'other direction', up to the parent...

Say you're "in" the controller which you have put in a container view ("Snap" in the example).

It can be a confusing to get to the "boss" view controller above you ("Dash" in the example). Fortunately, it is this simple:

// 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
}

Critical: Only works from viewDidAppear or later. Will not work in viewDidLoad.

You're done.


Important: that only works for container views.

Tip, don't forget, that only works for container views.

These days with storyboard identifiers, it's commonplace to just pop new views on the screen (rather as in Android development). So, let's say the user wants to edit something...

    // 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)

When using a container view, IT IS GUARANTEED that Dash will be the parent view controller of Snap.

However that is NOT NECESSARILY THE CASE when you use instantiateViewController.

Very confusingly, in iOS the parent view controller is not related to the class which instantiated it. (It might be the same, but usually it is not the same.) The self.parent pattern is only for container views.

(For a similar result in the instantiateViewController pattern, you have to use a protocol and a delegate, remembering that the delegate will be a weak link.)

Note though that these days it's pretty easy to dynamically load a container view from another storyboard - see last section below. It's often the best way.


prepareForSegue poorly named...

It's worth noting that "prepareForSegue" is a really bad name!

"prepareForSegue" is used for two purposes: loading container views, and, segueing between scenes.

But in practice, you very rarely segue between scenes! Whereas almost every app has many, many, container views as a matter of course.

It would make more sense if "prepareForSegue" was called something like "loadingContainerView".


More than one...

A common situation is: You have a small area on the screen, where you want to show one of a number of different view controllers. For example, one of four widgets.

The simplest way to do this: just have four different container views all sitting inside the same identical area. In your code, simply hide all four and turn on the one you want visible.

Easy.


Container views "from code" ...

... dynamically load a Storyboard in to a container view.

2019+ Syntax

Say you have a storyboard file "Map.storyboard", storyboard ID is "MapID", and the storyboard is a view controller for your Map class.

let map = UIStoryboard(name: "Map", bundle: nil)
           .instantiateViewController(withIdentifier: "MapID")
           as! Map

Have an ordinary UIView in your main scene:

@IBOutlet var dynamicContainerView: UIView!

Apple explain here the four things you have to do to add a dynamic container view

addChild(map)
map.view.frame = dynamicContainerView.bounds
dynamicContainerView.addSubview(map.view)
map.didMove(toParent: self)

(In that order.)

And to remove that container view:

map.willMove(toParent: nil)
map.view.removeFromSuperview()
map.removeFromParent()

(Also in that order.) That's it.

Note however in that example, the dynamicContainerView is simply a fixed view. It does not change or resize. This would only work if your app never rotates or anything else. Usually, you would have to add the four usual constraints to simply keep the map.view inside dynamicContainerView, as it resizes. In fact, here is the "world's handiest extension" which one needs in any iOS app,

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

Thus, in any real app the code above would be:

addChild(map)
dynamicContainerView.addSubview(map.view)
map.view.bindEdgesToSuperview()
map.didMove(toParent: self)

(Some folks even make an extension .addSubviewAndBindEdgesToSuperview() to avoid a line of code there!)

A reminder that the order must be

  • add the child
  • add the actual view
  • call the didMove

Removing one of those?

You've added map dynamically to the holder, now you want to remove it. The correct and only order is:

map.willMove(toParent: nil)
map.view.removeFromSuperview()
map.removeFromParent()

Often you will have a holder view, and you want to swap different controllers in and out. So:

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)
}

这篇关于如何在 Objective-C 中添加一个拥有自己的 UIViewController 的子视图?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆