Несколько лет назад, когда я еще работал в мобильном консалтинге, я работал над приложением для крупного инвестиционного банка. В крупных компаниях, особенно в банках, обычно существуют процессы, обеспечивающие безопасность, надежность и удобство обслуживания программного обеспечения.
Часть этого процесса включала отправку кода приложения, которое я написал, третьей стороне для проверки. Это не беспокоило меня, потому что я думал, что мой код безупречен, и что рецензент скажет то же самое.
Когда их ответ вернулся, вердикт был другим, чем я думал. Хотя они сказали, что качество кода было неплохим, они указали на тот факт, что код было сложно поддерживать и тестировать (модульное тестирование тогда не было популярным в разработке для iOS).
Я отклонил их суждение, думая, что мой код был великолепен и не было никакого способа, которым он мог быть улучшен. Они просто не должны этого понимать!
У меня была типичная гордость разработчика: мы часто думаем, что то, что мы делаем, прекрасно, а другие не понимают этого.
Оглядываясь назад, я ошибался. Не намного позже я начал читать о некоторых лучших методах. С тех пор проблемы в моем коде начали появляться как больной палец. Я понял, что, как и многие разработчики iOS, я попал в ряд классических ловушек плохой практики кодирования.
Что большинство разработчиков iOS ошибаются
Одна из наиболее распространенных плохих практик разработки для iOS возникает при передаче состояния между контроллерами представления приложения. Я сам попал в эту ловушку в прошлом.
Распространение состояний через контроллеры представления жизненно важно в любом приложении iOS. Когда ваши пользователи перемещаются по экранам вашего приложения и взаимодействуют с ним, вам необходимо сохранять глобальное состояние, которое отслеживает все изменения, которые пользователь вносит в данные.
И именно здесь большинство разработчиков iOS ищут очевидное, но неверное решение: шаблон синглтона.
Шаблон Singleton очень быстро внедряется, особенно в Swift, и он работает хорошо. Вам просто нужно добавить статическую переменную в класс, чтобы сохранить общий экземпляр самого класса, и все готово.
1
2
3
|
class Singleton {
static let shared = Singleton()
}
|
Затем легко получить доступ к этому общему экземпляру из любой точки вашего кода:
1
|
let singleton = Singleton.shared
|
По этой причине многие разработчики считают, что нашли лучшее решение проблемы распространения состояния. Но они не правы.
Паттерн синглтона фактически считается анти-паттерном Было много обсуждений этого в сообществе разработчиков. Например, см. Этот вопрос переполнения стека .
Короче говоря, одиночные игры создают следующие проблемы:
- Они привносят множество зависимостей в ваши классы, затрудняя их изменение в будущем.
- Они делают глобальное состояние доступным для любой части вашего кода. Это может создать сложные взаимодействия, которые трудно отследить и вызвать много неожиданных ошибок.
- Они делают ваши уроки очень сложными для тестирования, так как вы не можете легко отделить их от одиночного.
На этом этапе некоторые разработчики думают: «Ах, у меня есть лучшее решение. AppDelegate
этого я буду использовать AppDelegate
».
Проблема заключается в том, что класс AppDelegate
в приложениях iOS доступен через общий экземпляр UIApplication
:
1
|
let appDelegate = UIApplication.shared.delegate
|
Но общий экземпляр UIApplication
сам по себе является UIApplication
. Значит, ты ничего не решил!
Решением этой проблемы является внедрение зависимостей. Внедрение зависимостей означает, что класс не извлекает и не создает свои собственные зависимости, но получает их извне.
Чтобы увидеть, как использовать внедрение зависимостей в приложениях iOS и как он может разрешить совместное использование состояний, нам сначала нужно пересмотреть один из фундаментальных архитектурных шаблонов приложений iOS: шаблон Model-View-Controller.
Расширение шаблона MVC
Короче говоря, в паттерне MVC говорится, что в архитектуре приложения для iOS есть три уровня:
- Слой модели представляет данные приложения.
- Слой вида показывает информацию на экране и позволяет взаимодействовать.
- Уровень контроллера действует как клей между двумя другими слоями, перемещая данные между ними.
Обычное представление шаблона MVC выглядит примерно так:
Проблема в том, что эта диаграмма неверна.
Этот «секрет» скрывается в виде нескольких строк в документации Apple :
«Можно объединить роли MVC, которые выполняет объект, например, чтобы объект выполнял роли контроллера и представления — в этом случае он будет называться контроллером представления. Точно так же вы можете иметь объекты модели-контроллера ».
Многие разработчики считают, что контроллеры представлений являются единственными контроллерами, которые существуют в iOS-приложении. По этой причине, много кода заканчивается написанием внутри них из-за отсутствия лучшего места. Это то, что заставляет разработчиков использовать синглтоны, когда им нужно распространять информацию о состоянии: это кажется единственно возможным решением.
Из приведенных выше строк ясно, что мы можем добавить новую сущность к нашему пониманию шаблона MVC: контроллер модели. Контроллеры модели работают с моделью приложения, выполняя роли, которые сама модель не должна выполнять. Вот как должна выглядеть приведенная выше схема:
Прекрасный пример того, когда полезен контроллер модели, — это сохранение состояния приложения. Модель должна представлять только данные вашего приложения. Состояние приложения не должно быть его заботой.
Сохранение этого состояния обычно заканчивается внутри контроллеров представления, но теперь у нас есть новое и лучшее место для его установки: контроллер модели. Затем этот контроллер модели можно передать для просмотра контроллеров, когда они появляются на экране посредством внедрения зависимостей.
Мы решили синглтон анти-шаблон. Давайте посмотрим наше решение на практике на примере.
Распространение состояния между контроллерами представления с помощью внедрения зависимостей
Мы собираемся написать простое приложение, чтобы увидеть конкретный пример того, как это работает. Приложение покажет вашу любимую цитату на одном экране и позволит вам редактировать цитату на втором экране.
Это означает, что нашему приложению понадобятся два контроллера представления, которые должны будут совместно использовать состояние. После того, как вы увидите, как работает это решение, вы можете расширить концепцию до приложений любого размера и сложности.
Для начала нам нужен тип модели для представления данных, который в нашем случае является кавычкой. Это можно сделать с помощью простой структуры:
1
2
3
4
|
struct Quote {
let text: String
let author: String
}
|
Контроллер модели
Затем нам нужно создать контроллер модели, который будет содержать состояние приложения. Эта модель контроллера должна быть классом. Это потому, что нам понадобится один экземпляр, который мы передадим всем нашим контроллерам представления. Типы значений, такие как структуры, копируются, когда мы их передаем, поэтому они явно не являются правильным решением.
В нашем примере все, что нужно нашему контроллеру модели, — это свойство, в котором он может хранить текущую кавычку. Но, конечно, в больших приложениях контроллеры моделей могут быть более сложными, чем это:
1
2
3
4
5
6
|
class ModelController {
var quote = Quote(
text: «Two things are infinite: the universe and human stupidity; and I’m not sure about the universe.»,
author: «Albert Einstein»
)
}
|
Я назначил значение по умолчанию для свойства quote, чтобы у нас уже было что отображать на экране при запуске приложения. В этом нет необходимости, и вы можете объявить свойство необязательным, инициализированным как nil , если вы хотите, чтобы ваше приложение запускалось с пустым состоянием.
Создать пользовательский интерфейс
Теперь у нас есть модель контроллера, которая будет содержать состояние нашего приложения. Далее нам нужны контроллеры представления, которые будут представлять экраны нашего приложения.
Сначала мы создаем их пользовательские интерфейсы. Так выглядят два контроллера представления внутри раскадровки приложения.
Интерфейс первого контроллера представления состоит из пары меток и кнопки, соединенных с простыми автоматическими ограничениями макета. (Вы можете прочитать больше об автоматической компоновке здесь на Envato Tuts + .)
Интерфейс второго контроллера представления такой же, но имеет текстовое представление для редактирования текста цитаты и текстовое поле для редактирования автора.
Два контроллера представления связаны одним модальным переходом представления, который происходит от кнопки Изменить цитату .
Вы можете изучить интерфейс и ограничения контроллеров представления в репозитории GitHub .
Кодирование контроллера представления с внедрением зависимостей
Теперь нам нужно кодировать наши контроллеры представления. Здесь важно помнить, что им нужно получать экземпляр контроллера модели извне, посредством внедрения зависимостей. Поэтому им нужно выставить недвижимость для этой цели.
1
|
var modelController: ModelController!
|
Мы можем назвать наш первый контроллер представления QuoteViewController
. Этот контроллер представления нуждается в нескольких выходах к меткам для цитаты и автора в его интерфейсе.
1
2
3
4
5
6
|
class QuoteViewController: UIViewController {
@IBOutlet weak var quoteTextLabel: UILabel!
@IBOutlet weak var quoteAuthorLabel: UILabel!
var modelController: ModelController!
}
|
Когда этот контроллер представления появляется на экране, мы заполняем его интерфейс, чтобы показать текущую цитату. Мы поместили код для этого в метод viewWillAppear(_:)
контроллера.
01
02
03
04
05
06
07
08
09
10
11
12
13
|
class QuoteViewController: UIViewController {
@IBOutlet weak var quoteTextLabel: UILabel!
@IBOutlet weak var quoteAuthorLabel: UILabel!
var modelController: ModelController!
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
let quote = modelController.quote
quoteTextLabel.text = quote.text
quoteAuthorLabel.text = quote.author
}
}
|
Вместо этого мы могли бы поместить этот код в метод viewDidLoad()
, что довольно часто. Проблема, однако, заключается в том, что viewDidLoad()
вызывается только один раз, когда создается контроллер представления. В нашем приложении нам нужно обновлять пользовательский интерфейс QuoteViewController
каждый раз, когда он появляется на экране. Это потому, что пользователь может редактировать цитату на втором экране.
Вот почему мы используем метод viewWillAppear(_:)
вместо viewDidLoad()
. Таким образом, мы можем обновлять пользовательский интерфейс контроллера представления каждый раз, когда он появляется на экране. Если вы хотите узнать больше о жизненном цикле контроллера представления и всех вызываемых методах, я написал статью, подробно описывающую все из них .
Контроллер Edit View
Теперь нам нужно кодировать второй контроллер вида. Мы назовем этот EditViewController
.
01
02
03
04
05
06
07
08
09
10
11
12
13
|
class EditViewController: UIViewController {
@IBOutlet weak var textView: UITextView!
@IBOutlet weak var textField: UITextField!
var modelController: ModelController!
override func viewDidLoad() {
super.viewDidLoad()
let quote = modelController.quote
textView.text = quote.text
textField.text = quote.author
}
}
|
Этот контроллер вида похож на предыдущий:
- Он имеет выходы для просмотра текста и текстовое поле, которое пользователь будет использовать для редактирования цитаты.
- У него есть свойство для внедрения зависимостей экземпляра контроллера модели.
- Он заполняет свой пользовательский интерфейс до появления на экране.
В этом случае я использовал метод viewDidLoad()
потому что этот контроллер представления появляется на экране только один раз.
Делить государство
Теперь нам нужно передать состояние между двумя контроллерами представления и обновить его, когда пользователь редактирует кавычку.
Мы передаем состояние приложения в методе prepare(for:sender:)
для QuoteViewController
. Этот метод вызывается подключенным переходом, когда пользователь нажимает кнопку « Изменить цитату» .
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
|
class QuoteViewController: UIViewController {
@IBOutlet weak var quoteTextLabel: UILabel!
@IBOutlet weak var quoteAuthorLabel: UILabel!
var modelController: ModelController!
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
let quote = modelController.quote
quoteTextLabel.text = quote.text
quoteAuthorLabel.text = quote.author
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if let editViewController = segue.destination as?
editViewController.modelController = modelController
}
}
}
|
Здесь мы передаем экземпляр ModelController
который сохраняет состояние приложения. Здесь происходит внедрение зависимости для EditViewController
.
В EditViewController
мы должны обновить состояние до вновь введенной кавычки, прежде чем мы вернемся к предыдущему контроллеру представления. Мы можем сделать это в действии, связанном с кнопкой Сохранить :
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
|
class EditViewController: UIViewController {
@IBOutlet weak var textView: UITextView!
@IBOutlet weak var textField: UITextField!
var modelController: ModelController!
override func viewDidLoad() {
super.viewDidLoad()
let quote = modelController.quote
textView.text = quote.text
textField.text = quote.author
}
@IBAction func save(_ sender: AnyObject) {
let newQuote = Quote(text: textView.text, author: textField.text!)
modelController.quote = newQuote
dismiss(animated: true, completion: nil)
}
}
|
Инициализировать контроллер модели
Мы почти закончили, но вы, возможно, заметили, что мы все еще что-то QuoteViewController
: QuoteViewController
передает ModelController
в EditViewController
посредством внедрения зависимостей. Но кто в первую очередь передает этот экземпляр QuoteViewController
? Помните, что при использовании внедрения зависимостей контроллер представления не должен создавать свои собственные зависимости. Это должно прийти извне.
Но до QuoteViewController
нет контроллера QuoteViewController
, потому что это первый контроллер представления нашего приложения. Нам нужен какой-то другой объект для создания экземпляра ModelController
и передачи его в QuoteViewController
.
Этот объект является AppDelegate
. Роль делегата приложения заключается в том, чтобы реагировать на методы жизненного цикла приложения и соответствующим образом настраивать приложение. Одним из этих методов является application(_:didFinishLaunchingWithOptions:)
, которое application(_:didFinishLaunchingWithOptions:)
при запуске приложения. Вот где мы создаем экземпляр ModelController
и передаем его в QuoteViewController
:
1
2
3
4
5
6
7
8
9
|
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
if let quoteViewController = window?.rootViewController as?
quoteViewController.modelController = ModelController()
}
return true
}
}
|
Наше приложение завершено. Каждый контроллер представления получает доступ к глобальному состоянию приложения, но мы нигде не используем синглтоны в нашем коде.
Вы можете скачать проект Xcode для этого примера приложения в репозитории GitHub .
Выводы
В этой статье вы увидели, что использование одиночных кодов для распространения состояния в приложении iOS — плохая практика. Одиночные игры создают много проблем, несмотря на то, что их очень легко создавать и использовать.
Мы решили проблему, присмотревшись к паттерну MVC и понимая скрытые в нем возможности. Благодаря использованию контроллеров моделей и внедрению зависимостей, мы смогли распространить состояние приложения на все контроллеры представления без использования синглетонов.
Это простой пример приложения, но концепцию можно обобщить для приложений любой сложности. Это стандартная лучшая практика для распространения состояния в приложениях iOS. Теперь я использую его в каждом приложении, которое я пишу для своих клиентов.
Несколько вещей, о которых следует помнить, когда вы расширяете концепцию до более крупных приложений:
- Контроллер модели может сохранить состояние приложения, например, в файле. Таким образом, наши данные будут помнить каждый раз, когда мы закрываем приложение. Вы также можете использовать более сложное решение для хранения данных, например Core Data. Я рекомендую хранить эту функциональность в отдельном контроллере модели, который заботится только о хранилище. Этот контроллер может затем использоваться контроллером модели, который сохраняет состояние приложения.
- В приложении с более сложным потоком у вас будет много контейнеров в потоке приложения. Обычно это навигационные контроллеры со случайным контроллером панели вкладок. Концепция внедрения зависимостей по-прежнему применима, но вы должны принять во внимание контейнеры. Вы можете либо копаться в содержащихся в них контроллерах представления при выполнении внедрения зависимостей, либо создавать собственные подклассы контейнеров, которые передают контроллер модели.
- Если вы добавляете сеть к своему приложению, это также должно происходить в отдельном контроллере модели. Контроллер представления может выполнить сетевой запрос через этот сетевой контроллер и затем передать полученные данные контроллеру модели, который сохраняет состояние. Помните, что роль контроллера представления заключается именно в том, чтобы действовать как связующий объект, который передает данные между объектами.
Следите за новыми советами и рекомендациями по разработке приложений для iOS!