Эта статья для уровня trainee, а значит для совсем начинающих великолепных разработчиков
Основная цель статьи - рассказать просто, на примере, как можно использовать паттерн делегирования в Swift
.
Статья состоит из двух частей. Первая - удалим из проекта Storyboard
и напишем кодом простой интерфейс. Вторая - разберем, как с помощью делегата передать данные на предыдущий контроллер.
Часть 1
Создаем новый проект, назовем его DelegatePattern:
Размещать элементы будем кодом, поэтому удаляем Storyboard
из проекта:
Выбираем проект (стрелка 1), вкладка General
и в разделе Deployment Info
выделяем и удаляем Main
(стрелка 2)
Переходим в файл info.plist
(стрелка 1) и удаляем строку Application Scene Manifest
У нас не будет поддержки IPad
- файл SceneDelegate
можно тоже удалить:
Так как мы удалили Storyboard
, в файле AppDelegate
объявим свойство window
, а в методе application didFinishLaunchingWithOptions
, нужно добавить код ниже, чтобы указать стартовый контороллер. Остальные методы удалим, в нашем проекте они использоваться не будут.
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
window = UIWindow(frame: UIScreen.main.bounds)
let rootVC = ViewController()
window?.rootViewController = rootVC
window?.makeKeyAndVisible()
return true
}
Теперь AppDelegate
выглядит вот так:
Переходим во ViewController
, в методе viewDidLoad
добавляем фиолетовый цвет для бэкграунда view
контроллера:
Запускаем проект, и если все сделали верно, запустится симулятор с фиолетовым контроллером:
Отлично, теперь мы можем продолжать писать интерфейс в коде. Для начала создадим еще один ViewController
. Нажимаем Command + N
и выбираем Cocoa Touch Class
:
Назовем его SecondViewController:
Поработаем с ViewController
, добавим на него кнопку перехода на SecondViewController:
Объявим новый метод makeConstraints
, в нем добавим кнопку на view
и создадим констрейнты.
private func makeConstraints() {
view.addSubview(toSecondViewControllerButton) // добавляем кнопку на view
NSLayoutConstraint.activate([
toSecondViewControllerButton.centerXAnchor.constraint(equalTo: view.centerXAnchor), // центр по оси Х
toSecondViewControllerButton.centerYAnchor.constraint(equalTo: view.centerYAnchor) // центр по оси Y
])
}
И обязательно, нужно установить значение свойстваbutton.translatesAutoresizingMaskIntoConstraints = false
в клоужере создания кнопки. Вот как теперь выглядит ViewController
:
Проверим симулятор:
Теперь аналогично добавим UILabel, в нем в последующем и будем менять текст при возвращении из следующего контроллера.
Можете сами потренироваться и создать лейбл. Если возникнут проблемы, подглядите здесь :)
Первым делом добавим сам элемент UILabel:
Добавляем лейбл на view
и, создадаем констрейнты, расширив метод makeConstraints
:
private func makeConstraints() {
view.addSubview(toSecondViewControllerButton)
view.addSubview(someLabel) // добавляем на экран
NSLayoutConstraint.activate([
toSecondViewControllerButton.centerXAnchor.constraint(equalTo: view.centerXAnchor),
toSecondViewControllerButton.centerYAnchor.constraint(equalTo: view.centerYAnchor),
someLabel.centerXAnchor.constraint(equalTo: toSecondViewControllerButton.centerXAnchor), // центр по оси X
someLabel.centerYAnchor.constraint(equalTo: toSecondViewControllerButton.centerYAnchor, constant: 40) // центр по оси Y
])
}
Отлично, теперь на view
есть кнопка и лейбл. Симулятор выглядят так:
Чтобы кнопка заработала и при ее нажатии открывался следующий экран, добавим ей свойство button.addTarget():
Объявим методtoSecondVCButtonPressed()
, который будет отрабатывать по нажатию на кнопку:
Перейдем в SecondViewController
и в методе viewDidLoad()
добавим цвет бэкграунда view
второго контроллера:
Проверяем, что получилось, собираем проект. Если все сделано верно, по нажатию на кнопку Go to second VC
, откроется второй контроллер с серым фоном.
Часть 2
Пора приступать к передаче данных при закрытии "серого" контроллера. Как вы видели в названии статьи, будем использовать делегат :)
Объявим протокол SecondViewControllerDelegate
в файле класса ViewController
и обязательно укажем тип AnyObject
. Это нужно для того, чтобы протокол работал с классами, а это, в свою очередь, позволит создавать слабые ссылки и избежать retain cycle между контроллерами. Наш протокол будет содержать только один метод - для замены текста в лейбле ViewController
'a.
Реализовывать метод протокола будет ViewController
, подпишем его под протокол в extension
'е и напишем логику для метода, которая будет менять текст:
В аргумент text
, находящийся на 68 строке, придет новый текст с другого контроллера, а на 69 строке мы заменим стандартный текст лейбла.
Если навести курсор на аргумент text
, Xcode
подсветит какой text
к какому относится.
Переходим в SecondViewController
. Помните мы создали протокол и объявили его anyObject
? Теперь пора создать слабую ссылку, которая будет иметь тип делегата, она будет жить в SecondViewController
и через нее мы сможем добраться до методов делегата.
Когда мы закрываем SecondViewController
, смахивая его вниз, срабатывает метод deinit
. В нашем примере это отличное место, чтобы передать новый текст в лейбл ViewController
'a. Добираемся через переменную delegate
до метода newTextForLabel
и передаем в него новый текст для лейбла на первом контроллере "New text".
(*PS: метод deinit() в статье используется только для примера, поскольку мы точно уверены в том, что контроллер выгрузится из памяти.)
Как пример, если создать кнопку закрытия второго экрана, тогда self.delegate?.newTextForLabel(text: "New text")
поселился бы в методе, срабатывающем по нажатию на кнопку закрытия.
Все почти готово, осталось дело за малой деталью, о которой лично я всегда забываю :) нужно сообщить нашему SecondViewController
, кто будет его делегатом (то есть кто будет что-то делать с теми данными которые, он отправил).
Возвращаемся в ViewController,
и там, где мы создавали для кнопки метод перехода на SecondViewController
подпишемся под делегата:
Собираем проект и проверяем:
PS: Почему текст обновляется с задержкой? Дело в том, что deinit
выгружает контроллер из памяти, поэтому проходит какое-то количество времени, пока контроллер выгрузится и текст сменится. Если вы будете использовать делегат, например, в методе UIViewController
'a - dismiss()
или в вашем методе кнопки, то никаких задержек не будет.
GitHub с финальным проектом - ссылка.
Спасибо, что дочитали :)
Комментарии (10)
RileyUsagi
05.12.2022 11:20Создавать проект со сторибордами, чтобы затем удалять их. Любопытный подход.
Может быть проще было сразу создать современный проект без всего этого?ForestLamp Автор
05.12.2022 11:31Интерфейс очень легкий, а одно из ключевых мест - создание кнопки с таргетом. Создав его самостоятельно, думаю, что человек более детально сможет разобраться и подобраться к месту подписки на делегата. Обычно этот момент вызывает много вопросов.
storoj
Я не понимаю людей, которые плюсуют такие статьи. Это просто шок контент.
storoj
Мало того, что половина статьи вообще не имеет ни малейшего отношения к делегатам, так и сам делегат сделан неправильно!
storoj
Меня неимоверно огорчают статьи на тему iOS. На мой взгляд, в 99% сталкиваешься с просто вопиющей неграмотностью, которую я могу объяснить лишь полнейшим непониманием пределов собственного познания. Что ещё хуже обычной некомпетентности.
Код картинками. Ну это ладно;
Слишком большой фокус на удалении сториборда. Чёрт бы с ним, статья вообще не об этом, пусть бы был сториборд. Но это тоже ладно;
Не критика, но если уж хочется сделать тестовый экран с какой-то кнопкой, я всегда делаю rightBarButtonItem с одним из системных "стилей". Не нужно делать никакого лейаута, никаких пропертей, не нужно даже заголовок придумывать;
Всё началось с
newtextForLabel
. Как это вообще? Я понимаю, что это "мелочи", но мне это уже о многом говорит. Почему "text" не с большой буквы, хотя всё остальное в camelCase?;ViewControllerDelegate
. Это тоже для меня признак полнейшего непонимания темы. По-моему, по всем признакам это должно было называтьсяSecondViewControllerDelegate
;"Когда мы закрываем SecondViewController, смахивая его вниз, срабатывает метод deinit. В нашем примере это отличное место, чтобы передать новый текст в лейбл." – я многое видел, но такого я ещё не видел. Это как минимум просто неверное допущение – надеяться на то, что при "закрытии" UIViewController он обязательно будет деаллоцирован;
метод делегата мало того, что имеет нелепый нейминг (не сообщает о случившемся событии, не спрашивает никаких данных), так и не имеет первым аргументом объекта, пославшего событие. В Delegates and Data Sources есть пример того, как стоит оформлять подобные методы (раздел "The Form of Delegation Messages");
Кошмар, просто кошмар. Я всё чаще начинаю сам себя презирать за то, что продолжаю заниматься разработкой под iOS, потому что уже просто стыдно находиться в таком сообществе. На свой личный счёт это принимать не стоит, но раньше писатели статей хотя бы одну книжку прочитывали перед тем, как что-то опубликовать. Теперь как будто бы и этого не происходит.
storoj
... и если 10 лет назад не было толком никаких хороших ресурсов, мало где было что-то почитать, меньше было разработчиков под iOS – то сегодня же всё совсем не так. Уже каждая собака имеет блог на медиуме, есть множество обучающих видео (я уж не говорю о WWDC Sessions), и в принципе не такая проблема найти живого человека, чтобы учиться от него.
Как при всём этом изобилии контента получаются статьи вроде вот этой? Моя чаша переполнена, я раньше просто проходил мимо, теперь же я буду просто уничтожать такие статьи.
themen2
Можете объяснить чем делегаты принципиально отличаются от каллбеков?
Паттерн делегатов также активно используется в c#. Но в java каллбеки.
Мне казалось что по факту - это одно и то же;)
storoj
Делегаты от коллбеков отличаются принципиально.
Но если серьёзно, то мне было очень сложно ответить на этот вопрос, не получалось сформулировать не то что чёткую разницу, а даже строгие определения обоих терминов.
Существует такое определение для Delegation Pattern:
Я не был уверен, является ли передача "sender" первым аргументом неотъемлемым атрибутом делегирования, но, судя по всему, является.
Определение "коллбека" для меня было ещё более размытым. Можно подумать о коллбек-функциях и коллбек-замыканиях. Коллбек-функции (если это standalone функции), с одной стороны, не захватывают никакого контекста. С другой стороны, в коллбек-функцию всё равно всегда хочется передать какой-нибудь
void *context
, чтобы внутри функции можно было принимать какие-то решения в runtime.Другой реализацией коллбека (особенно в iOS разработке сегодняшнего дня) скорее будет Objective-C Block или Swift Closure. И мы, как правило, захватываем весь нужный нам контекст самостоятельно как сильную ссылку или копию примитива, но ничто не мешает передавать нужные данные в аргументах. Поэтому сигнатура может быть точно такой же как и у методов делегата, ровно как и семантика использования.
Если прочитать определение для "callback" на Википедии, то там говорится следующее:
В целом, я согласен с этим определением. Но тогда я для себя делаю вывод, что коллбек(и) в некоторых ситуациях являются частным случаем Delegate Pattern. Т.е. могут являться одним из способов реализации. В языках типа Си, где нет ООП, классов и интерфейсов/протоколов, примером реализации "интерфейса" делегата может быть структура с набором коллбеков.
Например:
Таким образом, я прихожу к выводу, что "коллбеком" можно назвать нечто (какой-то callable: функция или функтор), что ожидается быть вызванным в некоторый момент времени. И это может быть одной из реализаций паттерна Delegate. Т.е. для меня коллбеки и делегаты это понятия разного уровня, одно может быть частным случаем другого.
Напоследок, коллбек это одна функция, а под Delegate я склонен подразумевать некий интерфейс, т.е. набор функций, объединённых общей идеей. Но, как я показал выше на примере на Си, можно симулировать такую группировку даже в языках без встроенного ООП.
ForestLamp Автор
Спасибо за ваш комментарий.
Выбор в пользу скриншотов был сделан по причине того, что подсветка синтаксиса хорошо помогает понять семантику, когда ты новичок. Вставляя код блоками, подсветка отличается. Ссылка на GitHub с финальным проектом есть в конце статьи.
newtextForLabel и ViewControllerDelegate исправил, спасибо за внимательность.
Конечно, deinit() может не всегда отработать (дополнил этой сноской статью). Но в примере из статьи он отработает, меняя текст с задержкой добавляя наглядности.
house2008
Действительно забавно, что сам паттерн показан не совсем верно)