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
DAloG
April 21, 2018
Programming
720
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
260
Redux + MQTT
dalog
1
840
От задач к проблемам
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
AWSに止められる覚悟してますか?
morizo_1984
2
530
Workers Cache を知る
syumai
0
360
Herb in Rails 8.2: Your ERB views, now HTML-aware @ Rails World 2026, Austin, Texas
marcoroth
0
220
複雑なUIコンポーネントとの ⻑い付き合い⽅:波及するバグからテスト戦略を経て、安全な再設計へ
mixplace
0
410
TechTown - Creating Composable Callables in Contemporary C++
rollbear
0
130
Simple Storage Service(S3) is not simple
iwatsukayura
0
290
機械に任せるテスト、人が見るテスト ⽣成AIで引き直した、運⽤保守フェーズの線引き
dske104
0
190
RAG の “R” を Swift で覗いてみる 〜「意味から探す」検索の仕組み〜
nao_randd
0
160
[2026-09-26]空論ジェネリックプロセス~テスト資産とAIで紡ぐ、再現可能なパフォーマンスチューニングの話~
tosite
0
270
気持ちぃ〜角丸 Squircle
ryu1013
0
310
The Rails Doctrine Decade
koic
2
590
Everything will be SERVERLESS — 信じて運用した10年の経験値 / Everything Will be Serverless — Lessons Learned from 10 Years of Operational Experience
seike460
PRO
1
1.1k
Featured
See All Featured
Marketing to machines
jonoalderson
1
5.9k
Refactoring Trust on Your Teams (GOTO; Chicago 2020)
rmw
35
3.8k
A designer walks into a library…
pauljervisheath
211
25k
Bootstrapping a Software Product
garrettdimon
PRO
306
120k
Efficient Content Optimization with Google Search Console & Apps Script
katarinadahlin
PRO
1
930
The Limits of Empathy - UXLibs8
cassininazir
2
720
Documentation Writing (for coders)
carmenintech
77
5.6k
SEO Brein meetup: CTRL+C is not how to scale international SEO
lindahogenes
2
2.9k
Embracing the Ebb and Flow
colly
88
5.2k
Discover your Explorer Soul
emna__ayadi
2
1.3k
技術選定の審美眼(2025年版) / Understanding the Spiral of Technologies 2025 edition
twada
PRO
120
120k
SEO in 2025: How to Prepare for the Future of Search
ipullrank
3
3.9k
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