Эта статья для уровня trainee, а значит для совсем начинающих великолепных разработчиков

Основная цель статьи - рассказать просто, на примере, как можно использовать паттерн делегирования в Swift.

Статья состоит из двух частей. Первая - удалим из проекта Storyboard и напишем кодом простой интерфейс. Вторая - разберем, как с помощью делегата передать данные на предыдущий контроллер.

Часть 1

Создаем новый проект, назовем его DelegatePattern:

Галочки не понадобятся, Interface - Storyboard, Language - Swift.
Галочки не понадобятся, Interface - Storyboard, Language - Swift.

Размещать элементы будем кодом, поэтому удаляем Storyboard из проекта:

Удаляем Main.storyboard
Удаляем Main.storyboard
Выбираем Move to Trash - удалить в корзину
Выбираем Move to Trash - удалить в корзину

Выбираем проект (стрелка 1), вкладка General и в разделе Deployment Info выделяем и удаляем Main(стрелка 2)

Поле Main Interface должно остаться пустым
Поле Main Interface должно остаться пустым

Переходим в файл info.plist (стрелка 1) и удаляем строку Application Scene Manifest

В итоге останется только строка Information Property List
В итоге останется только строка Information Property List

У нас не будет поддержки 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 контроллера:

view.backgroundColor = .purple
view.backgroundColor = .purple

Запускаем проект, и если все сделали верно, запустится симулятор с фиолетовым контроллером:

Отлично, теперь мы можем продолжать писать интерфейс в коде. Для начала создадим еще один ViewController. Нажимаем Command + N и выбираем Cocoa Touch Class:

Назовем его SecondViewController:

После создания удалите весь закоментированый код из SecondViewController.
После создания удалите весь закоментированый код из SecondViewController.

Поработаем с ViewController, добавим на него кнопку перехода на SecondViewController:

Строки 12 - 17
Строки 12 - 17

Объявим новый метод 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.

Проверим симулятор:

Кнопка есть, все отлично.
Кнопка есть, все отлично.

Теперь аналогично добавим UILabel, в нем в последующем и будем менять текст при возвращении из следующего контроллера.

Можете сами потренироваться и создать лейбл. Если возникнут проблемы, подглядите здесь :)

Первым делом добавим сам элемент UILabel:

строка 21 - 26. Сразу зададим текст лейбла "Standart text"
строка 21 - 26. Сразу зададим текст лейбла "Standart text"

Добавляем лейбл на 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():

Строка 18
Строка 18

Объявим методtoSecondVCButtonPressed(), который будет отрабатывать по нажатию на кнопку:

Перейдем в SecondViewController и в методе viewDidLoad() добавим цвет бэкграунда view второго контроллера:

Строка 15.
Строка 15.

Проверяем, что получилось, собираем проект. Если все сделано верно, по нажатию на кнопку Go to second VC, откроется второй контроллер с серым фоном.

Часть 2

Пора приступать к передаче данных при закрытии "серого" контроллера. Как вы видели в названии статьи, будем использовать делегат :)

Объявим протокол SecondViewControllerDelegate в файле класса ViewController и обязательно укажем тип AnyObject. Это нужно для того, чтобы протокол работал с классами, а это, в свою очередь, позволит создавать слабые ссылки и избежать retain cycle между контроллерами. Наш протокол будет содержать только один метод - для замены текста в лейбле ViewController'a.

Реализовывать метод протокола будет ViewController, подпишем его под протокол в extension'е и напишем логику для метода, которая будет менять текст:

В аргумент text, находящийся на 68 строке, придет новый текст с другого контроллера, а на 69 строке мы заменим стандартный текст лейбла.

Если навести курсор на аргумент text, Xcode подсветит какой text к какому относится.

Переходим в SecondViewController. Помните мы создали протокол и объявили его anyObject? Теперь пора создать слабую ссылку, которая будет иметь тип делегата, она будет жить в SecondViewController и через нее мы сможем добраться до методов делегата.

Строка 12
Строка 12

Когда мы закрываем SecondViewController, смахивая его вниз, срабатывает метод deinit. В нашем примере это отличное место, чтобы передать новый текст в лейбл ViewController'a. Добираемся через переменную delegate до метода newTextForLabel и передаем в него новый текст для лейбла на первом контроллере "New text". (*PS: метод deinit() в статье используется только для примера, поскольку мы точно уверены в том, что контроллер выгрузится из памяти.)

Строка 20 - 22
Строка 20 - 22

Как пример, если создать кнопку закрытия второго экрана, тогда self.delegate?.newTextForLabel(text: "New text") поселился бы в методе, срабатывающем по нажатию на кнопку закрытия.

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

Возвращаемся в ViewController, и там, где мы создавали для кнопки метод перехода на SecondViewController подпишемся под делегата:

Строка 62
Строка 62

Собираем проект и проверяем:

PS: Почему текст обновляется с задержкой? Дело в том, что deinit выгружает контроллер из памяти, поэтому проходит какое-то количество времени, пока контроллер выгрузится и текст сменится. Если вы будете использовать делегат, например, в методе UIViewController'a - dismiss() или в вашем методе кнопки, то никаких задержек не будет.

GitHub с финальным проектом - ссылка.

Спасибо, что дочитали :)

Комментарии (10)


  1. storoj
    02.12.2022 20:52
    +1

    Я не понимаю людей, которые плюсуют такие статьи. Это просто шок контент.


    1. storoj
      02.12.2022 20:53
      +1

      Мало того, что половина статьи вообще не имеет ни малейшего отношения к делегатам, так и сам делегат сделан неправильно!


      1. storoj
        02.12.2022 21:24
        +5

        Меня неимоверно огорчают статьи на тему iOS. На мой взгляд, в 99% сталкиваешься с просто вопиющей неграмотностью, которую я могу объяснить лишь полнейшим непониманием пределов собственного познания. Что ещё хуже обычной некомпетентности.

        • Код картинками. Ну это ладно;

        • Слишком большой фокус на удалении сториборда. Чёрт бы с ним, статья вообще не об этом, пусть бы был сториборд. Но это тоже ладно;

        • Не критика, но если уж хочется сделать тестовый экран с какой-то кнопкой, я всегда делаю rightBarButtonItem с одним из системных "стилей". Не нужно делать никакого лейаута, никаких пропертей, не нужно даже заголовок придумывать;

        • Всё началось с newtextForLabel. Как это вообще? Я понимаю, что это "мелочи", но мне это уже о многом говорит. Почему "text" не с большой буквы, хотя всё остальное в camelCase?;

        • ViewControllerDelegate. Это тоже для меня признак полнейшего непонимания темы. По-моему, по всем признакам это должно было называться SecondViewControllerDelegate;

        • "Когда мы закрываем SecondViewController, смахивая его вниз, срабатывает метод deinit. В нашем примере это отличное место, чтобы передать новый текст в лейбл." – я многое видел, но такого я ещё не видел. Это как минимум просто неверное допущение – надеяться на то, что при "закрытии" UIViewController он обязательно будет деаллоцирован;

        • метод делегата мало того, что имеет нелепый нейминг (не сообщает о случившемся событии, не спрашивает никаких данных), так и не имеет первым аргументом объекта, пославшего событие. В Delegates and Data Sources есть пример того, как стоит оформлять подобные методы (раздел "The Form of Delegation Messages");

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


        1. storoj
          02.12.2022 21:30
          +2

          ... и если 10 лет назад не было толком никаких хороших ресурсов, мало где было что-то почитать, меньше было разработчиков под iOS – то сегодня же всё совсем не так. Уже каждая собака имеет блог на медиуме, есть множество обучающих видео (я уж не говорю о WWDC Sessions), и в принципе не такая проблема найти живого человека, чтобы учиться от него.

          Как при всём этом изобилии контента получаются статьи вроде вот этой? Моя чаша переполнена, я раньше просто проходил мимо, теперь же я буду просто уничтожать такие статьи.


        1. themen2
          03.12.2022 12:11

          Можете объяснить чем делегаты принципиально отличаются от каллбеков?

          Паттерн делегатов также активно используется в c#. Но в java каллбеки.

          Мне казалось что по факту - это одно и то же;)


          1. storoj
            04.12.2022 21:43

            Делегаты от коллбеков отличаются принципиально.

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

            Существует такое определение для Delegation Pattern:

            In delegation, an object handles a request by delegating to a second object (the delegate). The delegate is a helper object, but with the original context. With language-level support for delegation, this is done implicitly by having self in the delegate refer to the original (sending) object, not the delegate (receiving object).

            Я не был уверен, является ли передача "sender" первым аргументом неотъемлемым атрибутом делегирования, но, судя по всему, является.

            Определение "коллбека" для меня было ещё более размытым. Можно подумать о коллбек-функциях и коллбек-замыканиях. Коллбек-функции (если это standalone функции), с одной стороны, не захватывают никакого контекста. С другой стороны, в коллбек-функцию всё равно всегда хочется передать какой-нибудь void *context, чтобы внутри функции можно было принимать какие-то решения в runtime.

            Другой реализацией коллбека (особенно в iOS разработке сегодняшнего дня) скорее будет Objective-C Block или Swift Closure. И мы, как правило, захватываем весь нужный нам контекст самостоятельно как сильную ссылку или копию примитива, но ничто не мешает передавать нужные данные в аргументах. Поэтому сигнатура может быть точно такой же как и у методов делегата, ровно как и семантика использования.

            Если прочитать определение для "callback" на Википедии, то там говорится следующее:

            a callback or callback function is any reference to executable code that is passed as an argument to another piece of code; that code is expected to call back (execute) the callback function as part of its job. This execution may be immediate as in a synchronous callback, or it might happen at a later point in time as in an asynchronous callback.

            В целом, я согласен с этим определением. Но тогда я для себя делаю вывод, что коллбек(и) в некоторых ситуациях являются частным случаем Delegate Pattern. Т.е. могут являться одним из способов реализации. В языках типа Си, где нет ООП, классов и интерфейсов/протоколов, примером реализации "интерфейса" делегата может быть структура с набором коллбеков.

            Например:

            // Objective-C
            @protocol MyDelegate;
             
            @interface Object: NSObject
            @property (weak) id<MyDelegate> delegate;
            @end
            
            @protocol MyDelegate
            -(void)myObject:(id)object didCompleteWithData:(id)data;
            -(BOOL)shouldMyObjectDoTheWork(id)myObject;
            @end
            
            // Си
            struct MyDelegate;
            struct Object {
              MyDelegate *delegate;
            };
            struct MyDelegate {
              void (*didCompleteWithData)(Object *, void *data);
              BOOL (*shouldMyObjectDoTheWork)(Object *);
            };
            

            Таким образом, я прихожу к выводу, что "коллбеком" можно назвать нечто (какой-то callable: функция или функтор), что ожидается быть вызванным в некоторый момент времени. И это может быть одной из реализаций паттерна Delegate. Т.е. для меня коллбеки и делегаты это понятия разного уровня, одно может быть частным случаем другого.

            Напоследок, коллбек это одна функция, а под Delegate я склонен подразумевать некий интерфейс, т.е. набор функций, объединённых общей идеей. Но, как я показал выше на примере на Си, можно симулировать такую группировку даже в языках без встроенного ООП.


        1. ForestLamp Автор
          05.12.2022 11:12

          Спасибо за ваш комментарий.

          • Выбор в пользу скриншотов был сделан по причине того, что подсветка синтаксиса хорошо помогает понять семантику, когда ты новичок. Вставляя код блоками, подсветка отличается. Ссылка на GitHub с финальным проектом есть в конце статьи.

          • newtextForLabel и ViewControllerDelegate исправил, спасибо за внимательность.

          • Конечно, deinit() может не всегда отработать (дополнил этой сноской статью). Но в примере из статьи он отработает, меняя текст с задержкой добавляя наглядности.


      1. house2008
        02.12.2022 21:50

        Действительно забавно, что сам паттерн показан не совсем верно)


  1. RileyUsagi
    05.12.2022 11:20

    Создавать проект со сторибордами, чтобы затем удалять их. Любопытный подход.

    Может быть проще было сразу создать современный проект без всего этого?


    1. ForestLamp Автор
      05.12.2022 11:31

      Интерфейс очень легкий, а одно из ключевых мест - создание кнопки с таргетом. Создав его самостоятельно, думаю, что человек более детально сможет разобраться и подобраться к месту подписки на делегата. Обычно этот момент вызывает много вопросов.