Эта статья для уровня trainee, а значит для совсем начинающих великолепных разработчиков
Основная цель статьи - рассказать просто, на примере, как можно использовать паттерн делегирования в Swift
.
Статья состоит из двух частей. Первая - удалим из проекта Storyboard
и напишем кодом простой интерфейс. Вторая - разберем, как с помощью делегата передать данные на предыдущий контроллер.
Часть 1
Создаем новый проект, назовем его DelegatePattern:
![Галочки не понадобятся, Interface - Storyboard, Language - Swift. Галочки не понадобятся, Interface - Storyboard, Language - Swift.](https://habrastorage.org/getpro/habr/upload_files/85e/44b/504/85e44b5047c82dfe07c7fe15f81cd813.png)
Размещать элементы будем кодом, поэтому удаляем Storyboard
из проекта:
![Удаляем Main.storyboard Удаляем Main.storyboard](https://habrastorage.org/getpro/habr/upload_files/eaa/af3/fd8/eaaaf3fd83df4faba16eb7986ab72752.png)
![Выбираем Move to Trash - удалить в корзину Выбираем Move to Trash - удалить в корзину](https://habrastorage.org/getpro/habr/upload_files/944/1b2/91f/9441b291f8b458e6829175d9a93156ae.png)
Выбираем проект (стрелка 1), вкладка General
и в разделе Deployment Info
выделяем и удаляем Main
(стрелка 2)
![Поле Main Interface должно остаться пустым Поле Main Interface должно остаться пустым](https://habrastorage.org/getpro/habr/upload_files/3e4/058/c6f/3e4058c6fa10960aa82b1ca0d6939f83.png)
Переходим в файл info.plist
(стрелка 1) и удаляем строку Application Scene Manifest
![В итоге останется только строка Information Property List В итоге останется только строка Information Property List](https://habrastorage.org/getpro/habr/upload_files/966/87b/e7a/96687be7aa57b9801cc07918ba70d84c.png)
У нас не будет поддержки IPad
- файл SceneDelegate
можно тоже удалить:
![Вот так теперь выглядит проект Вот так теперь выглядит проект](https://habrastorage.org/getpro/habr/upload_files/7c1/b6e/273/7c1b6e2735e06eac2fc74d205f44cccc.png)
Так как мы удалили 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
выглядит вот так:
![](https://habrastorage.org/getpro/habr/upload_files/551/ead/2c6/551ead2c670f823083d10ed561bf9d69.png)
Переходим во ViewController
, в методе viewDidLoad
добавляем фиолетовый цвет для бэкграунда view
контроллера:
![view.backgroundColor = .purple view.backgroundColor = .purple](https://habrastorage.org/getpro/habr/upload_files/188/e85/f7a/188e85f7a25e707587bff509101d4185.png)
Запускаем проект, и если все сделали верно, запустится симулятор с фиолетовым контроллером:
![](https://habrastorage.org/getpro/habr/upload_files/7e8/3bf/037/7e83bf037ebf6637714b3380d759ebde.png)
Отлично, теперь мы можем продолжать писать интерфейс в коде. Для начала создадим еще один ViewController
. Нажимаем Command + N
и выбираем Cocoa Touch Class
:
![](https://habrastorage.org/getpro/habr/upload_files/cdc/f34/242/cdcf342427613e6e92edda02002cf9c6.png)
Назовем его SecondViewController:
![После создания удалите весь закоментированый код из SecondViewController. После создания удалите весь закоментированый код из SecondViewController.](https://habrastorage.org/getpro/habr/upload_files/fdb/684/66d/fdb68466d3ef63864987bae366bd894b.png)
Поработаем с ViewController
, добавим на него кнопку перехода на SecondViewController:
![Строки 12 - 17 Строки 12 - 17](https://habrastorage.org/getpro/habr/upload_files/f9f/9c9/2ff/f9f9c92ff664fb60daee0a9900233956.png)
Объявим новый метод 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
:
![Что бы все заработало не забудьте добавить вызов makeConstraints() во viewDidLoad(). Строка 27. Что бы все заработало не забудьте добавить вызов makeConstraints() во viewDidLoad(). Строка 27.](https://habrastorage.org/getpro/habr/upload_files/964/c3a/aeb/964c3aaeb11ca107a4657a3d1b4c0e40.png)
Проверим симулятор:
![Кнопка есть, все отлично. Кнопка есть, все отлично.](https://habrastorage.org/getpro/habr/upload_files/957/c26/09e/957c2609e03fe73d6987d6852a2ea7f4.png)
Теперь аналогично добавим UILabel, в нем в последующем и будем менять текст при возвращении из следующего контроллера.
Можете сами потренироваться и создать лейбл. Если возникнут проблемы, подглядите здесь :)
Первым делом добавим сам элемент UILabel:
![строка 21 - 26. Сразу зададим текст лейбла "Standart text" строка 21 - 26. Сразу зададим текст лейбла "Standart text"](https://habrastorage.org/getpro/habr/upload_files/805/8f4/281/8058f4281a7737e40b5e3e3663e5e413.png)
Добавляем лейбл на 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
есть кнопка и лейбл. Симулятор выглядят так:
![](https://habrastorage.org/getpro/habr/upload_files/b39/711/c4e/b39711c4e481f21c825cc059d53891c1.png)
Чтобы кнопка заработала и при ее нажатии открывался следующий экран, добавим ей свойство button.addTarget():
![Строка 18 Строка 18](https://habrastorage.org/getpro/habr/upload_files/e8b/4a7/ea2/e8b4a7ea2c97a00d7f795bb57612cf3e.png)
Объявим методtoSecondVCButtonPressed()
, который будет отрабатывать по нажатию на кнопку:
![](https://habrastorage.org/getpro/habr/upload_files/7cc/de6/a3a/7ccde6a3ab54324cef8f9cff6e944e77.png)
Перейдем в SecondViewController
и в методе viewDidLoad()
добавим цвет бэкграунда view
второго контроллера:
![Строка 15. Строка 15.](https://habrastorage.org/getpro/habr/upload_files/6d9/73f/bd9/6d973fbd9bb890124b722a25d80a7c2c.png)
Проверяем, что получилось, собираем проект. Если все сделано верно, по нажатию на кнопку Go to second VC
, откроется второй контроллер с серым фоном.
![](https://habrastorage.org/getpro/habr/upload_files/dc7/f82/8ee/dc7f828ee0b1166de59be60b59f46dec.gif)
Часть 2
Пора приступать к передаче данных при закрытии "серого" контроллера. Как вы видели в названии статьи, будем использовать делегат :)
Объявим протокол SecondViewControllerDelegate
в файле класса ViewController
и обязательно укажем тип AnyObject
. Это нужно для того, чтобы протокол работал с классами, а это, в свою очередь, позволит создавать слабые ссылки и избежать retain cycle между контроллерами. Наш протокол будет содержать только один метод - для замены текста в лейбле ViewController
'a.
![](https://habrastorage.org/getpro/habr/upload_files/fa0/b72/ced/fa0b72ced6f5518b4d917efafc20cfc4.png)
Реализовывать метод протокола будет ViewController
, подпишем его под протокол в extension
'е и напишем логику для метода, которая будет менять текст:
![](https://habrastorage.org/getpro/habr/upload_files/b23/cbe/e35/b23cbee3565f6005490c71a78a7fd13c.png)
В аргумент text
, находящийся на 68 строке, придет новый текст с другого контроллера, а на 69 строке мы заменим стандартный текст лейбла.
Если навести курсор на аргумент text
, Xcode
подсветит какой text
к какому относится.
![](https://habrastorage.org/getpro/habr/upload_files/717/1ae/edb/7171aeedb6acdaa3f823023e5d39dc6f.png)
Переходим в SecondViewController
. Помните мы создали протокол и объявили его anyObject
? Теперь пора создать слабую ссылку, которая будет иметь тип делегата, она будет жить в SecondViewController
и через нее мы сможем добраться до методов делегата.
![Строка 12 Строка 12](https://habrastorage.org/getpro/habr/upload_files/028/594/8e7/0285948e731b7a2d3ba488332e5e1fcd.png)
Когда мы закрываем SecondViewController
, смахивая его вниз, срабатывает метод deinit
. В нашем примере это отличное место, чтобы передать новый текст в лейбл ViewController
'a. Добираемся через переменную delegate
до метода newTextForLabel
и передаем в него новый текст для лейбла на первом контроллере "New text".
(*PS: метод deinit() в статье используется только для примера, поскольку мы точно уверены в том, что контроллер выгрузится из памяти.)
![Строка 20 - 22 Строка 20 - 22](https://habrastorage.org/getpro/habr/upload_files/8b1/e45/def/8b1e45def89ab7293e9cafa4a05b395e.png)
Как пример, если создать кнопку закрытия второго экрана, тогда self.delegate?.newTextForLabel(text: "New text")
поселился бы в методе, срабатывающем по нажатию на кнопку закрытия.
Все почти готово, осталось дело за малой деталью, о которой лично я всегда забываю :) нужно сообщить нашему SecondViewController
, кто будет его делегатом (то есть кто будет что-то делать с теми данными которые, он отправил).
Возвращаемся в ViewController,
и там, где мы создавали для кнопки метод перехода на SecondViewController
подпишемся под делегата:
![Строка 62 Строка 62](https://habrastorage.org/getpro/habr/upload_files/bff/f51/cf1/bfff51cf142821b4bb16bd578b25c9a2.png)
Собираем проект и проверяем:
![](https://habrastorage.org/getpro/habr/upload_files/6a5/750/13e/6a575013ed1e691da9c755605d5dd6b7.gif)
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
Действительно забавно, что сам паттерн показан не совсем верно)