Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Sign up for free
Menu
Search
Features
All features
Private URLs
Password Protection
Custom URLS
Scheduled publishing
Remove Branding
Restrict embedding
Deck Collections
Notes
Features
All features
Private URLs
Password Protection
Custom URLS
Scheduled publishing
Remove Branding
Restrict embedding
Deck Collections
Notes
Explore
Featured decks
Featured speakers
Programming
Technology
Storyboards
Explore
Featured decks
Featured speakers
Programming
Technology
Storyboards
Pricing
Search
Sign in
Sign up for free
Unlimited power of Data-Driven UI
Search
Sponsored
·
SiteGround - Reliable hosting with speed, security, and support you can count on.
→
DAloG
April 21, 2018
Programming
710
4
Share
Embed
Copy iframe code
Copy JS code
Copy link
Start on current slide
Unlimited power of Data-Driven UI
DAloG
April 21, 2018
More Decks by DAloG
See All by DAloG
State normalization (RU)
dalog
0
250
Redux + MQTT
dalog
1
830
От задач к проблемам
dalog
1
290
Data-Driven View Controllers. Tips and Tricks
dalog
5
2k
2 years of Redux in iOS. Lessons learned
dalog
0
430
Why unidirectional architecture matter for iOS.
dalog
1
340
Mobile backend without REST
dalog
2
160
Self managed teams 101
dalog
0
210
FMVP
dalog
1
210
Other Decks in Programming
See All in Programming
一人だけ、Kiroが静止する日
hideg
0
110
App Intentsのビルドプロセスを支える技術
kntkymt
0
380
wkhtmltopdfの次どうするか問題2026
willnet
2
1.2k
TiDB Cloudのカスタムコントローラーによるオートスケール対応
takaidohigasi
0
130
Building an Out-of-Order CPU
latte72
1
780
モデルのリファクタリングが難しいと思ったら、そもそも複雑だったのはビジネス仕様だった ? / is-the-business-domain-the-real-complexity
hatsu38
0
310
What We Talk About When We Talk About XP
m_seki
2
640
大喜利で理解するLLM as a Judge / Understanding LLM-as-a-Judge through Ogiri
rockname
0
120
海上で動くGoサーバー: goroutineとchannelでさばく航行データストリーム
atsuki_seo
0
720
AHC070解法紹介
eijirou
0
120
AI に Inclusive UI を書かせよう — Design Rules Skill で Compose UI を作り直す
theoriatec2024
1
490
Go × SIMDで高速化するベクトル検索 ~ルーフラインモデルでSIMDが効く境界を探れ! ~
po3rin
1
1.4k
Featured
See All Featured
Information Architects: The Missing Link in Design Systems
soysaucechin
1
1.1k
Done Done
chrislema
186
16k
Believing is Seeing
oripsolob
1
220
Site-Speed That Sticks
csswizardry
13
1.5k
Groundhog Day: Seeking Process in Gaming for Health
codingconduct
0
350
Building Adaptive Systems
keathley
44
3.2k
The B2B funnel & how to create a winning content strategy
katarinadahlin
PRO
1
520
The SEO identity crisis: Don't let AI make you average
varn
0
560
How to build a perfect <img>
jonoalderson
1
6k
How to audit for AI Accessibility on your Front & Back End
davetheseo
0
540
Reality Check: Gamification 10 Years Later
codingconduct
0
2.3k
Imperfection Machines: The Place of Print at Facebook
scottboms
270
14k
Transcript
Unlimited power of data-driven UI by @DAlooG // 2018 @
Mobius conf
О себе — инженер в Sigma Software — 7 лет
мобильной разработки — 100% запуск чужих продуктов by @DAlooG // 2018 @ Mobius conf 2
О чем поговорим? — про проблемы разработки UI — про
однонаправленный поток данных — про тестирование UI — про анимации by @DAlooG // 2018 @ Mobius conf 3
Проблемы разработки UI by @DAlooG // 2018 @ Mobius conf
4
Моё обычное приложение — 80% бюджета уйдет на UI (дизайн,
тестирование, разработка) — 100% приложений изменят свой UI после релиза — новый функционал всегда влияет на уже готовый UI — тестирование UI ограничено возможностями уже написанной системы by @DAlooG // 2018 @ Mobius conf 5
UI это итеративный процесс by @DAlooG // 2018 @ Mobius
conf 6
Нужно много дешевых итераций by @DAlooG // 2018 @ Mobius
conf 7
Идеальный код UI — дешево тестировать — дешево добавлять новый
функционал — дешево удалять функционал — дешево двигать функционал между экранами by @DAlooG // 2018 @ Mobius conf 8
Как снизить стоимость UI разработки? — быстрые итерации — масштабировать
по людям — не трогать то что работает by @DAlooG // 2018 @ Mobius conf 9
Почему этого трудно добиться в iOS? — системные зависимости в
UI слое — жизненный цикл UIKit — UI в конце цикла поставки — зависимость от реализации back-end by @DAlooG // 2018 @ Mobius conf 10
by @DAlooG // 2018 @ Mobius conf 11
Попытка №1: MVVM + FRP — выносим логику из VC
— держим ссылку на VM в VC — слушаем изменения VM by @DAlooG // 2018 @ Mobius conf 12
by @DAlooG // 2018 @ Mobius conf 13
Проблемы MVVM — зависимость от порядка изменения полей — накопление
ошибки переходных состояний — реактивная сложность — жизненный цикл UI by @DAlooG // 2018 @ Mobius conf 14
Попытка №2: Presenter — внешний компонент у View — посылает
команды внутрь View — принимает команды из View — знает про жизненный цикл View by @DAlooG // 2018 @ Mobius conf 15
by @DAlooG // 2018 @ Mobius conf 16
Попытка №2: шаблон Presenter protocol View { func enableLoginAction() func
disableLoginAction() func beginUserLoading() ... } by @DAlooG // 2018 @ Mobius conf 17
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
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
Недостатки Presenter: — зависимость от порядка операция — зависимость от
жизненного цикла — иногда зависимость от предыдущих операций — внутреннее состояние ожидания ввода by @DAlooG // 2018 @ Mobius conf 20
Однонаправленный поток данных by @DAlooG // 2018 @ Mobius conf
21
Попытка №3: Props — внутренний компонент у View — полностью
изолирован — просто данные — не несет контекста рендеринга by @DAlooG // 2018 @ Mobius conf 22
by @DAlooG // 2018 @ Mobius conf 23
class ViewController { struct Props { let useraname: Field let
password: Field let login: LoginAction } render(props: Props) { ... } } by @DAlooG // 2018 @ Mobius conf 24
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
Props это внутренний компонент — меняется только вместе с UI
— дублирование кода — не использует доменный словарь — рендерится только одним View by @DAlooG // 2018 @ Mobius conf 26
Props не связан с другими слоями — способен противостоять огромным
изменениям — позволяет итерировать дизайн без логики — не требует вложений в проектирование by @DAlooG // 2018 @ Mobius conf 27
Props не обладает состоянием — семантика значения а не ссылки
— неизменяемые значения — композиция — поддерживает enum by @DAlooG // 2018 @ Mobius conf 28
View это рендеринг потока Props by @DAlooG // 2018 @
Mobius conf 29
Обратная связь: Command by @DAlooG // 2018 @ Mobius conf
30
Что такое Command? final class CommandWith<T> { private let action:
(T) -> () func perform(with value: T) { action(value) } } by @DAlooG // 2018 @ Mobius conf 31
Command это — замыкание 2.0 — гарантирует однонаправленность — помогает
в отладке by @DAlooG // 2018 @ Mobius conf 32
by @DAlooG // 2018 @ Mobius conf 33
by @DAlooG // 2018 @ Mobius conf 34
Хорошие Props это — гарантия отсутствия невозможных состояний — краткое
описание возможностей View — формат для хранения и передачи представления — однозначное описание UI by @DAlooG // 2018 @ Mobius conf 35
Как хорошо рендерить Props? func render(props: Props) { self.lablel.text =
props.title ... } by @DAlooG // 2018 @ Mobius conf 36
Как хорошо рендерить Props? func render(props: Props) { self.props =
props self.tableView.reloadData() } by @DAlooG // 2018 @ Mobius conf 37
Как хорошо рендерить Props? func render(props: Props) { self.props =
props self.view.setNeedsLayout() } by @DAlooG // 2018 @ Mobius conf 38
Как выбрать ячейку в таблице? struct ListViewController.Props { let items:
[Item] struct Item { let name: String let select: Command } } by @DAlooG // 2018 @ Mobius conf 39
Про тестирование UI by @DAlooG // 2018 @ Mobius conf
40
Как писать 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
Использование в реальном2 проекте: 2 https://github.com/aol-public/OneMobileSDK-controls-ios/tree/master/SnapshotTests by @DAlooG // 2018
@ Mobius conf 42
UI storybooks — очень быстрые итерации — исследовательское тестирование —
проверка граничных условий верстки by @DAlooG // 2018 @ Mobius conf 43
by @DAlooG // 2018 @ Mobius conf 44
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
by @DAlooG // 2018 @ Mobius conf 46
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
Про анимацию Props by @DAlooG // 2018 @ Mobius conf
48
by @DAlooG // 2018 @ Mobius conf 49
by @DAlooG // 2018 @ Mobius conf 50
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
Как анимировать позицию? 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
Как анимировать прозрачность? addAnimation(view: shadowView, keyPath: "opacity") { self.shadowView.isHidden =
true } shadowView.alpha = 0 by @DAlooG // 2018 @ Mobius conf 53
Как применять 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
А теперь всё вместе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
Сложные Props struct A { let b: enum B {
case c(enum C { case d(struct D { ... by @DAlooG // 2018 @ Mobius conf 56
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
Призмы!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
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
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
Вопросы? — Twitter: @DAlooG — Email:
[email protected]
by @DAlooG //
2018 @ Mobius conf 61