Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Unlimited power of Data-Driven UI

DAloG
April 21, 2018

Unlimited power of Data-Driven UI

DAloG

April 21, 2018
Tweet

More Decks by DAloG

Other Decks in Programming

Transcript

  1. О себе — инженер в Sigma Software — 7 лет

    мобильной разработки — 100% запуск чужих продуктов by @DAlooG // 2018 @ Mobius conf 2
  2. О чем поговорим? — про проблемы разработки UI — про

    однонаправленный поток данных — про тестирование UI — про анимации by @DAlooG // 2018 @ Mobius conf 3
  3. Моё обычное приложение — 80% бюджета уйдет на UI (дизайн,

    тестирование, разработка) — 100% приложений изменят свой UI после релиза — новый функционал всегда влияет на уже готовый UI — тестирование UI ограничено возможностями уже написанной системы by @DAlooG // 2018 @ Mobius conf 5
  4. Идеальный код UI — дешево тестировать — дешево добавлять новый

    функционал — дешево удалять функционал — дешево двигать функционал между экранами by @DAlooG // 2018 @ Mobius conf 8
  5. Как снизить стоимость UI разработки? — быстрые итерации — масштабировать

    по людям — не трогать то что работает by @DAlooG // 2018 @ Mobius conf 9
  6. Почему этого трудно добиться в iOS? — системные зависимости в

    UI слое — жизненный цикл UIKit — UI в конце цикла поставки — зависимость от реализации back-end by @DAlooG // 2018 @ Mobius conf 10
  7. Попытка №1: MVVM + FRP — выносим логику из VC

    — держим ссылку на VM в VC — слушаем изменения VM by @DAlooG // 2018 @ Mobius conf 12
  8. Проблемы MVVM — зависимость от порядка изменения полей — накопление

    ошибки переходных состояний — реактивная сложность — жизненный цикл UI by @DAlooG // 2018 @ Mobius conf 14
  9. Попытка №2: Presenter — внешний компонент у View — посылает

    команды внутрь View — принимает команды из View — знает про жизненный цикл View by @DAlooG // 2018 @ Mobius conf 15
  10. Попытка №2: шаблон Presenter protocol View { func enableLoginAction() func

    disableLoginAction() func beginUserLoading() ... } by @DAlooG // 2018 @ Mobius conf 17
  11. class Presenter { let view: View let network: NetworkService func

    login() { view.disableLoginAction() view.beginUserLoading() network.login().onComplete { view.enableLoginAction() view.endUserLoading() } } } by @DAlooG // 2018 @ Mobius conf 18
  12. class Presenter { let view: View let network: NetworkService func

    login() { view.disableLoginAction() view.beginUserLoading() network.login().onComplete { view.enableLoginAction() view.endUserLoading() } } } by @DAlooG // 2018 @ Mobius conf 19
  13. Недостатки Presenter: — зависимость от порядка операция — зависимость от

    жизненного цикла — иногда зависимость от предыдущих операций — внутреннее состояние ожидания ввода by @DAlooG // 2018 @ Mobius conf 20
  14. Попытка №3: Props — внутренний компонент у View — полностью

    изолирован — просто данные — не несет контекста рендеринга by @DAlooG // 2018 @ Mobius conf 22
  15. class ViewController { struct Props { let useraname: Field let

    password: Field let login: LoginAction } render(props: Props) { ... } } by @DAlooG // 2018 @ Mobius conf 24
  16. struct ViewController.Props.Field { let value: String let update: CommandWith<String> }

    enum ViewController.Props.LoginAction { case possible(Command) case inProgress case disabled } by @DAlooG // 2018 @ Mobius conf 25
  17. Props это внутренний компонент — меняется только вместе с UI

    — дублирование кода — не использует доменный словарь — рендерится только одним View by @DAlooG // 2018 @ Mobius conf 26
  18. Props не связан с другими слоями — способен противостоять огромным

    изменениям — позволяет итерировать дизайн без логики — не требует вложений в проектирование by @DAlooG // 2018 @ Mobius conf 27
  19. Props не обладает состоянием — семантика значения а не ссылки

    — неизменяемые значения — композиция — поддерживает enum by @DAlooG // 2018 @ Mobius conf 28
  20. Что такое Command? final class CommandWith<T> { private let action:

    (T) -> () func perform(with value: T) { action(value) } } by @DAlooG // 2018 @ Mobius conf 31
  21. Хорошие Props это — гарантия отсутствия невозможных состояний — краткое

    описание возможностей View — формат для хранения и передачи представления — однозначное описание UI by @DAlooG // 2018 @ Mobius conf 35
  22. Как хорошо рендерить Props? func render(props: Props) { self.props =

    props self.tableView.reloadData() } by @DAlooG // 2018 @ Mobius conf 37
  23. Как хорошо рендерить Props? func render(props: Props) { self.props =

    props self.view.setNeedsLayout() } by @DAlooG // 2018 @ Mobius conf 38
  24. Как выбрать ячейку в таблице? struct ListViewController.Props { let items:

    [Item] struct Item { let name: String let select: Command } } by @DAlooG // 2018 @ Mobius conf 39
  25. Как писать snapshot1 тесты? func testSomeState() { let props =

    .init(...) controller.render(props: props) verify(controller, for: Device.iPhoneX.portrait) verify(controller, for: Device.iPhoneX.landscapeLeft) verify(controller, for: Device.iPadPro9.portrait.oneThird) } 1 https://github.com/AndriiDoroshko/SnappyShrimp by @DAlooG // 2018 @ Mobius conf 41
  26. UI storybooks — очень быстрые итерации — исследовательское тестирование —

    проверка граничных условий верстки by @DAlooG // 2018 @ Mobius conf 43
  27. import Moscapsule let mqttClient: MQTTClient = { let mqttConfig =

    MQTTConfig( clientId: "iOS Application", host: "127.0.0.1", port: 1883, keepAlive: 60) return MQTT.newConnection(mqttConfig) }() if let data = try? JSONEncoder().encode(self.props) { mqttClient.publish(data, topic: "CardListViewController") } by @DAlooG // 2018 @ Mobius conf 45
  28. mqttClient.subscribe("CardListViewController/setProps", qos: 0) mqttCommands.insert(CommandWith { message in guard message.topic ==

    "CardListViewController/setProps" else { return } guard let payload = message.payload else { return } do { self.props = try JSONDecoder().decode(Props.self, from: payload) } catch { print(error) } }.dispatched(on: .main)) by @DAlooG // 2018 @ Mobius conf 47
  29. func addAnimation(view: UIView, keyPath: String, onComplete: @escaping () -> ())

    { let animation = CABasicAnimation(keyPath: keyPath) animation.duration = animationsDuration animation.delegate = AnimationDelegate(didStop: { _, completed in guard completed else { return } onComplete() }) view.layer.add(animation, forKey: keyPath) } by @DAlooG // 2018 @ Mobius conf 51
  30. Как анимировать позицию? addAnimation(view: sideBarView, keyPath: "position") { self.sideBarView.isHidden =

    true } sideBarBottomConstraint.constant = { guard #available(iOS 11, *) else { return view.frame.height - sideBarView.frame.height } return view.frame.height - sideBarView.frame.height - view.safeAreaInsets.top }() sideBarVisibleConstraint.isActive = false sideBarInvisibleConstraint.isActive = true sideBarBottomConstraint.isActive = true by @DAlooG // 2018 @ Mobius conf 52
  31. Как применять Props во время анимации func renderShadowView() { switch

    (currentUIProps.controlsViewHidden, nextUIProps.controlsViewHidden) { case (false, true): hideShadowView() case (true, false): showShadowView() default: guard shadowView.layer.animationKeys() == nil else { return } shadowView.isHidden = nextUIProps.controlsViewHidden } } by @DAlooG // 2018 @ Mobius conf 54
  32. А теперь всё вместе3 if nextUIProps.bottomItemsHidden { addAnimation(view: seekerView, keyPath:

    "position") { self.seekerView.isHidden = true self.seekerView.alpha = 0 setupSeekerView() } seekerToSafeAreaConstraint.isActive = false bottomItemsSeekerConstraint.isActive = true bottomItemsVisibleConstraint.isActive = false bottomItemsInvisibleConstraint.isActive = false bottomItemsAndSeekerAnimatedConstraint.isActive = true } else { addAnimation(view: seekerView, keyPath: "opacity", onComplete: setupSeekerView) if currentUIProps.bottomItemsHidden { addAnimation(view: seekerView, keyPath: "position", onComplete: setupSeekerView) } bottomItemsAndSeekerAnimatedConstraint.isActive = false seekerToSafeAreaConstraint.isActive = false bottomItemsSeekerConstraint.isActive = true seekerView.alpha = 0 } 3 https://github.com/aol-public/OneMobileSDK-controls-ios/blob/master/PlayerControls/ sources/DefaultControlsViewController.swift by @DAlooG // 2018 @ Mobius conf 55
  33. Сложные Props struct A { let b: enum B {

    case c(enum C { case d(struct D { ... by @DAlooG // 2018 @ Mobius conf 56
  34. Enum и вложенные значения enum ViewController.Props.LoginAction { case possible(Command) case

    inProgress case disabled } func render(props: Props.LoginAction) { self.loginButton.isEnabled = ??? self.loadingIndicator.isHidden = ??? } by @DAlooG // 2018 @ Mobius conf 57
  35. Призмы!4 extension Props.LoginAction { var possible: Command? { guard case

    let .possible(command) = self else { return nil } return command } var isInProgress: Bool { guard case .inProgress = self else { return false } return true } var isDisabled: Bool { guard case .disabled = self else { return false } return true } 4 https://medium.com/flawless-app-stories/enums-and-sourcery-5da57cda473b by @DAlooG // 2018 @ Mobius conf 58
  36. Enum и вложенные значения enum ViewController.Props.LoginAction { case possible(Command) case

    inProgress case disabled } func render(props: Props.LoginAction) { self.loginButton.isEnabled = ??? self.loadingIndicator.isHidden = ??? } by @DAlooG // 2018 @ Mobius conf 59
  37. Enum и вложенные значения enum ViewController.Props.LoginAction { case possible(Command) case

    inProgress case disabled } func render(props: Props.LoginAction) { self.loginButton.isEnabled = props.possible != nil self.loadingIndicator.isHidden = props.isInProgress == false } by @DAlooG // 2018 @ Mobius conf 60