Со времени 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 здесь . Наслаждайтесь!