Advertisement
  1. Code
  2. iOS SDK

Правильный способ передавать состояние между View Controllers в Swift

by
Read Time:13 minsLanguages:

Russian (Pусский) translation by Ellen Nelson (you can also view the original English article)

Final product imageFinal product imageFinal product image
What You'll Be Creating

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

Часть этого процесса заключается в отправке кода приложения, который я написал третей стороне, для рассмотрения. Меня это не напрягало, потому что я думал что мой код был безупречен и что компания, рассматривающая код скажет тоже самое.

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

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

Как у разработчика у меня было типичное высокомерие: мы часто думаем, что то, что мы делаем отлично, а другие просто этого не понимают.

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

Что большинство iOS разработчиков делают неправильно

Одна из наиболее распространенных проблем при разработки для iOS, возникает при передаче состояния между контроллерами вида приложения — "view controller". В прошлом я сам попадал в эту ловушку.

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

И именно здесь большинство iOS разработчиков делаю очевидные, но неправильные решения: шаблон проектирования «одиночка».

Шаблон «одиночка» очень быстро реализовать, особенно в Swift, и он хорошо работает. Вам просто нужно добавить статическую переменную в класс, чтобы сохранить общий экземпляр самого класса, и все готово.

А потом легко получить доступ к этому общему экземпляру из любого места в вашем коде:

По этой причине многие разработчики считают, что они нашли наилучшее решение проблемы передача состояний. Но они ошибаются.

Шаблон «одиночка» фактически считается анти-шаблоном. В сообществах разработчиков было очень много разговоров на эту тему. Примеру можете посмотреть вопросы на Stack Overflow.

Вкратце, «одиночки» создают следующие проблемы:

  • Это добавляет большое количество зависимостей в своих классах, что усложняет изменение их в будущем.
  • Это делает так, что глобальные состояния доступны из любой части кода. Это может создать сложности при взаимодействии, что сложно отследить и приводит к множеству неожиданных багов.
  • Это усложняет тестирование ваших классов, так как вы не можете запросто отделить их от «одиночки».

На этой стадии, некоторые разработчики думают: "Аа, У меня есть решение получше. Вместо этого я буду использовать AppDelegate.

Проблема в том, что в iOS приложениях к классу AppDelegate можно получить доступ через общий экземпляр UIApplication:

Но, дело в том, что этот общий экземпляр UIApplication сам является «одиночкой». Поэтому, вы ничего не решили!

Решением этой проблемы является внедрение зависимостей. Внедрение зависимостей означает, что класс не принимает или создает свои собственные зависимости, но принимает их извне.

Чтобы посмотреть, как использовать внедрение зависимостей в приложениях iOS и как с помощью этого можно передавать состояние, сначала нам нужно пересмотреть один фундаментальный шаблон проектирования приложений для iOS: «Модель–Вид–Контроллер» — Model-View-Controller.

Дополнение шаблона MVC

Шаблон MVC, в двух словах, утверждает, что в архитектуре приложений iOS есть три уровня:

  • Уровень модели представляет данные приложения.
  • Слой вида отображает информацию на экране и позволяет взаимодействовать с ней.
  • Слой контроллер действует как клей между двумя другими слоями, перемещая данные между ними.

Обычное представление MVC шаблона выглядит примерно так:

Simplistic view of the MVC patternSimplistic view of the MVC patternSimplistic view of the MVC pattern

Проблема в том, что эта диаграмма неверна.

Эта «тайна» кроется на виду, в нескольких строках документации Apple:

"Можно объединить роли MVC в объект, создав объект, например, чтобы он выполнял обе роли и контроллера, и вида — в этом случае, это будет называться контроллером вида (view controller). Таким же образом, вы можете создать объект модель-контроллер."

Многие разработчики полагают, что контроллер вида, это единственный контроллер, который существует в iOS приложениях. По этой причине, большая часть кода пишется внутри их, за неимением лучшего места. Именно это приводит разработчиков к использованию «одиночки», когда им нужно передать состояние: это кажется единственным решением.

Из строк, процитированных выше, становится ясно, что мы можем добавить новую сущность в наше понимание шаблона MVC: модель-контроллер. Контроллеры модели имеют дело с моделью приложения, исполняя роли, которые модель, сама по себе, не должна выполнять. Вот как, в действительности, должна выглядеть схема представленная выше:

Diagram of the MVC pattern updated with view and model controllersDiagram of the MVC pattern updated with view and model controllersDiagram of the MVC pattern updated with view and model controllers

Идеальный пример того, когда контроллеры модели полезны для поддержания состояния предложения. Модель должна только обрабатывать данные вашего приложения. Состояние приложения не должно относиться к её обязанностям.

Хранение состояния обычно не выходит за рамки контроллера вида, но теперь, У нас есть более подходящие для этого место: контроллер модели. Такой контроллер модели затем можно передать в контроллер вида, когда те показываются на экране через внедрение зависимости.

Мы решили анти-шаблон «одиночка». Давайте посмотрим на наше решение в действии и с примерами.

Распространение состояния через контроллеры вида (View Controllers) через внедрение зависимости

Мы напишем простое приложение, чтобы посмотреть как работает конкретный пример. Приложение будет показывать на экране ваши любимые цитаты, с возможностью редактировать цитаты на втором экране (виде).

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

Для начала, вам нужен тип модели для отображения данных, что в нашем случае является цитатой. Это можно сделать очень просто:

Контроллер модели

Зачем нам нужно создать контроллер модели, который содержит состояние приложения. Этот контроллер модели должен быть классом. Это потому, что нам нужeн единственный экземпляр, который мы будем передавать во все наши контроллеры вида. Значения с таким типом как struct, копируются при их передаче, поэтому они определённо не является правильным решением.

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

Я назначил значение по умолчанию на свойство quote, чтобы нам было что показать на экране, при запуске приложения. Это не обязательно и вы можете объявить это свойство необязательным, задав его как nil, если вы хотите, чтобы ваше приложение запустилась с пустым состоянием.

Создаем интерфейс пользователя

Теперь у нас есть контроллер модели, который будет содержать состояние нашего приложения. Далее, нам понадобится контроллер вида, который будет отображать экраны нашего приложения.

Для начала, создадим их пользовательский интерфейс. Так выглядят два контроллера вида внутри раскадровки приложения (storyboard).

view controllers in the storyboardview controllers in the storyboardview controllers in the storyboard

Интерфейс первого контроллера вида сделан из нескольких кнопок и заголовков, в сочетании с простыми ограничениями автоматической компоновки. (Вы можете прочитать подробнее об автоматической компоновке здесь на Envato Tuts+.)

Интерфейс второго контроллера вида такой же, только содержит текст (text view) для редактирования текста цитаты и текстовое поле (text field) для изменения автора.

Эти два контроллера вида связаны единым модальным представлениям перехода, который берет своё начало на кнопке Edit quote (Редактировать цитату).

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

Пишем контроллер вида с добавлением зависимости

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

Мы можем называть наш первый контроллер вида как QuoteViewController. В интерфейсе этого контроллера вида потребуется несколько выводов (outlet) для меток цитаты и автора.

Когда этот контроллер вида появляется на экране, мы заполняем его интерфейс, отобразив в текущую цитату. Код, который это делает, мы размещаем в методе контроллера viewWillAppear(_:).

Мы могли бы разместить этот код внутри метода viewDidLoad(), что было бы более привычно. Проблема в том, что viewDidLoad() вызывается только один раз, при создании контроллера вида. В нашем приложении, нам нужно обновлять пользовательский интерфейс QuoteViewController каждый раз когда он отображается на экране. Это нужно, потому что пользователь может редактировать цитату на втором экране.

Вот почему мы используем метод viewWillAppear(_:) вместо viewDidLoad(). Таким образом мы можем обновлять UI контроллера вида, каждый раз когда он появляется на экране. Если вы хотите больше узнать о жизненном цикле контроллера вида и всех вызываемых методах, я написал статью, в которой подробно описываются все о них.

Редактирование контроллера вида

Теперь нам нужно написать код для второго контроллера вида. Мы можем назвать его EditViewController.

Этот контроллер вида похож на предыдущий:

  • Есть выводы (outlet) для текстового отображения (text view, строка 2) и текстового поля (text field, строка 3), которое пользователь будет использовать для редактирования цитаты.
  • Есть свойство для внедрения зависимости в экземпляр контроллера вида.
  • Это заполняет интерфейс пользователя, перед показом на экране.

В этом случае, я использовал метод viewDidLoad(), потому что, этот контроллер вида выйдет на экран только один раз.

Передача состояния

Теперь, нам надо передать состояние между двумя контроллерами вида и обновить его, если пользователь отредактировал цитату.

Мы передаем состояние приложение методом prepare(for:sender:) от QuoteViewController. Этот метод запускается подключением segue, когда пользователь нажимает кнопку «Редактировать цитату» (Edit quote).

Здесь мы передаем дальше экземпляр ModelController, который содержит состояние приложения. Вот где происходит внедрение зависимости для EditViewController.

В EditViewController,Нам нужно обновить состояние до новой введённой цитаты, прежде чем вернуться к предыдущему контроллеру вида. Мы можем делать это по действию назначенному на кнопку Save:

Инициализация контроллера модели

Мы почти закончили, но вы наверно заметили, что мы всё же что-то пропустили: QuoteViewController передает ModelController в EditViewController через внедрение зависимости. А кто в первую очередь передает этот экземпляр в QuoteViewController? Помните, что при внедрении зависимостей, контроллер вида не должен создавать свои собственные зависимости. Они должны поступить извне.

Но перед QuoteViewController нет никакого контроллера вида, потому что это первый контроллер вида в нашем приложении. Нам нужно какой-то другой объект, для создания экземпляра ModelController и передачи его в QuoteViewController.

Этим объектом будет AppDelegate. Его роль — реагировать на методы жизненного цикла приложения и настраивать приложение соответственно им. Один из этих методов application(_:didFinishLaunchingWithOptions:), который называется сразу, как только запускается приложение. Вот где мы создаем экземпляр ModelController и передаем его к QuoteViewController:

Теперь наше приложение готово. Каждый контроллер вида получает доступ к глобальному состоянию приложения, и мы обошлись без использования «одиночки».

Вы можете загрузить пример проекта Xcode для этого приложения из репозитория на GitHub.

Заключение

В этой статье вы увидели, что использование «одиночки» для передачи состояния в приложении iOS — это плохой выбор. «Одиночки» создают множество проблем, несмотря на то, что их очень легко создавать и использовать.

Мы решили проблему, более внимательно рассмотрев модель MVC и поняв её скрытые возможности. Благодаря использованию контроллеров модели и внедрение зависимостей мы смогли передать состояние приложения на все контроллеры вида, не прибегая к использованию шаблона «одиночка».

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

Несколько вещей, о которых нужно помнить, когда вы расширяете концепцию до более крупных приложений:

  • Контроллер модели может сохранять состояние приложения, например в файле. Таким образом, наши данные будут сохраняться каждый раз, при закрытии приложения. Вы также можете использовать более сложное решение для хранения данных, например Core Data. Я рекомендую держать это функциональность в отдельном контроллере модели, которые покрывают только работу с хранилищем. Тогда этот контроллер может быть использован контроллером модели, который содержит состояние приложение.
  • В приложении с более сложным потоком, у вас будет больше контейнеров в потоке вашего приложения. Как правило, это контроллеры навигации, со случайным контроллером панели вкладок. Концепция внедрения зависимостей по-прежнему применяется, но вам нужно учитывать контейнеры. При выполнении внедрения зависимостей вы можете либо вникать с содержащие их контроллеры вида, либо создавать собственные подклассы контейнеров, которые передают контроллер модели.
  • Если вы добавили в ваше приложение работу с интернетом, это должно идти в отдельном контроллере модели. Контроллер вида может выполнять сетевой запрос через этот сетевой контроллер, а затем передавать полученные данные контроллеру модели, который сохраняет состояние. Помните, что роль контроллера вида заключается в следующем: действовать как объект клея, который передает данные между объектами.

Оставайтесь с нами, чтобы узнать больше подсказок и советов по разработке приложений для iOS!

Advertisement
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.