Как добавить subview с собственным UIViewController в Objective-C?

Я борюсь с subviews, у которых есть свой UIViewControllers. У меня есть UIViewController с видом (светло-розовый) и двумя кнопками на toolbar. Я хочу, чтобы синий вид отображался при нажатии первой кнопки и нажатии желтого изображения, отображаемого с помощью второй кнопки. Должно быть легко, если я просто хочу отобразить представление. Но синий вид будет содержать таблицу, поэтому ей нужен собственный контроллер. Это был мой первый урок. Я начал с этот вопрос SO, где я узнал, что мне нужен контроллер для таблицы.

Итак, я собираюсь сделать резервную копию и сделать здесь несколько детских шагов. Ниже приведено изображение простой начальной точки с моей утилитой ViewController (главный контроллер представления) и двумя другими контроллерами (синий и желтый). Представьте себе, что при первом отображении утилиты ViewController (основной вид) отображается синий (по умолчанию) вид, где расположен розовый вид. Пользователи смогут щелкнуть две кнопки, чтобы вернуться назад и вперед, и розовый вид НИКОГДА не будет отображаться. Я просто хочу, чтобы синий вид шел туда, где находится розовый вид, а желтый вид - туда, где находится розовый вид. Надеюсь, это имеет смысл.

Simple Storyboard image

Я пытаюсь использовать addChildViewController. Из того, что я видел, есть два способа сделать это: Container View в storyboard или addChildViewController программно. Я хочу сделать это программно. Я не хочу использовать NavigationController или панель вкладок. Я просто хочу добавить контроллеры и перетащить правильный вид в розовое изображение при нажатии соответствующей кнопки.

Ниже приведен код, который у меня есть. Все, что я хочу сделать, это показать синий вид, где находится розовый вид. Из того, что я видел, я мог бы просто 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

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

Self.aboutVC.aboutView равен нулю. Но я подключил его в storyboard. Мне все еще нужно создать его экземпляр?

enter image description here

Ответы

Ответ 1

Этот пост относится к ранним дням современной iOS. Он обновляется текущей информацией и текущим синтаксисом Swift.

Сегодня в iOS "все является контейнером". Это основной способ создания приложений сегодня.

Приложение может быть настолько простым, что оно имеет только один вид. Но даже в этом случае каждая "вещь" на экране является контейнером.

Это так просто...


(A) Перетащите вид контейнера на свою сцену...

Перетащите вид контейнера в вид сцены. (Подобно тому, как вы перетаскиваете любой элемент, например UIButton.)

Контейнерный вид - коричневая вещь на этом изображении. На самом деле это внутри вашего вида сцены.

enter image description here

Когда вы перетаскиваете вид контейнера в вид сцены, Xcode автоматически дает вам две вещи:

  1. Вы получаете вид контейнера внутри вида сцены и

  2. вы получаете совершенно новый UIViewController, который просто сидит где-то на белом фоне вашей раскадровки.

Эти два связаны с "масонским символом" - объяснено ниже!


(B) Нажмите на этот новый контроллер представления. (Так что новая вещь, которую Xcode сделал для вас где-то в белой области, а не вещь внутри вашей сцены.)... и измените класс!

Это действительно так просто.

Вы сделали.


Здесь то же самое объясняется визуально.

Обратите внимание на контейнер на (A).

Обратите внимание на контроллер на (B).

shows a container view and the associated view controller

Нажмите на B. (Это B - не A!)

Идите к инспектору в правом верхнем углу. Обратите внимание, что там написано "UIViewController"

enter image description here

Измените его на свой собственный класс, который является UIViewController.

enter image description here

Итак, у меня есть класс Swift Snap, который является UIViewController.

enter image description here

Поэтому там, где написано "UIViewController" в Инспекторе, я набрал "Snap".

(Как обычно, Xcode автоматически завершит "Snap", когда вы начнете набирать "Snap...".)

Это все, что нужно сделать - все готово.


Как изменить вид контейнера - скажем, на вид таблицы.

Поэтому, когда вы нажимаете, чтобы добавить представление контейнера, Apple автоматически предоставляет вам контроллер связанного представления, расположенный на раскадровке.

В настоящее время (2019) по умолчанию он становится UIViewController.

Это глупо: нужно спросить, какой тип тебе нужен. Например, часто вам нужно представление таблицы.

Вот как изменить его на что-то другое:

На момент написания Xcode по умолчанию дает вам UIViewController. Допустим, вы хотите вместо этого UICollectionViewController:

(i) Перетащите контейнерный вид на вашу сцену. Посмотрите на UIViewController на раскадровке, который XCode предоставляет вам по умолчанию.

(ii) Перетащите новый UICollectionViewController в любое место основной белой области раскадровки.

(iii) Нажмите на контейнер внутри вашей сцены. Нажмите инспектор соединений. Обратите внимание, что есть один "Triggered Segue". Наведите курсор мыши на "Seggered Segue" и обратите внимание, что Xcode выделяет весь нежелательный UIViewController.

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

(v) DRAG из этого запущенного сега (viewDidLoad - единственный выбор). Перетащите через раскадровку на свой новый UICollectionViewController. Отпустите, и появится всплывающее окно. Вы должны выбрать для встраивания.

(vi) Просто удалите весь ненужный UIViewController. Вы сделали.

Короткая версия:

  • удалите ненужный UIViewController.

  • Поместите новый UICollectionViewController в любое место на раскадровке.

  • Перетащите элемент управления из контейнера Подключения - Запуск триггера - viewDidLoad, в ваш новый контроллер.

  • Обязательно выберите "вставлять" во всплывающем окне.

Это так просто.


Ввод текстового идентификатора...

У вас будет один из этих "квадрат в квадрате" масонских символов: он находится на "линии сгиба", соединяющей ваш контейнерный вид с контроллером представления.

"Масонский символ" - это переход.

enter image description here

Выберите переход, нажав на "масонский символ".

Посмотрите направо.

Вы ДОЛЖНЫ ввести текстовый идентификатор для перехода.

Вы выбираете имя. Это может быть любая текстовая строка. Часто хорошим выбором является "segueClassName".

Если вы будете следовать этому шаблону, все ваши сегменты будут называться 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 = self.parent as? Dash
}

Критический: Работает только с viewDidAppear или новее. Не будет работать в viewDidLoad.

Вы сделали.


Важно: это работает только для представлений контейнера.

Совет, не забывайте, что это работает только для контейнеров.

В наши дни с идентификаторами раскадровки, это обычное дело, просто чтобы выскочить новые представления на экране (а не в разработке Android). Итак, допустим, пользователь хочет что-то редактировать...

    // let 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 необходимо использовать протокол и делегат, помня, что делегат будет слабым звеном.)

Обратите внимание, что в наши дни довольно просто динамически загружать представление контейнера из другой раскадровки - см. последний раздел ниже. Это часто лучший способ.


prepareForSegue плохо назван...

Стоит отметить, что "prepareForSegue" - это действительно плохое имя!

"prepareForSegue" используется для двух целей: загрузки представлений контейнера и перехода между сценами.

Но на практике вы очень редко переходите между сценами! Принимая во внимание, что почти каждое приложение имеет много-много контейнерных представлений как само собой разумеющееся.

Было бы больше смысла, если бы "prepareForSegue" называлось что-то вроде "loadingContainerView".


Более одного...

Распространенная ситуация: у вас есть небольшая область на экране, где вы хотите показать один из нескольких различных контроллеров представления. Например, один из четырех виджетов.

Самый простой способ сделать это: просто иметь четыре разных контейнера, все они находятся внутри одинаковой области. В своем коде просто скройте все четыре и включите тот, который вы хотите видеть.

Легко.


Контейнерные представления "из кода"...

... динамически загружать раскадровку в представление контейнера.

Синтаксис 2019

Допустим, у вас есть файл раскадровки "Map.storyboard", идентификатор раскадровки - "MapID", а раскадровка - это контроллер представления для вашего класса Map.

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

У вас есть обычный UIView на главной сцене:

@IBOutlet var dynamicContainerView: UIView!

Apple объясняет здесь четыре вещи, которые нужно сделать, чтобы добавить динамическое представление контейнера

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 - это просто фиксированный вид. Он не изменяется и не изменяет размер. Это предполагает, что ваше приложение никогда не вращается или что-либо еще. Обычно вам нужно добавить четыре обычных ограничения, чтобы просто сохранить карту .view внутри dynamicContainerView, как он изменяет размеры.)

Ответ 2

Я вижу две проблемы. Во-первых, поскольку вы создаете контроллеры в раскадровке, вы должны создавать их с помощью 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];
}