Жизненный цикл UIViewController’а, пытаемся разобраться (iOS 7 / iOS 8, swift edition)

Разработка под iOS для меня в данный момент является хобби, поэтому еще с iOS 3.0 я читал литературу, выполнял примеры, писал небольшие утилиты для себя и благополучно оставлял серьезное изучение на потом. Ситуация усугублялась тем, что iOS — молодая платформа, она активно развивается. Apple постоянно модернизирует API, как добавляя новые методы, так и удаляя старые. Поэтому со временем в голове образуется знатная каша.
Мне захотелось освежить и упорядочить знания с учетом последних нововведений + создать вспомогательный инструмент, который позволит экономить время при изучении изменений в API в дальнейшем.

В любой области есть раздел, являющийся ключевым, без понимания которого крайне трудно сделать что то стоящее. В фреймворке Cocoa на мой взгляд таким разделом являются UIView и UIViewController и их взаимодействие, реакция на поворот устройства и т.д. Нужно обладать четким пониманием чего ждать в какой момент, когда добавить компоненты на вьюху, когда выставить размеры и почему.
Все примеры я буду приводить на swift, с целью лишний раз попрактиковаться, ну и в связи с тем что язык очень молод — примеров на нем намного меньше.

Официальная документация по ViewController’у доступна здесь
Советую обращаться к ней, в случае возникновении вопросов.

Основным источником при написании этой статьи являлась книга Programming iOS 7, Fourth Edition by Matt Neuburg Очень рекомендую, как и версию для iOS 8. Авто поднимает тонну мелочей, но прет как танк, практически без иллюстраций. Мне захотелось выделить самую интересующую меня тему и сваять руководство с советами по применению конкретных методов.

Так же к вопросу о терминологии я использую слово ограничение чтобы обозначить constraint в Auto Layout

Создание View

Итак, начнем с базового: когда и каким способом Controller будет загружать View. View обычно довольно тяжеловесный обьект, поэтому он грузится только когда действительно нужен (перед отображением, а не при создании ViewController’а).

Создадим Single View Application проект в XCode (в качестве языка выбираем swift разумеется)

Существует несколько вариаций на тему того как Controller может получить View

1) Ручная инициализация. Только хардкор и все такое.

Для этого мы должны переопределить внутри контроллера метод loadView и внутри него присвоить self.view какую либо вьюху.

Простейший пример, создаем базовую вьюху и делаем фон синего цвета:
В файле ViewController.swift добавим метод

Запускаем проект и видим синий экран, работает.

2) Задействуем viewDidLoad и избавляемся от loadView

Суть в том, что если не определен метод loadView, — контроллер делает по сути то же, что и мы в предыдущем примере — присваивает self.view дефолтную UIView, что позволяет нам закомментировать метод loadView и назначить фон в viewDidLoad

Тот же результат что и в 1м примере. Но такой подход конечно не подойдет, если нужен не View по умолчанию а кастомный.

3) View загружается из Nib файла

Хотя сейчас правит бал StoryBoard — знать истоки нужно, это позволит нам лучше понять что происходит внутри StoryBoard.
По умолчанию у нас метод точки входа в AppDelegate.swift определен как

Т.к. в Info.plist в качестве Main storyboard file base name значится Main — при старте программы загрузка root view controller происходит из Main.storyboard. Но мы можем это изменить.
Для начала удалим ключ — «Main storyboard file base name», затем удалим сам Main.storyboard

Создадим CustomView
В меню
File -> New -> File -> User Interface -> View, Назовем CustomView
Выставим красный цвет в качестве фона, чтобы было понятно, что загрузилась именно эта версия View

redback

У Files Owner в Identity Inspector в качестве Custom class прописываем наш ViewController

customClass

После этого в Connection Inspectors появляется view Outlet, который нужно присвоить нашему View с красным фоном

view

Теперь parent’ом нашем customView будет считаться ViewController, но если запустить программу сейчас, красной View мы пока не увидим, т.к. раньше в загрузке основного ViewVontroller’а помогал Main.storyboard, теперь же это нужно сделать самим.

Меняем метод application на

Комментируем метод viewDidLoad в ViewController, чтобы фон не переназначался в коде.
Теперь при старте приложения мы явно указываем какую View сделать основной для ViewController’а, и при старте программы мы увидим красный фон у View

Таким образом мы имеем следующую последовательность

1. сначала у контроллера вызывается метод loadView. Если он переопределен в нашем контроллере, View создается именно в нем.
Именно поэтому нельзя вызывать super.loadView() из переопределенного loadView, т.к. в этом случае будут загружены 2 view — тот что в нашем коде — и из xib. Последствия могут быть разнообразные

2. если же loadView не переопределен — базовая реализация loadView попытается подгрузить нужный xib по указанному nibName

3. если ViewController был создан без указания nibName — будет создан базовый View, как было указано в пункте 2

Подводя итог — хотим создавать View вручную — переопределяем loadView. Грузим из nib — не переопределяем.

И еще одно, мы напрямую указывали xib в параметре nibName

но если разобраться в паре нюансов, — можно буде создавать так

Каким же образом система поймет какой xib надо грузить?
В Objective-C все было несколько проще, если не указывался nibName — система брала имя Controller’а и искала xib или с тем же именем, или с тем же именем но без слова Controller
К примеру в случае ViewController.m система бы искала ViewController.xib и View.xib
В случае swift все стало несколько сложнее.

1. Теперь xib должен называться как {AppName}.{ControllerName} (ибо так и называется swift файл контроллера на самом деле)
В моем случае это будет WhatWasChanged.ViewController.xib, возможность называть xib без части Controller сохранилась так что и WhatWasChanged.View.xib подойдет
Но если в Objective-C такое именование имело смысл, то сейчас только делает все более громоздким.

2. Можно сделать ViewController — не swift, а objective-c, и тогда можно будет обойтись {ControllerName}.xib, но мы в данный момент говорим только о swift, так что просто заметка на будущее.

3. атрибут @objc
Его основное предназначение — позволить swift классам не наследуемым от NSObject стать доступными для использования в Objective-C коде. Но этот атрибут так же позволяет задавать имя класса под которым он будет виден для Objective-C

Т.е. если обьявить наш контроллер вот так:

то он станет известен в системе не как {AppName}.{ControllerName}, а так, как вы его назовете в атрибуте. Так что опять можно назвать View.xib и все заработает как встарь.

4. можно поиграться с инициализацией,

При вызове инициализации с любым nibName мы вызовем в итоге со своим (#define TRUE = false // счастливой отладки.. коллеги)

5. Ну и на закуску еще один вариант, переопределить nibName

Стоит учесть, что он довольно топорен, в том плане что будет загружен именно CustomView.xib, если у вас CustomViewController.xib, — он загружен не будет.

4) View Controller, вызываемый из Nib файла

Как известно при создании nib файла можно на форму перетащить компонент ViewController. После того как он загружается из файла, controller пытается найти view по тем же правилам что уже были описаны. Т.е. сначала loadView, потом загрузка по имени контроллера, правда можно указать имя nib файла из которого должна загрузиться view

nibName

Но добавляется самый простой способ используемый по умолчанию — при перетаскивании компонента ViewСontroller на форму — контроллер создается сразу с формой, которая и будет загружена по умолчанию (если не переопределять loadView у контроллера)

5) View Controller, вызываемый из storyboard

Самый актуальный способ создания, но мы прошли долгий путь, и теперь должны понять, что storyboard — это сборник сцен, каждая из которых содержит ViewController обьект (который по умолчанию обычно содержит и view). Каждая сцена вызывается только при необходимости — запуская механизм создания нужного ViewController’а, который в свою очередь создает View.

Последовательность событий View Controller

Что ж, мы прошли первую часть пути, оказавшуюся на удивление длинной. Теперь мы знаем как и когда ViewController будет создавать View.
Следующий вопрос, который нас интересует, что же собственно происходит после создания View. И что происходит при повороте устройства, а что будет если запустить устройство в landscape режиме и т.д.

Простейшим способом является логирование, нужно определить круг методов у ViewController который нам интересен. Мой выбор следующий:

// Responding to View Events
методы, которые будут вызваны при появлении/исчезновении UIView
viewWillAppear
viewDidAppear
viewWillDisappear
viewDidDisappear

// Configuring the View’€™s Layout Behavior
методы, которые будут вызваны при изменении layout’а view

viewWillLayoutSubviews
viewDidLayoutSubviews
updateViewConstraints

// Rotate
События возникающие при поворотах устройства.
То, что было в iOS 7

//deprecated in iOS 8.0
willRotateToInterfaceOrientation
willAnimateRotationToInterfaceOrientation
didRotateFromInterfaceOrientation

То что стало в iOS 8

viewWillTransitionToSize
willTransitionToTraitCollection

Часть же методов я не стал логировать из за чрезмерно частого их вызова

shouldAutorotate, к примеру, при запуске ViewControllerа в портретном положении на iPad отработал раз 5
supportedInterfaceOrientations
shouldAutomaticallyForwardRotationMethods

Так же я не стал трогать частные случаи, типа методы, которые стоит переназначать только при создании своей реализации Custom Container
addChildViewController
removeFromParentViewController
transitionFromViewController
shouldAutomaticallyForwardAppearanceMethods
beginAppearanceTransition
endAppearanceTransition
setOverrideTraitCollection
overrideTraitCollectionForChildViewController

Логирование будет простейшим типа

в теле переназначенного метода, чтобы не только понимать какой метод был выполнен, но и какие размеры имеет в тот момент view.
Таким образом код класса ViewController будет следующим

Перед тем как начать разбирать конкретные случаи хочу оговориться, везде где в выводе лога присутствует метод updateViewConstraints — он будет вызван только если view использует autolayout и созданы ограничения

При запуске в портретном режиме на iPhone на iOs 7.1 получим вывод в консоли (на iPad будут отличаться только размеры)

Проверим как отработает поворот

Что же случится если устройство будет запущено сразу в landscape режиме на iPhone

// тут же начинается поворот

Как видим, — результат совпадает с тем, если бы мы запустили программу в портретном и повернули. Теперь запустим на iPad 2 сразу в ландшафтном

Вот это уже интересно, получается на iPad мы сразу получаем нужный режим.

Что ж проверим как все это отрабатывает на iOS 8.3 при запуске в портретном на iPhone 5 при использовании autoLayout (на iPad будут отличаться только размеры)

iPhone 5 iOS 7.1 (Auto Layout)

Сразу замечаем, что теперь метод updateViewConstraints вызывается не перед viewWillLayoutSubviews, а после.
Так же дважды стали вызываться viewWillLayoutSubviews и viewDidLayoutSubviews

В случае если мы отключим autoLayout, получим лог попроще (для iPad будут отличаться только размеры)

iPhone 5 iOS 7.1 (без Auto Layout)

Теперь повернем устройство.

Поворот iPhone 5 iOS 7.1 (Auto Layout)

Поворот iPhone 5 iOS 7.1 (без Auto Layout)

Поворот iPad 2 iOS 7.1 (Auto Layout)

Поворот iPad 2 iOS 7.1 (без Auto Layout)

Как мы видим, для iPad событие willTransitionToTraitCollection не поступает. Хоть на мой взгляд и глупо, но легко объяснимо, для iPad и портретный и ландшафтный режимы имеют Width и Height равные Regular, получается при повороте устройства изменение не происходит.

Ну и запустим сразу в ландшафтном

iPhone 5 iOS 8.3 (Auto Layout)

iPhone 5 8.3 (без Auto layout)

iPad 2 iOS 8.3 (Auto Layout)

iPad 2 iOS 8.3 (без Auto Layout)

iPhone просто повторяет действия портретный, переход на ландшафт, в то время как iPad сразу получает ландшафт, как и в iOS 7

Благодаря этим простым тестам сразу можно нащупать подводные камни при очередном изменении в iOS (не забывая добавлять на логирование новые методы относящиеся к исследуемому аспекту в случае их появлении)

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

Описание методов

1. Сразу после загрузки view вызывается метод viewDidLoad

Это прекрасная возможность создать и добавить в иерархию subview, как то закастомизировать их. Но есть одно НО, на момент вызова этого метода view еще не добавлен на UIWindow, соответственно корректной информации по размерам получить тут нельзя.
К примеру если вставить логирование размера view

И запустить ipad в ландшафтном режиме на ios 7 мы получим
viewDidLoad size: w:768.0, h: 1024.0
А на самом деле должны 1024×768

Хотя напрямую в документации не указана необходимость вызова super.viewDidLoad() я считаю, что это делать стоит. Я прочитал много обсуждений на эту тему, большинство склоняется к тому, что хуже от вызова метода в базовом классе — не будет. А вот если не вызывать — в последующих релизах iOS это может понадобиться.

Так же стоит предостеречь от попытки инициализировать данные в этом методе. Хотя в идеальном случае она вызывается всего один раз, бывают нюансы, view может быть выгружено из за нехватки памяти, при попытке повторно воспользоваться ViewController’ом — он обнаружит что view выгружено и попробует загрузить его снова, что приведет к вызову viewDidLoad еще раз.
Так что инициализировать данные лучше в методе init

При использовании Auto Layout — это прекрасное место для определении ограничений в случае если view создается вручную. Для этих целей так же подойдет loadView

2. Следом всегда идет viewWillAppear

Этот метод вызывается перед тем как view будет добавлена в текущую иерархию, перед отработки какой бы тот ни было анимации.
До iOs 7 здесь все советовали выполнять код, основываясь на корректных размерах view, но в данный момент это не совсем так.
При запуске в портретном режиме iPhone/iPad показали корректные размеры, это верно. Но при запуске в горизонтальном — результаты разнятся
iPhone на iOS 7/8 показал 320*480 не зависимо от того в портретном или ландшафтном размере был запущен.
iPad на 7ке показал 768×1024, но на 8ке уже 1024×768 в ландшафтном.

Для чего можно использовать этот метод? Для более тонкой настройки subview, которые нужно сделать без анимации. К примеру выставить корректную позицию в scrollView, заполнить данными текстовое поле или таблицу, чтобы к моменту показа view пользователю — все было корректно заполнено данными.
Если смотреть официальную документацию: в этом методе можно менять стиль статус бара, или стиль всего view
Так же здесь отрабатывается код по управлению детьми view controller в случае если мы пишем свою реализацию Custom Container
Но основное применение при Auto Layout — вызов updateViewConstraints, в случае если нужно менять ограничения
Обязательно нужно вызывать super.viewWillAppear()

3. viewWillLayoutSubviews

Здесь наконец мы получаем актуальные размеры view. Этот метод вызывается перед тем как view упорядочит свои subview.
Здесь самое место менять размеры subview, их положение, если только вы не перешли наконец на Auto Layout, и за вас все теперь делают ограничения

Не нужно вызывать super.viewWillLayoutSubviews()

4. updateViewConstraints

Только для Auto Layout.
В этом методе мы должны при необходимости править наши ограничения, к примеру при повороте устройства
super.updateViewConstraints() нужно вызывать в конце переопределенного метода

5. viewDidLayoutSubviews

Здесь можно быть уверенным, что view корректно выставила положения для всех своих детей (но это еще не означает что и дети выставили положение своих детей). Порыскав по исходникам, я обнаружил что в основном люди применяют этот метод чтобы сохранить состояние какого либо элемента (выделение в tableView, position в scrollView), или для каких то костылей.
По умолчанию метод не делает ничего так что не нужно вызывать super.viewDidLayoutSubviews()

6. viewDidAppear

Порывшись в инете я обнаружил, что бывает, что этот метод не вызывается, так что следует это учитывать. Область применения схожа с viewDidLayoutSubviews. Это финальный метод в цепочке, так что при первом запуске сюда можно загнать инициализацию логина (выставляя флаг чтобы делать это только при первом запуске). Так же можно здесь вызвать becomeFirstResponder, чтобы сразу начать ввод в нужное поле.
Нужно вызывать super.viewDidAppear()

Есть еще пару методов, которые было бы неправильно обойти вниманием. Они в нашем случае в логах и не отобразились, но это лишь потому, что мы не переключали viewController’ы между собой.

Если создать проект на основе TabbedApplication и вставить тот же код напичканный логированием, то при загрузке приложения активным станет первый контроллер и в логах будет (для iOS 8.3)

А после того, как мы выберем вторую вкладку в логах увидим

Таким образом видно, что у первого контроллера вызывается viewWillDisappear в момент когда у второго контроллера уже был вызван viewWillAppear,
то же самое касается и 1.viewDidDisappear / 2.viewDidAppear

7. viewWillDisappear

Предупреждение что вот вот view будет удален из иерархии вьюх. Здесь обычно коммитят изменения, убирают first responder статус, ставят на паузу выполняемые действия, отменяют ориентацию/стиль статус бара если ее меняли в viewWillAppear и т.д. Можно почистить данные, обнулить кеш и все в таком духе.

Нужно вызывать super.viewWillDisappear()

8. viewDidDisappear

Оповещение о том, что view было удалено из иерархии view. А так область применения крайне схожа с viewWillDisappear, здесь так же удаляют ненужные данные, ставят на паузу плеер. Нужно просто понимать, что этот метод вызовется после анимации удаления view, а предыдущий перед.

Нужно вызывать super.viewDidDisappear()

На мой взгляд документации Apple очень не хватает практического подхода, описания тонкости применения отдельных методов, тактику выбора одних а не других. То, что попытался сделать я. Т.к. я не считаю себя профессионалом iOS разработки — мог допустить неточности, а то и ошибки, буду рад выслушать замечания и исправления.

1 thought on “Жизненный цикл UIViewController’а, пытаемся разобраться (iOS 7 / iOS 8, swift edition)

  1. Максим

    Так же стоит предостеречь от попытки инициализировать данные в этом методе. Хотя в идеальном случае она вызывается всего один раз, бывают нюансы, view может быть выгружено из за нехватки памяти, при попытке повторно воспользоваться ViewController’ом — он обнаружит что view выгружено и попробует загрузить его снова, что приведет к вызову viewDidLoad еще раз. <<< в документации указано это поведение, но если посмотреть на метод viewWillUnload (deprecated in ios6), так что выгрузки не происходит)))

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *