Статьи

Диспетчеризация событий в Swift с расширениями протокола

Со времени WWDC много писали о протоколно-ориентированном программировании в Swift от таких блоггеров, как  SketchyTechДэвид Оуэнс  и  Рэй Вендерлих , и я подумал, что пора заняться этим самому.

После майской работы с  диспетчеризацией событий в ActionScript  расширения протоколов казались идеальной техникой для реализации подобного шаблона в Swift. Действительно, расширения протокола обеспечивают немедленное преимущество, заключающееся в том, что я могу добавить диспетчеризацию событий для любого типа объекта без необходимости расширения базового класса этим объектом. Например, компоненты пользовательского интерфейса могут не только отправлять события, но и объекты-значения и структуры данных могут: идеально подходит для  шаблона MVVM, где представление может реагировать на события в модели представления для обновления. 

Мой проект,  Protocol Extension Event Dispatcher , содержит демонстрационное приложение, содержащее несколько компонентов пользовательского интерфейса: слайдер, степпер, метку и кнопку. Существует одна «модель»: целое число, которое отправляет событие изменения, когда его значение изменяется с помощью этих компонентов. Конечный результат — когда пользователь взаимодействует с любым компонентом, весь пользовательский интерфейс обновляется через события, чтобы отразить изменение. 

Это не полная реализация диспетчеризации событий в Swift, а скорее демонстрация того, что возможно в Swift с протоколно-ориентированным программированием. Для более полной версии взгляните на  ActionSwift

Давайте посмотрим, как работает мой код. Прежде всего, у меня есть протокол EventDispatcher, который определяет несколько методов. Это протокол класса, потому что мы хотим, чтобы диспетчер был единственным ссылочным объектом:

    protocol EventDispatcher: class

    {

        func addEventListener(type: EventType, handler: EventHandler)



        func removeEventListener(type: EventType, handler: EventHandler)



        func dispatchEvent(event: Event)

    }

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

Первый камень преткновения заключается в том, что расширения могут не содержать сохраненные свойства. Есть несколько вариантов решения этой проблемы: я мог бы создать глобальное хранилище или использовать objc_getAssociatedObject и objc_setAssociatedObject. Все эти функции позволяют мне присоединять прослушиватели событий к каждому экземпляру EventDispatcher с простым синтаксисом. Код для моей реализации по умолчанию addEventListener выглядит так:


extension EventDispatcher

{

    func addEventListener(type: EventType, handler: EventHandler)

    {

        var eventListeners: EventListeners



        if let el = objc_getAssociatedObject(self, &EventDispatcherKey.eventDispatcher) as? EventListeners

        {

            eventListeners = el



            if let _ = eventListeners.listeners[type]

            {

                eventListeners.listeners[type]?.insert(handler)

            }

            else

            {

                eventListeners.listeners[type] = Set<EventHandler>([handler])

            }

        }

        else

        {

            eventListeners = EventListeners()

            eventListeners.listeners[type] = Set<EventHandler>([handler])

        }



        objc_setAssociatedObject(self,

            &EventDispatcherKey.eventDispatcher,

            eventListeners,

            objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN)

    }



}

Для данного типа и обработчика событий я проверяю, существует ли объект EventListeners, если таковой имеется, я проверяю, есть ли у этого объекта запись для типа, и соответственно создаю или обновляю значения. Как только у меня появится современный объект EventListeners, я могу записать его обратно с помощью objc_setAssociatedObject.

Аналогичным образом для dispatchEvent () я запрашиваю связанный объект, проверяю обработчики на тип события и выполняю их, если они есть:


extension EventDispatcher

{

    func dispatchEvent(event: Event)

    {

        guard let eventListeners = objc_getAssociatedObject(self, &EventDispatcherKey.eventDispatcher) as?EventListeners,

            handlers = eventListeners.listeners[event.type]

            else

        {

            // no handler for this object / event type

            return

        }



        for handler in handlers

        {

            handler.function(event)

        }

    }



}

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


class DispatchingValue: EventDispatcher

{

    required init(_ value: T)

    {

        self.value = value

    }



    var value: T

    {

        didSet

        {

            dispatchEvent(Event(type: EventType.change, target: self))

        }

    }



}

Мое демонстрационное приложение использует DispatchingValue для переноса целого числа:

    let dispatchingValue = DispatchingValue(25)

… который обновляет элементы управления пользовательского интерфейса при его изменении, добавляя прослушиватель событий:


    let dispatchingValueChangeHandler = EventHandler(function: {

        (event: Event) in

        self.label.text = "\(self.dispatchingValue.value)"

        self.slider.value = Float(self.dispatchingValue.value)

        self.stepper.value = Double(self.dispatchingValue.value)

        })





    dispatchingValue.addEventListener(.change, handler: dispatchingValueChangeHandler)

Я также создал расширение для UIControl, которое заставляет все элементы управления пользовательского интерфейса соответствовать EventDispatcher и отправлять изменения и события касания:


extension UIControl: EventDispatcher

{

    override public func didMoveToSuperview()

    {

        super.didMoveToSuperview()



        addTarget(self, action: "changeHandler", forControlEvents: UIControlEvents.ValueChanged)

        addTarget(self, action: "tapHandler", forControlEvents: UIControlEvents.TouchDown)

    }



    override public func removeFromSuperview()

    {

        super.removeFromSuperview()



        removeTarget(self, action: "changeHandler", forControlEvents: UIControlEvents.ValueChanged)

        removeTarget(self, action: "tapHandler", forControlEvents: UIControlEvents.TouchDown)

    }



    func changeHandler()

    {

        dispatchEvent(Event(type: EventType.change, target: self))

    }



    func tapHandler()

    {

        dispatchEvent(Event(type: EventType.tap, target: self))

    }



}

Итак, мой слайдер, например, может обновлять dispatchingValue, когда пользователь меняет свое значение:


    let sliderChangeHandler = EventHandler(function: {

        (event: Event) in

        self.dispatchingValue.value = Int(self.slider.value)

    })



    slider.addEventListener(.change, handler: sliderChangeHandler)

… который в свою очередь вызовет dispatchingValueChangeHandler и обновит другие компоненты пользовательского интерфейса. Моя кнопка сброса устанавливает значение dispatchingValue равным нулю при нажатии:


    let buttonTapHandler = EventHandler(function: {

        (event: Event) in

        self.dispatchingValue.value = 0

    })





    resetButton.addEventListener(.tap, handler: buttonTapHandler)

Я надеюсь, что этот пост даст представление о невероятной силе, предлагаемой протоколно-ориентированным программированием. Еще раз, мой проект доступен в  моем репозитории GitHub здесь . Наслаждайтесь!