Как добавить subview, у которого есть собственный UIViewController в Objective-C?

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

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

Простое изображение раскадровки

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

Ниже приведен код, который у меня есть. Все, что я хочу сделать, это отобразить синий вид вместо розового. Судя по тому, что я видел, я могу просто addChildViewController и добавить SubView. Этот код не делает этого для меня. Мое замешательство берет верх надо мной. Может ли кто-нибудь помочь мне отобразить синий вид вместо розового?

Этот код не предназначен для чего-либо, кроме отображения синего представления в 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. Мне все еще нужно создать его экземпляр?

введите описание изображения здесь


person Patricia    schedule 30.04.2014    source источник
comment
Какая у вас настоящая проблема?   -  person Abhi Beckert    schedule 01.05.2014
comment
Когда представление загружается, я вижу розовый вид. Полагаю, я неправильно это кодирую.   -  person Patricia    schedule 01.05.2014
comment
Хорошо, хорошо. Прежде всего, выполните код по одной строке за раз и убедитесь, что ни один из объектов не является nil. В частности, self.aboutVC, self.utilityView и self.aboutVC.aboutView.   -  person Abhi Beckert    schedule 01.05.2014
comment
Ты прав. Self.aboutVC.aboutView равен нулю. Но я все спаял в раскадровке. Я обновлю свой вопрос изображением.   -  person Patricia    schedule 01.05.2014
comment
Какая у тебя первая кнопка, а какая у тебя вторая? И когда вы говорите blueview вместо розового ... вы имеете в виду, когда blueview появляется на экране - по-прежнему «X» и «?» Висят там вверху обзора?   -  person Honey    schedule 04.01.2017


Ответы (2)


Этот пост относится к ранним временам современной 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.

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

Вот и все - готово.


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

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

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

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

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

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

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

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

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

(iv) Щелкните x, чтобы на самом деле удалить вызвавший переход.

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

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

Укороченная версия:

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

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

  • Удерживая нажатой клавишу Control, перетащите из окна контейнера Connections - Trigger Segue - viewDidLoad на ваш новый контроллер.

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

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


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

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

Масонский символ - это segue.

введите описание изображения здесь

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

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

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

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

Если вы будете следовать этому шаблону, все ваши сегменты будут называться segueClockView, seguePersonSelector, segueSnap, segueCards и так далее.

Далее, где вы используете этот текстовый идентификатор?


Как подключиться к дочернему контроллеру ...

Затем сделайте следующее в коде во ViewController всей сцены.

Допустим, у вас есть три представления контейнера в сцене. Каждое представление контейнера содержит отдельный контроллер, например Snap, Clock и прочее.

Последний синтаксис

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.

Готово.


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

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

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

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


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

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

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

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

Было бы лучше, если бы prepareForSegue назывался чем-то вроде loadContainerView.


Больше, чем один...

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

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

Легкий.


Представления контейнера из кода ...

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

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 - это просто фиксированный вид. Он не меняется и не масштабируется. Это будет работать только в том случае, если ваше приложение никогда не вращается или что-то еще. Обычно вам нужно добавить четыре обычных ограничения, чтобы просто сохранить 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)
}
person Fattie    schedule 01.05.2014
comment
Я попробую это, когда приду в офис. Спасибо. Я все еще думаю, что ты УДИВИТЕЛЬНЫЙ !!! :-) - person Patricia; 01.05.2014
comment
Ты забавный, Джо. Это решает мою чертову проблему с использованием представления контейнера. :-) Я хотел сделать это в коде просто потому, что делал это раньше в коде (однажды ... и больше года назад). Я начал, как и все остальные, с раскадровки. Мои бывшие коллеги были старшими разработчиками iOS и не хотели использовать раскадровки, потому что у нас было слишком много окон. Так что мой опыт работы с раскадровками и переходами крайне слаб. Недавно я работал с AVFoundation и элементами управления камерой, но НЕ с пользовательским интерфейсом. Когда тебя бросают, легко забыть основы. - person Patricia; 01.05.2014
comment
Мне все еще может понадобиться небольшая помощь с маленьким квадратом в квадратной штуке с масонским символом !!! :-) - person Patricia; 01.05.2014
comment
По поводу проклятого масонского символа - в чем проблема? ..... Я был смешным в ответ. Черт побери !! :-) - person Patricia; 02.05.2014
comment
Привет, @Joe, позволь мне взглянуть на это. Я действительно получил эту работу и собирался поделиться с вами, как я это сделал, но в последнее время я пытаюсь интегрировать сторонний инструмент, и приложение вылетает при postNotification. Дай мне немного. Да, приятно слышать от вас. - person Patricia; 23.06.2014
comment
Привет, Джо, у меня есть новый фаворит. Это заставило меня подумать о тебе! :-) Здесь: stackoverflow.com/q/8318911/1735836 - person Patricia; 16.02.2016
comment
IMHO это самый чистый способ, если вы хотите использовать IB. Намного лучше, чем пытаться реализовать с помощью перьев. - person abc123; 07.03.2016
comment
Ссылки на раскадровку в iOS 9 делают представления контейнеров даже круче. Вы можете определить свое многократно используемое представление (контроллер) где угодно и ссылаться на него из любого представления контейнера в нескольких модульных раскадровках. - person SimplGy; 30.04.2016
comment
@Fattie Отличный учебник! Было бы полезно, если бы вы немного объяснили, как изменять элементы внутри контроллера представления, на который ссылается представление контейнера. Для такого новичка, как я, было не так очевидно, что я мог, наконец, изменить текст своего UILabel внутри viewDidLoad, потому что подготовка (для перехода :), наконец, сделала доступной действительную (не нулевую) ссылку на контроллер представления и его элементы (надеюсь, я используется для исправления терминов. Извините!) - person Michele Dall'Agata; 10.08.2018

Я вижу две проблемы. Во-первых, поскольку вы создаете контроллеры в раскадровке, вы должны создавать их экземпляры с помощью 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];
}
person rdelmar    schedule 01.05.2014
comment
Нужна ли установка рамки? или он предоставит представлению рамку по умолчанию в соответствии с раскадровкой? - person Brian; 01.05.2014
comment
@Brian, вероятно, в этом нет необходимости, если вид, который вы добавляете, является полноэкранным, но я обычно добавляю его любым способом, чтобы убедиться. Кроме того, в этом случае я не был уверен, был ли self.utilityView полным представлением или частью представления под панелью инструментов, и в этом случае вам, вероятно, необходимо установить фрейм. - person rdelmar; 01.05.2014
comment
@rdelmar - Спасибо за информацию. Я задавался вопросом, следует ли мне использовать instantiateViewControllerWithIdentifier, но был немного сбит с толку. Кроме того, да, self.utilityView не является полным представлением. Все 3 подпредставления имеют одинаковый размер, поэтому я не думал, что мне нужно устанавливать размер кадра. Я попробую ваше предложение, когда приду в офис. - person Patricia; 01.05.2014
comment
@JoeBlow, OP упомянул использование представления контейнера в раскадровке, но сказал, что они хотят сделать это в коде. - person rdelmar; 01.05.2014