Привязки В Swift. Делаем Первые Шаги На Пути К Mvvm

Добрый день.

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

при разработке iOS-приложений.

Для тех, кто заинтересован, пожалуйста, обратитесь к кат. Предыстория с большим количеством текстов Я не могу назвать себя опытным iOS-разработчиком.

Хотя с миром iOS я познакомился несколько лет назад, желание совершенствоваться в разработке приложений для iOS появилось у меня совсем недавно.

Мой путь был тернист. Obj-C меня не сразу впечатлил; Мне хотелось разрабатывать приложения на чем-то знакомом.

Поэтому использовались PhoneGap, Apcelerator Titanium и всё.

Но, естественно, из этих попыток ничего не вышло.

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

Я ничего не выдумывал и не облегчал себе жизнь — выполнял работу исключительно в ObjC, без использования сторонних фреймворков.

И это было для меня болью.

Простые вещи оказались сложными, с авторазметкой я не справился, код невозможно было посмотреть.

Поэтому в следующем проекте использовался Xamarin Forms. Поработав над проектом около двух месяцев, стало понятно, что эта технология еще далека от совершенства (в итоге выяснилось, что проект находится в статусе бета-версии, но об этом редко упоминалось).

Но работая с Xamarin Forms, я проникся многими паттернами, которыми пронизан этот проект, более того, мне пришлось сделать кучу кастомных компонентов, что привело к более четкому пониманию того, как работает UIKit. В тот момент, когда стало понятно, что наш проект надо переписать на нативный, Swift стремительно приближался к релизу.

Я прочитал книгу на этом языке и почувствовал, что могу начать все сначала.

Но первый опыт всё равно напомнил мне самого себя, поэтому я начал копать в сторону MVVM в iOS. Мне очень понравилась эта концепция.

На тот момент все статьи, которые попадались мне на глаза, предлагали решить проблему с помощью ReactiveCocoa. Глядя на примеры кода этой библиотеки, я понял, что мне еще учиться и учиться, потому что.

я ничего не понял из увиденного.

Для Swift также предлагалось использовать ReactiveCocoa. На самом деле статья Колин Эберхардт был моей отправной точкой.

Но вскоре мне пришлось столкнуться с тем, что описанный выше автором подход привел к утечкам памяти.

Видимо я что-то делал не так и тогда не понял что именно.

Плюс ReactiveCocoa остался для меня черным ящиком.

От этой библиотеки было решено избавиться, учитывая, что она использовалась только для связи моделей представлений с представлениями.

Я наткнулся на проект Наблюдаемый Свифт , что решило проблему привязки.

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



Постановка задачи

На данный момент я не представляю, как легко внедрить MVVM в UIKit. Имеется в виду тот самый MVVM, который я видел в Xamarin Forms и который меня так впечатлил.

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

Мы пойдем по пути наименьшего сопротивления: будем использовать то, что дает нам Apple. Но при этом мы будем стремиться к более декларативному описанию пользовательского интерфейса.

Первое и главное, что меня привлекло в MVVM, — это динамическая привязка ViewModel и View. Это позволяет нам описывать бизнес-логику отдельно от презентации.

Мы уже привыкли описывать логику во ViewController. И это настоящий ад. Давайте стремиться минимизировать код во ViewController. Во-первых, нам нужно научиться понимать, что состояние нашей ViewModel изменилось и это изменение должно быть отражено в пользовательском интерфейсе.

Apple предлагает нам использовать, например, КВО.

ReactiveCocoa облегчит эту задачу.

Но у нас есть Свифт. И мы хотим сделать наше решение максимально простым и понятным.

Вот как наши коллеги предлагают решить эту проблему:

Кстати, не забываем про предстоящий релиз Реактивное какао 3.0 .

А пока библиотека Связь наиболее подходит для нашей задачи.

Пока я работал над тем, что покажу ниже, Связь Это только начиналось и не соответствовало моим требованиям.

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

Мне хотелось максимально упростить все.

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

Связь .



Динамический

Начнем с малого и, в то же время, самого главного.

Нам нужно уметь узнавать об изменениях состояния переменной и как-то реагировать на эти изменения.

Напомню, что мы стремимся к простоте и лаконичности.

И в этом случае Свифт предстает во всей красе.

Он дает нам дженерики, лямбды с удивительным синтаксисом и наблюдаемыми свойствами.

Итак, давайте что-нибудь из этого придумаем.

  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
   

class Dynamic<T> { init(_ v: T) { value = v } var value: T { didSet { println(value) } } }

Теперь у нас есть возможность отслеживать изменение стоимости ценить .

На практике это будет выглядеть примерно так:

let dynamicInt: Dynamic<Int> = Dynamic(0) println(dynamicInt.value) dynamicInt.value = 1 dynamicInt.value = 17

Давайте добавим поддержку прослушивателя для нашей изменяемой сущности.

Слушателем будет анонимная функция, в аргумент которой мы передадим новое значение.

ценить .



class Dynamic<T> { typealias Listener = T -> () private var listeners: [Listener] = [] init(_ v: T) { value = v } var value: T { didSet { for l in listeners { l(value) } } } func bind(l: Listener) { listeners.append(l) l(value) } func addListener(l: Listener) { listeners.append(l) } }

Метод добавить прослушиватель просто добавляет обработчик в свой список прослушивателей, а метод связывать делает то же самое, но сразу вызывает добавленный прослушиватель и передает ему текущее значение ценить .



let dynText: Dynamic<String> = Dynamic("") dynText.bind { someLabel.text = $0 } dynText.addListener { otherLabel.text = $0 } dynText.value = "New text"

Используя дженерики, нам не нужно проверять или приводить типы данных.

Компилятор сделает это за нас.

Например, в следующем случае код не скомпилируется:

let dynInt: Dynamic<Int> = Dynamic(0) dynInt.bind { someLabel.text = $0 }

Компилятор знает, что аргумент нашего слушателя имеет тип Int и мы не можем присвоить значение этого аргумента полю текст объект класса UILabel , поскольку тип этого поля Нить .

Более того, благодаря упрощенному синтаксису анонимных функций мы смогли добавлять слушателей без лишней записи.

Но нет предела совершенству.

Мы можем определить пару операторов или перегрузить существующие, чтобы еще больше сократить код.

func >> <T>(left: Dynamic<T>, right: T -> Void) { return left.addListener(right) } infix operator >>> {} func >>> <T>(left: Dynamic<T>, right: T -> Void) { left.bind(right) }



let dynText: Dynamic<String> = Dynamic("") dynText >>> { someLabel.text = $0 } dynText >> { otherLabel.text = $0 } dynText.value = "New text"

Мысли о бесхозных, слабых, Пустоте и Пустоте? На практике описанные выше примеры приведут к утечкам памяти.

Вот пример:

class MyViewController: UIViewController { @IBOutlet weak var label: UILabel! let viewModel = MyViewModel() override func viewDidLoad() { viewModel.someText >>> { self.label.text = $0 } super.viewDidLoad() } }

Очевидно, теперь функция слушателя и себя тесно связаны друг с другом и объектом класса Мойвиевконтроллер Никогда не оставляйте.

Чтобы этого не произошло, нужно ослабить соединение:

viewModel.someText >>> { [unowned self] in self.label.text = $0 }

Так-то лучше.

Но есть одна вещь.

Нет гарантии, что функция прослушивателя не будет вызвана после удаления объекта.

Мойвиевконтроллер .

Чтобы защитить себя, мы используем слабый :

viewModel.someText >>> { [weak self] in self?.

label.text = $0 }

Но в этом случае код не будет скомпилирован, потому что наш слушатель имеет тип Строка -> Пустота? , но должен иметь тип Строка -> Пустота для успешной компиляции.

Вот почему я изначально добавил Динамический два типа слушателей: с возвращаемыми значениями Void и Void?.

Соответственно перегружены методы связывать И добавить прослушиватель для поддержки двух типов слушателей.

Но вскоре выяснилось, что компилятор не сможет определить, какой метод вызывать, если сделать, например, так:

viewModel.someText >>> { [weak self] in if self != nil { self!.

label.text = $0 } }

Поэтому нам пришлось отказаться от идеи поддержки двух типов слушателей и использовать аналогичные приёмы:

viewModel.someText >>> { [weak self] in self?.

label.text = $0; return } viewModel.someText >>> { [weak self] in self?.

label.text = $0; () } viewModel.someText >>> { [weak self] v in v; self?.

label.text = v }

Конечно, можно было отказаться от использования слабый в пользу передачи динамическому объекту помимо функции-обработчика ещё и ссылки на объект и не вызова функции, если объект вдруг окажется удалённым.

Именно такой подход используется в библиотеке Связь .

Но это был не мой путь :)

Упрощение вашего опыта с UIKit

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

viewModel.someText >>> label

Нет ничего невозможного.

В конце концов, мы легко можем прийти к такому синтаксису.

Идея реализации снова была любезно позаимствована у Связь .

Идея проста: мы будем хранить поле в каком-то объекте, у которого есть прослушиватель, и можем привязать этот прослушиватель к динамическому объекту.



final class PropertyModifier<T> { typealias Modifier = (T) -> () let modifier: Modifier init (_ l: Modifier) { self.modifier = l } }

Объект класса Модификатор свойства будет создано самим представлением, а в конструктор будет передана лямбда с кодом, изменяющим значение определенного поля представления.



private var UILabelPropertyKeyTextModifier: UInt8 = 0 extension UILabel { var textModifier: PropertyModifier<String?> { if let pm: AnyObject = objc_getAssociatedObject(self, &UILabelPropertyKeyTextModifier) { return pm as PropertyModifier<String?> } else { let pm = PropertyModifier<String?> { [weak self] in self?.

text = v; () } objc_setAssociatedObject(self, &UILabelPropertyKeyTextModifier, pm, objc_AssociationPolicy(OBJC_ASSOCIATION_RETAIN_NONATOMIC)) return pm } } }

Замечу, что в расширение мы не можем описывать хранимые поля, поэтому на помощь приходят ObjC Runtime и функции objc_setAssociatedObject , objc_getAssociatedObject .

Теперь мы можем сделать это:

viewModel.someText >>> label.textModifier.modifier

Давайте упростим:

func >> <T>(left: Dynamic<T>, right: PropertyModifier<T>) { left.addListener(right.modifier) } func >>> <T>(left: Dynamic<T>, right: PropertyModifier<T>) { left.bind(right.modifier) } viewModel.someText >>> label.textModifier

Намного лучше.

Но это еще не все.

Мы можем выбрать любое из наиболее часто используемых свойств представления и назначить его.

Модификатор свойства по умолчанию.



protocol BindableObject { typealias DefaultPropertyModifierTargetType var defaulPropertytModifier: PropertyModifier<DefaultPropertyModifierTargetType> { get } } extension UILabel: BindableObject { typealias DefaultPropertyModifierTargetType = String? var defaulPropertytModifier: PropertyModifier<DefaultPropertyModifierTargetType> { return textModifier } } func >> <T, B: BindableObject where B.DefaultPropertyModifierTargetType == T>(left: Dynamic<T>, right: B) { left.addListener(right.defaulPropertytModifier.modifier) } func >>> <T, B: BindableObject where B.DefaultPropertyModifierTargetType == T>(left: Dynamic<T>, right: B) { left.bind(right.defaulPropertytModifier.modifier) }

Вот и все.

UILabel получил стандарт Модификатор свойства , что изменяет значения полей текст .

И мы достигли намеченной цели, а именно можем создать соединение следующим образом:

viewModel.someText >>> label



Команды

Одна из примечательных концепций Xamarin Forms, которая мне понравилась, — это команды.

Фактически мы можем описать команду с помощью двух функций: одна возвращает истинный или ЛОЖЬ , указывающий, что команду можно выполнить, а второй — действие, которое выполняет команда.

Допустим, у нас есть кнопка ( UIButton ).

У кнопки есть поле включено , кнопка может быть нажата пользователем, после чего должно произойти какое-то действие.

Помните, что мы стремимся к декларативному описанию поведения интерфейса? Итак, давайте распространим эту идею на наши элементы управления.



final class Command<T> { typealias CommandType = (value: T, sender: AnyObject?) -> () weak var enabled: Dynamic<Bool>? private let command: CommandType init (enabled: Dynamic<Bool>, command: CommandType) { self.enabled = enabled self.command = command } init (command: CommandType) { self.command = command } func execute(value: T) { execute(value, sender: nil) } func execute(value: T, sender: AnyObject?) { var enabled = true if let en = self.enabled?.

value { enabled = en } if enabled { command(value: value, sender: sender) } } } protocol Commander { typealias CommandType func setCommand(command: Command<CommandType>) } func >> <T, B: Commander where B.CommandType == T>(left: B, right: Command<T>) { left.setCommand(right) } private var UIButtonPropertyKeyCommand: UInt8 = 0 extension UIButton: Commander { typealias CommandType = () func setCommand(command: Command<CommandType>) { if let c: AnyObject = objc_getAssociatedObject(self, &UIButtonPropertyKeyCommand) { fatalError("Multiple assigment to command") return } objc_setAssociatedObject(self, &UIButtonPropertyKeyCommand, command, objc_AssociationPolicy(OBJC_ASSOCIATION_ASSIGN)) command.enabled?.

bind { [weak self] in self?.

enabled = $0; () } addTarget(self, action: Selector("buttonTapped:"), forControlEvents: .

TouchUpInside) } func buttonTapped(sender: AnyObject?) { if let c: Command<CommandType> = objc_getAssociatedObject(self, &UIButtonPropertyKeyCommand) as? Command<CommandType> { c.execute((), sender: sender) } } }

Итак, у нас есть команда, у которой есть поле включено и функция, которая должна выполняться при вызове метода выполнять .

Нам нужно связать нашу команду с кнопкой.

Для этого был создан протокол Командир с методом setCommand .

Мы реализуем наш протокол для UIButton путем привязки поля динамической команды включено с соответствующим свойством UIButton .

Мы также перегрузили оператор > > для комфорта.

Что мы получаем в результате:

class PageModel { let nextPageEnabled: Dynamic<Bool> = Dynamic(true) lazy var openNextPage: Command<()> = Command ( enabled: self.nextPageEnabled, command: { [weak self] value, sender in //Open next page }) } class MyViewController: UIViewController { @IBOutlet weak var nextButton: UIButton! let pageModel = PageModel() override func viewDidLoad() { nextButton >> pageModel.openNextPage super.viewDidLoad() } }



Заключение

Теперь в нашем распоряжении есть динамические объекты, которые мы можем связать с чем угодно.

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

И этого уже достаточно, чтобы упростить нашу UIViewController .

Остался за кадром карта И фильтр Для Динамический , двунаправленные привязки и упрощенная работа с УИТаблевиев .

Но вы можете посмотреть на это сами.

Проект, демонстрирующий возможности описанного подхода, доступен по адресу GitHub .

Рекомендую взглянуть на это.

Пара примеров для начала

class TwoWayBindingPage: Page { typealias PMT = TwoWayBindingPageModel @IBOutlet weak var switchLabel: UILabel! @IBOutlet weak var switchControl: UISwitch! @IBOutlet weak var switchButton: UIButton! @IBOutlet weak var textFieldLabel: UILabel! @IBOutlet weak var textField: UITextField! @IBOutlet weak var textFieldButton: UIButton! @IBOutlet weak var sliderLabel: UILabel! @IBOutlet weak var slider: UISlider! @IBOutlet weak var sliderButton: UIButton! override func bindPageModel() { super.bindPageModel() let pm = pageModel as PMT switchButton >> pm.changeSomethingEnabled textFieldButton >> pm.changeUserName sliderButton >> pm.changeAccuracy pm.somethingEnabled | { "Current dynamic value: \($0)" } >>> switchLabel pm.userName | { "Current dynamic value: \($0)" } >>> textFieldLabel pm.accuracy | { "Current dynamic value: \($0)" } >>> sliderLabel pm.somethingEnabled <<>>> switchControl pm.userName <<>>> textField pm.accuracy <<>>> slider } } class BeerListPage: Page { typealias PMT = BeerListPageModel @IBOutlet weak var tableView: UITableView! private var tableViewHelper: SimpleTableViewHelper! override func bindPageModel() { super.bindPageModel() let pm = pageModel as PMT tableViewHelper = SimpleTableViewHelper(tableView: tableView, data: pm.beerList, cellType: BeerTableCell.self, command: pm.openBeerPage) tableView.pullToRefreshControl >> pm tableView.infiniteScrollControl >> pm } }

Спасибо за внимание.

Комментарии, предложения и критика приветствуются.

Теги: #Swift #разработка для iOS #mvvm #bindings #Разработка для iOS #Swift

Вместе с данным постом часто просматривают: