View controller swift что это
Жизненный цикл UIViewController’a
Большинство прикладных приложения под iOS таким или иным образом используют UIViewController‘ы. Там где UIKit фрэймворк — там и UIViewController‘ы. Их много, они повсюду, они сидят в засадах и выглядывают из-за каждого угла. Поэтому, любой программист под iOS — будь он зеленым новичком, едва ступившим на тропу программирования, либо матерым профессионалом своего дела, должны знать о UIViewController‘aх все.
Причиной написания данной поста является то, что, как оказалось, можно преспокойно программировать под iOS полгода, и не знать полностью о жизненном цикле UIViewcontroller‘ов. И на небольших проектах это даже получается. Однако, когда приходится иметь дело с серьезным, достаточно большим проектом, то появляются определенные проблемы с нехваткой памяти, «неправильной» и «непонятной» работой контроллеров, пропажей данных, и еще со многими типичными проблемами, о которых будет написано ниже.
Так вот. В данном посте, я еще раз расскажу о жизненном цикле UIViewController‘ов, расскажу о том, что и где стоит делать, и в каком случае. Пост ориентирован на разработчиков разных уровней, так что кто-то узнает для себя что-то новое, а кто-то найдет повод отпинать моменты, на которые стоит обратить внимание Junior’ов в команде.
Всех заинтересовавшихся, прошу под кат
Сразу должен сказать, что многое, что здесь будет описано, можно найти в документации, в разного вида источниках. Как говорится, кто ищет — тот всегда найдет, изучит, посягнет, и напишет о своем видении проблемы. В посте я буду часто ссылаться на документацию, особенно, в тех моментах, которые этот пост не затрагивает, но которые тоже желательно знать. Так, например, это пост не рассказывает об изменениях ориентации UIView при переворотах устройства, однако заинтересованные именно в этом вопросе могут почитать об этом самостоятельно.
О UIViewController’е
Является Controller’ом согласно шаблону проектирования MVC. Обеспечивает взаимосвязь модели и отображения. В iOS на него возлагаются задачи, связанные с контролем жизненного цикла view, и отображением UIView в различных ориентациях устройства. Общую информацию можно найти в той же документации.
Жизненный цикл UIViewController’a и его UIView
Сконцентрируемся на методах, которые отвечают именно за жизненный цикл UIViewControllera:
А теперь обо всем по порядку.
Создание
Для создания контроллера, вернее для его инициализации существует два основных метода — init и initWithNibName:(NSString *)nibNameOrNil. На самом деле, метод init, вызовет initWithNibName:, так что можно рассматривать только его.
Есть одно исключение, связанное с наследниками UITableViewController, о котором надо знать (спасибо хабраюзеру muryk):
Важно: На этом этапе еще нет ни самой view, ни аутлетов(Outlets).
Созданный UIViewController может, находится в состоянии «без view» достаточно долго, вплоть до удаления его из памяти. Создание view произойдет только после того, как будет вызван метод [viewController view] (viewController.view). Это можете сделать вы, или это может за вас сделать UINavigationController, UITabBarController и многие другие.
Примером, когда UIViewController находится в состоянии «без view», может быть вариант с использованием UITabBarController’а, когда изначально он содержит в себе ссылки на N контроллеров, и только у того, который на данный момент показан на экране, будет загружен view. Все остальные будут ждать, пока пользователь не переключит табу, либо пока криворукий неопытный программист вызовет nonVisibleViewcontroller.view.
Ошибка #1(l): Доступ к аутлетам до загрузки view
Ошибка #2(p): Загрузка view до того, как она на самом деле необходимо, игнорирование метода isViewLoaded
Ошибка #3(p): Создание визуальных компонент в методе initWithNibName
Создание UIView
Прежде, чем перейти к следующему пункту, необходимо сказать пару слов о создании UIView, которые находятся внутри xib’ов. Представим ситуацию, что в xib’е находится созданный UIPrettyView, который при своей инициализации должен поставить себе цвет фона в розовый. Так вот, чтобы не тянуть никого ни за какие части тела, скажу сразу — если UIView будет загружена из xib’а, то будет вызван метод инициализации initWithCoder:, в противном случае (при создании в коде) обычно будет вызван метод initWithFrame:
После того, как view загрузилась из xib’а, хорошим местом для «допиливания» элементов дизайна, является метод viewDidLoad. Именно здесь стоит создавать визуальные компоненты, которые, по каким-то причинам, не попали в xib или в метод loadView.
Ошибка #4(l): Путаница с initWithFrame: и initWithCoder:.
Обработка изменения состояния view
Как уже было сказано, viewDidLoad, является наилучшим местом для продолжения инициализации контроллера. С этого метода у начинающих, и не только, программистов начинаются проблемы. Очень часто в виду незнания/непонимания жизненного цикла контроллера, можно увидеть следующий код:
А теперь, пора посмотреть на условную диаграмму жизни UIViewController‘а
Как видно (надеюсь) из диаграммы, метод viewDidLoad в процессе жизни контроллера может быть вызван более одного раза. В результате, приведенный выше код может привести утечкам памяти, при каждом новом вызове viewDidLoad
Ошибка #5(l): viewDidLoad вызывается один раз при жизни контроллера.
На самом деле, очень полезно использовать данный метод для восстановления состояния view контроллера. Так, очень часто в примерах можно встретить установку позиции у UIScrollView, установка визуальных компонент в актуальное состояние (active/inactive).
Необходимо заметить, что на данном этапе жизненного цикла контроллера, размеры view не актуальны, т.е. не такие, какими они будут после вывода на экран. Поэтому, использовать вычисления, основанные на ширине / высоте view, в методе viewDidload не рекомендуется.
Ошибка #6(u): viewDidload возвращать интерфейс в исходное состояние.
Ошибка #7(l): Определение размеров компонентов, расчеты, использование ширины / высоты view
viewWillAppear и viewDidAppear
Методы, которые вызываются перед и после появления view на экране.
В случае анимации(появление контроллера в модальном окне, или переход в UINavigationController‘e), viewWillAppear будет вызван до анимации, а viewDidAppear — после.
При вызове viewWillAppear, view уже находится в иерархии отображения (view hierarchy) и имеет актуальные размеры, так, что здесь можно производить расчеты, основанные на ширине / высоте view.
viewWillDisappear и viewDidDisappear
Методы, которые очень нечасто используются программистами. Работают так же само, как и viewWillAppear и viewDidAppear, только наоборот 😉
viewDidUnload
Наибольшее количество ошибок чаще всего находится именно в этом методе.
Поэтому, надо разобраться, что в нем происходит.
Метод вызывается когда, view был выгружен из памяти.
При вызове этого метода аутлеты все еще находятся в памяти, НО по сути, они не актуальны, т.к. не находятся в иерархии отображения, а при следующем viewDidLoad будут переписаны новыми.
Так что, для корректной работы данного метода и программы в целом, необходимо:
— Обнулить все аутлеты.
— По возможности сохранить состояние view, чтобы восстановить его в следующем вызове viewDidLoad.
— Не вызывать методы, которые приведут к загрузке view.
— Не использовать аутлеты для хранения состояния контроллера.
Задача проста и ясна, но 90% начинающих разработчиков про нее не знают и забывают.
Ошибка #8(p) Метод viewDidUnload пуст, и выглядит как [super viewDidUnload]
Ошибка #9(p) Метод viewDidUnload не освобождает аутлеты
Ошибка #10(u) Метод viewDidUnload не сохраняет состояние контроллера
Ошибка #11(l) Использование аутлетов для хранения состояния контроллера
Обработка memory warning
Метод didReceiveMemoryWarning вызывается системой, при нехватке памяти.
По умолчанию, реализация этого метода для контроллера, который не находится в видимой области, вызовет, освободит view (после этого _view == nil), что, в свою очередь приведет к вызову viewDidUnload.
В этом методе необходимо освободить как можно больше памяти, если нельзя освободить — то сбросить в кэш (в файл, например)
Ошибка #12(p) Хранение в памяти ресурсов, которые можно свободно сбросить в кэш в файл.
Уничтожение
Из особенностей dealloc у контроллера, необходимо отметить то, что не факт, что перед этим будет вызван viewDidUnload.
В реализациях некоторых библиотек сторонних разработчиков, например, в Three20, можно встретить вызов viewDidUnload напрямую в dealloc.
В остальном — руководствуйтесь принципами, изложенными Memory Management Programming Guide.
На этом почти все.
Дальше будут даны некоторые пояснения по поводу часто допускаемых ошибок.
Пояснения к частым ошибкам
Ошибки условно разделены на три типа:
p — perfromance error (ошибка, приводящая к утечкам памяти, снижению скорости работы программы)
l — logic error (ошибка которая может привести к неправильной работе программы, вследствие неправильных допущений/убеждений)
u — user unfriendly error (Не совсем ошибка, скорее просто напоминание о том, что пользовательский интерфейс должен быть дружелюбен к пользователю)
Пожалуй, на первое время хватит. Надеюсь, пост был информативным, и поможет разработчикам iOS делать по-настоящему хорошие, не только снаружи, но и внутри приложения.
Всем спасибо за внимание.
UPDATE. Подправил описание метода viewDidUnload
UPDATE. Рассмотрено исключение для наследников UITableViewController в методе initWithNibName:bundle: (Cпасибо хабраюзеру muryk)
UPDATE. По просьбам трудящихся, выкладываю небольшой проект для закрепления материала
Правильная передача данных
Правильная передача данных
Доброго времени суток, друзья!
Сегодня мы рассмотрим передачу данных между ViewControllers. На самом деле передача данных между ViewControllers может показаться тривиальной задачей, но если учесть, что любое реальное приложение для iOS будет иметь много ViewControllers, то коммуникация становится их важной частью. Неправильное понимание этого может привести к трудному исправлению ошибок.
В целом существует много способов сделать это, но лишь некоторые из них являются лучшей практикой.
В этой статье я покажу вам такие практики передачи данных:
Передача данных вперед
Передача данных происходит каждый раз, когда на экране появляется новый ViewController.
Это может произойти через segue или программно.
Передача данных вперед между ViewController с использованием segues
Заметка
Всегда правильно задавайте идентификатор для segue. Хорошей практикой будет называть идентификатор тем, что он делает. Потому что может быть ситуации когда Вам нужно передать 2 и более segue в один контроллер. В дальнейшем это поможет свободно ориентироваться в проекте.
Передача данных вперед между ViewController без segues
Иногда вы можете подключить ViewController программно, а не использовать segue.
И готово! Так как в SecondViewController не поменялся код, показываем код FirstViewController :
Передача данных в обратном направлении
Передача данных назад в приложении iOS так же важна, как и их перемещение вперед. Пользователи часто возвращаются к предыдущему экрану, который они посетили.
Когда пользователь взаимодействует с вашим приложением, вы должны обновлять эти предыдущие экраны. Это не происходит автоматически, поэтому вы можете использовать разные методы.
Передача данных в обратном направлении через unwind segue
Передача данных в обратном направлении при помощи делегата
Иногда техники, которые Вы видели, все еще недостаточны.
Заметка
Для закрепления можете сами попробовать сделать это, взяв примеры из 1.2 Передача данных вперед между ViewController без segues.
Продвинутые техники
Замена делегирования на замыкания (closures) Swift
Некоторые разработчики используют замыкания ( closures ) Swift для передачи данных назад между ViewControllers. Этот метод похож на делегирование, но более гибкий. Это также причина, почему я обычно рекомендую не использовать его.
И теперь в FirstViewController также устанавливает связь с этим замыканием, когда происходит переход. Но в этом случае вместо передачи ссылки на себя, он передает замыкание.
Также обратите внимание, что замыкание содержит ссылку на себя. Таким образом, как и делегирование, использование замыканий по-прежнему создает связь между двумя ViewController.
Этот подход немного более лаконичен, чем делегирование. Но использование замыканий также имеет ряд особенностей / недостатков, которых нет у делегирования.
Если вам нужно более одного замыкания для связи с предыдущим контроллером представления, вам нужно сохранить свойство для каждого из них. При делегировании весь интерфейс выделяется внутри протокола, и вам нужно только одно свойство делегата.
На мой взгляд, замыкания лучше работают как обратные вызовы для асинхронных задач, таких как сетевые запросы или анимации. Делегирование является лучшим решением для связи ViewController.
Неправильные техники
Мы познакомились с одними из лучших практик для передачи данных между ViewController. Но к сожалению, в просторах интернета наблюдается и много неправильных.
В этом разделе мы рассмотрим, какие из них, и почему вы не должны их использовать.
Не используйте UserDefaults iOS
В iOS UserDefaults хранят пользовательские настройки, которые должны сохраняться между запусками приложения.
Кроме того, вы можете хранить только простые типы данных в UserDefaults в форме списков свойств. Это означает, что вам нужно преобразовать любой пользовательский тип, прежде чем вы сможете поместить его туда.
В общем, ваше приложение должно получать доступ к UserDefaults через одну точку, которая обычно является настраиваемым контроллером совместно используемой модели.
Не используйте Notifications
Notifications в iOS дают вам канал, по которому какой-то код может отправлять сообщение другим объектам, на которые он не имеет прямой ссылки.
Я видел, как многие разработчики используют Notifications для передачи данных между контроллерами представления. Это не то, для чего они нужны!
Выводы
Как я и говорил, существует много способов передачи данных между ViewController. Но только некоторые из них можно назвать хорошей практикой. Поначалу другие могут показаться удобными, но потом они могут создать Вам проблемы в будущем.
Поэтому выбирайте правильный подход и двигайтесь дальше!
Композиция UIViewController-ов и навигация между ними (и не только)
В этой статье я хочу поделиться опытом который мы успешно используем уже несколько лет в наших iOS приложениях, 3 из которых в данный момент находятся в Appstore. Данный подход хорошо зарекомендовал себя и недавно мы сегрегировали его от остального кода и оформили в отдельную библиотеку RouteComposer о которой собственно и пойдет речь.
Но, для начала, давайте попробуем разобраться что подразумевается под композицией вью контроллеров в iOS.
Все UIViewController ы можно условно разделить на Обычные Вью Контроллеры, которые отвечают за некую видимую область на экране, и Контейнер Вью Контроллеры, которые, помимо отображения себя самих и некоторых своих элементов управления, способны также отображать дочерние вью контролеры интегрированные в них тем или иным способом.
Процесс внедрения стандартных вью контролеров в контейнер вью контроллеры, а также интеграцию вью контролеров в стек вью контролеров мы будем называть композицией в этой статье.
Почему же стандартное решение для композиции вью контроллеров оказалось для нас не оптимальным и мы разработали библиотеку облегчающую наш труд.
Давайте рассмотрим для начала композицию некоторых стандартных контейнер вью контролеров для примера:
Примеры композиции в стандартных контейнерах
UINavigationController
UITabBarController
UISplitViewController
Примеры интеграции (композиции) вью котроллеров в стек
Установка вью контролера рутом
Модальная презентация вью контролера
Почему мы решили создать библиотеку для композиции
Как видно из примеров выше, нет единого способа интеграции обычных вью контролеров в контейнеры, как и нет единого способа построения стека вью контролеров. И, стоит вам захотеть слегка изменить лайаут вашего приложения или способ навигации в нем, как вам потребуются значительные изменения кода приложения, так же вам потребуются ссылки на объекты контейнеров, что бы вы могли вставить в них ваши вью контроллеры и т.д. То есть, сам стандартный способ подразумевает достаточно большой объем работы, а так же наличие ссылок на вью контролеры для порождения действий и презентации других контролеров.
Всему этому добавляют головной боли различные способы дип-линкинга в приложение (например с использованием Universal links), так как вам придется ответить на вопрос: а что если контролер которой нужно показать пользователю так как он кликнул на ссылку в сафари уже показан, или вью контроллер который должен его показать еще не создан, вынуждая вас ходить по дереву вью контролеров и писать код от которого иной раз начинают кровоточить глаза и который любой iOS девелопер старается спрятать. Кроме того, в отличие от Android архитектуры где каждый экран строится отдельно, в iOS чтобы показать какую то часть приложения сразу после запуска может потребоваться построить достаточно большой стек вью контроллеров который будут скрыты под тем который вы показываете по запросу.
Кроме того, зачастую, наши приложения состоят из огромного количества экранов разрабатываемых разными командами, и, что бы добраться до одного из экранов в процессе разработки, нужно пройти через другой экран который возможно еще не создан. В нашей компании мы использовали подход который мы называем чашкой Петри. То есть в режиме разработки девелоперу и тестировщику доступен список всех экранов приложения и он может перейти к любому из них (разумеется к некоторым из них могут потребоваться некоторый входные параметры).
С ними можно взаимодействовать и тестировать индивидуально, а потом собрать в конечное приложение для продакшена. Такой подход сильно облегчает разработку, но, как вы видели из примеров выше, начинается ад композиции, когда нужно держать в коде несколько способов интеграции вью контролера в стек.
Остается добавить, что это все умножится на N как только ваша маркетинговая команда изъявит желание провести A/B тестирование на живых пользователях и проверить какой способ навигации работает лучше, например, таб бар или гамбургер меню?
Я попробую рассказать вам как мы подошли к решению этой проблемы и в конце концов выделили его в библиотеку RouteComposer.
Сусанин Route Composer
Но, о каждой из них по порядку:
Factory
Как и следует из названия Factory отвечает за создание вью контролера.
Далее я воздержусь от приведения описаний протоколов и примеров их реализаций, так как с ними можно подробно ознакомиться скачав пример, поставляемый вместе с библиотекой. Там приведены различные реализации фабрик для обычных вью контролеров и контейнеров, а так же способы их конфигурации.
Action
Сущность Action представляет описывает собой способ интеграции вью контролера, который будет построен фабрикой, в стек. Вью контроллер не может после создания просто повиснуть в воздухе и, поэтому, каждая фабрика должна содержать Action как видно из примера выше.
Самой банальной реализацией Action является модальная презентация контроллера:
Библиотека содержит реализацию большинства стандартных способов интеграции вью контролеров в стек и вам скорее всего не придется создавать свои самостоятельно до тех пор, пока вы не используете некий кастомный контейнер вью контроллер или способ презентации. Но создание кастомных Action не должно вызвать проблем если вы ознакомитесь с примерами.
Finder
Сущность Finder отвечает роутеру на вопрос — А создан ли уже такой вью контроллер и есть ли он уже в стеке? Возможно, ничего создавать не требуется и достаточно показать то что уже есть?.
Если вы храните ссылки на все созданные вами вью контроллеры — то в вашей реализации Finder вы можете просто возвращать ссылку на искомый вью контроллер. Но чаще всего это не так, ведь стек приложения, особенно особенно если оно большое, меняется довольно динамично. Кроме того, вы можете иметь несколько одинаковых вью контроллеров в стеке показывающих разные сущности (например несколько ProductViewController показывающие разные товары с разным productID), поэтому реализация Finder может потребовать кастомной имплементации и поиска соответствующего вью контролера в стеке. Библиотека облегчает эту задачу предоставляя StackIteratingFinder как расширение Finder — протокол с соответствующими настройками, позволяющий упростить эту задачу. В реализации StackIteratingFinder вам потребуется только ответить на вопрос — является ли данный вью контролер тем который роутер ищет по вашему запросу.
Пример такой реализации:
Вспомогательные сущности
RoutingInterceptor
RoutingInterceptor позволяет перед началом композиции вью контролеров выполнить некоторые действия и сообщить роутеру, можно ли интегрировать вью контроллеры в стек. Самым банальным примером такой задачи является аутентификация (но совсем не банальным в реализации). Например, вы хотите показать вью контроллер с деталями пользовательского аккаунта, но, для этого, пользователь должен быть залогинен в системе. Вы можете реализовать RoutingInterceptor и добавить его к конфигурации вью контроллера пользовательских деталей и внутри проверить: если пользователь залогинен — разрешить роутеру продолжить навигацию, если же нет — показать вью контроллер который предложит пользователю залогиниться и если данное действие пройдет успешно — разрешить роутеру продолжить навигацию или отменить ее, если пользователь откажется от входа в систему.
Реализация такого RoutingInterceptor с комментариями содержится в примере поставляемом с библиотекой.
ContextTask
PostRoutingTask
Реализация PostRoutingTask будет вызвана роутером после успешного завершения интеграции запрошенного вью контроллера в стек. В его реализации удобно добавлять различную аналитику или дергать различные сервисы.
Более подробно с реализацией всех описанных сущностей можно ознакомиться в документации к библиотеке а также в прилагаемом примере.
PS: Количество вспомогательных сущностей которое может быть добавлено в конфигурацию не ограничено.
Конфигурация
Все описанные сущности хороши тем что разбивают процесс композиции на маленькие, взаимозаменяемые и хорошо трестируемые блоки.
Теперь перейдем к самому главному — к конфигурации, то есть соединению этих блоков между собой. Для того что бы собрать данные блоки между собой и объединить в цепочку шагов, библиотека предоставляет билдер класс StepAssembly (для контейнеров — ContainerStepAssembly ). Его реализация позволяет нанизать блоки композиции в единый конфигурационный объект как бусины на ниточку, а также указать зависимости от конфигураций других вью контроллеров. Что делать с конфигурацией в дальнейшем — зависит от вас. Можете скормить ее роутеру с необходимыми параметрами и он построит для вас стек вью контроллеров, можете сохранить в словарь и использовать в последствии по ключу — зависит от вашей конкретной задачи.
Если разобрать этот пример без использования библиотеки, то выглядеть он будет примерно вот так:
Данный пример не включает в себя реализацию универсальных ссылок, который потребует вычленения кода авторизации и сохранения контекста куда юзер должен быть направлен после, а также поиска, вдруг пользователь кликнул ссылку, а этот продукт ему уже показан, что, в конечном итоге, сделает код весьма тяжело читаемым.
Рассмотрим конфигурацию этого примера с использованием библиотеки:
Если перевести это на человеческий язык:
Конфигурирование требует некоторого изучения как и многие комплексные решения, например концепция AutoLayout и, на первый взгляд, может показаться сложным и излишним. Однако, количество решаемых задач приведенным фрагментом кода охватывает все аспекты от авторизации до дип-линкига, а разбитие на последовательности действий дает возможность легко менять конфигурацию без необходимости внесения изменений в код. Кроме того, реализация StepAssembly поможет вам избежать проблем с незаконченой цепочкой шагов, а контроль типов — проблем с несовместимостью входных параметров у разных вью котроллеров.
Рассмотрим псевдокод полного приложения в котором в неком ProductArrayViewController выводится список продуктов и, если пользователь выбирает этот продукт, показывает его в зависимости от того залогинен пользователь или нет, или предлагает войти в систему и показывает после успешного входа:
Объекты конфигурации
Реализация списка продуктов
Реализация универсальных ссылок
Вот в сущности и все что требовалось для реализации всех условий из поставленного примера.
Вместо заключения
Библиотека, как и предоставляемая ей реализация роутера, не использует никаких трюков с objective c рантаймом и полностью следует всем концепциям Cocoa Touch, лишь помогая разбить процесс композиции на шаги и выполняет их в заданной последовательности. Библиотека протестирована с версиями iOS с 9 по 12.
Данный подход вписывается во все архитектурный паттерны которые подразумевают работу с UIViewController стеком (MVC, MVVM, VIP, RIB, VIPER и т.д.)
Библиотека находится в активной разработке и, тем не менее, как я уже писал выше, используется в продакшне. Я бы рекомендовал опробовать данный подход и поделиться впечатлениями. В нашем случае это позволило нам избавиться от большого количества головной боли.
Буду рад вашим комментариям и предложениям.