Всем доброго времени суток! В этой статье речь пойдет о паттерне Observer. Все, кто связан с iOS разработкой наверняка сталкивались с инструментами в основе работы которых лежит этот паттерн. Например, NotificationCenter, KVO или великий и могучий RxSwift, который сейчас очень популярен. В этой статье я на простом примере разберу принцип работы данного паттерна. Не ругайтесь, перфекционисты! Эта статья исключительно про паттерн Observer, поэтому для большей наглядности некоторыми принципами код стайла пришлось пренебречь.
Пример из жизни
Допустим, при просмотре Youtube вы наткнулись на интересный контент и захотели подписаться на этого автора. Вы нажимаете кнопку Subscribe и вуаля! Вы не пропустите ни одного нового видео от понравившегося вам автора.
Принцип работы паттерна точно такой же. Есть наблюдаемая сущность, и есть те, кто подписываются на ее изменения - подписчики. Причем подписчики мгновенно узнают об изменениях.
Перейдем от теории к практике.
Создаем новый проект.
Интерфейс выбираем Storyboard, писать будем на Swift.
Открываем файл Main.storyboard
Главный экран будет состоять из двух основных секций: "модуль блогера", с кнопкой при помощи которой он публикует контент и "модуль подписчика", где подписчик видит самое свежее видео от автора, а также может отписаться или подписаться на автора. Итого на сториборд помещаем 4xUILabel, 1XUIButton, 1xUISwitch. Черным цветом указаны UILabel, синим цветом UIButton.
Делаем шрифт пожирнее и устанавливаем констрейнты.
Протягиваем связи от всех элементов кроме заголовков. При зажатой клавише Option нажимаем на ViewController. Отрывается второе окно. При зажатой клавише перетаскиваем связи во ViewController. В итоге должно получиться как на рисунке ниже.
Со сторибордами все, осталось написать код.
Создаем протокол подписчика, это наш Observer.
protocol Subscriber : AnyObject {
func update(subject : Bloger )
}
Создаем класс Bloger. В нем указываем переменные, которые будут хранить последнюю актуальную информацию для наших подписчиков. В нашем случае это название последнего видео и его порядковый номер.
class Bloger {
var counter : Int = 0
var lastVideo = ""
}
Также, класс должен содержать список подписчиков. Но если мы просто создадим массив с объектами Subscriber, то получим retain cycle и, соответсвенно, утечку памяти. Чтобы этого не произошло, создадим простую структуру, которая будет содержать в себе объект типа Subscriber со слабой ссылкой:
//Fix retain cycle
struct WeakSubscriber {
weak var value : Subscriber?
}
class Bloger {
private lazy var subscribers : [WeakSubscriber] = [] // Создаем массив с подписчиками
}
Метод, который выполняет подписки на наблюдаемый объект.
func subscribe(_ subscriber: Subscriber) {
print("subscribed")
subscribers.append(WeakSubscriber(value: subscriber))
}
Метод, который служит для отмены подписки. Логика этого метода в том, что удаляется объект, содержащий такую же ссылку. Грубо говоря, идентификация объекта происходит по ссылке.
func unsubscribe(_ subscriber: Subscriber) {
subscribers.removeAll(where: { $0.value === subscriber })
print("unsubscribed")
}
Метод для рассылки уведомлений подписчикам:
func notify() {
subscribers.forEach { $0.value?.update(subject: self)
}
}
Ну и напоследок метод, который содержит в себе определенную бизнес логику. В нашем случае это публикация видео от автора и увеличение его номера на единицу с каждым последующим вызовом этого метода.
func releaseVideo() {
counter += 1
lastVideo = "video" + "\(counter)"
notify() //Notify subscribers
print("released!")
}
В итоге у нас получается:
//Fix retain cycle
struct WeakSubscriber {
weak var value : Subscriber?
}
class Bloger {
private lazy var subscribers : [WeakSubscriber] = [] // Создаем массив с подписчиками
var counter : Int = 0
var lastVideo = ""
func subscribe(_ subscriber: Subscriber) {
print("subscribed")
subscribers.append(WeakSubscriber(value: subscriber))
}
func unsubscribe(_ subscriber: Subscriber) {
subscribers.removeAll(where: { $0.value === subscriber })
print("unsubscribed")
}
func notify() {
subscribers.forEach { $0.value?.update(subject: self)
}
}
func releaseVideo() {
counter += 1
lastVideo = "video" + "\(counter)"
notify()
print("released!")
}
}
Осталось совсем чуть-чуть. Реализуем протокол Subscriber для класса ViewController.
В этом методе мы будем обновлять Label на экране, который показывает название последнего полученного видео от блогера.
func update(subject: Bloger) {
subscriberInfoLabel.text = subject.lastVideo
}
Создаем экземпляр класса Bloger() .
let bloger = Bloger()
В методе ViewDidLoad подписываемся на этот экземпляр.
bloger.subscribe(self)
Чтобы все заработало, осталось привязать методы для подписки и публикации к нашему интерфейсу. Для этого пропишем следующий код:
@IBAction func publishButton(_ sender: Any) {
bloger.releaseVideo()
}
@IBAction func subscribeToggle(_ sender: Any) {
if (sender as AnyObject).isOn {
bloger.subscribe(self)
} else {
bloger.unsubscribe(self)
}
}
В итоге у нас получается вот такой код:
import UIKit
class ViewController: UIViewController, Subscriber {
@IBOutlet weak var subscriberInfoLabel: UILabel!
var bloger = Bloger()
override func viewDidLoad() {
super.viewDidLoad()
bloger.subscribe(self)
}
@IBAction func publishButton(_ sender: Any) {
bloger.releaseVideo()
}
@IBAction func subscribeToggle(_ sender: Any) {
if (sender as AnyObject).isOn {
bloger.subscribe(self)
} else {
bloger.unsubscribe(self)
}
}
func update(subject: Bloger) {
subscriberInfoLabel.text = subject.lastVideo
}
}
//MARK:- Protocols
protocol Subscriber : UIViewController {
func update(subject : Bloger )
}
//Fix retain cycle
sruct WeakSubscriber {
weak var value : Subscriber?
}
class Bloger {
private lazy var subscribers : [WeakSubscriber] = [] // Массив с подписчиками
var counter : Int = 0
var lastVideo = ""
func subscribe(_ subscriber: Subscriber) {
print("subscribed")
subscribers.append(WeakSubscriber(value: subscriber))
}
func unsubscribe(_ subscriber: Subscriber) {
subscribers.removeAll(where: { $0.value === subscriber })
print("unsubscribed")
}
func notify() {
subscribers.forEach { $0.value?.update(subject: self)
}
}
func releaseVideo() {
counter += 1
lastVideo = "video" + "\(counter)"
notify()
print("released!")
}
}
Запускаем симулятор и проверяем, что вышло!
Надеюсь, теперь вы разобрались с основой работы паттерна Observer, и статья была вам полезна!
s_suhanov
Итак, у вью-контроллера есть сильная ссылка на блоггера:
А у блоггера есть массив с сильными ссылками на наблюдателей:
И в этот массив мы кладем ссылку на вью-контроллер.
Привет retain-cycle. )))
SurfSpot Автор
Все верно говоришь, уже поправил!
s_suhanov
Круто. Спасибо за диалог.
Если захочешь больше в паттерны нырнуть - пиши. С радостью помогу, если нужно будет. ?