Share
Embed
Data-Driven View Controllers. Tips and Tricks
Huge pile of goodness related to data-driven UI on iOS
DAloG
November 25, 2017
Transcript
46 slides
-
What makes good view controller? → predictable and stable behavior
→ easy to test and reproduce issues → minimum amount of business code → sync interface by @DAlooG // 2017 @ GDG Dev Fest -
What makes good abstractions? → fully incapsulated context → operation
order agnostic → decoupled from rest of the system by @DAlooG // 2017 @ GDG Dev Fest -
Usual view controller class ViewController: UIViewController { var database =
DatabaseWrapper.shared var network = NetworkAPI.shared ... } by @DAlooG // 2017 @ GDG Dev Fest -
What is wrong with this? → user action are not
visible, nobody can react. → change in core components can break everything. → zero reuse between parts/projects. by @DAlooG // 2017 @ GDG Dev Fest -
View controller with Props class ViewController: UIViewController { struct Props
{ ... } func render(props: Props) { ... } } by @DAlooG // 2017 @ GDG Dev Fest -
What is good with this? → code locality is great
→ change in app is not reflected in VC → all inputs and outputs is visible by @DAlooG // 2017 @ GDG Dev Fest -
Can we generalize this? protocol DataDriven { associatedtype Props func
render(props: Props) } by @DAlooG // 2017 @ GDG Dev Fest -
Should we? Probably, no. Exact shape is not so relevant.
Ideas matter more that implementation details by @DAlooG // 2017 @ GDG Dev Fest -
How we can handle view actions? → action closure →
Command pattern → dispatch closure by @DAlooG // 2017 @ GDG Dev Fest -
Action closure Simplest possible idea struct Props { struct Item
{ ... } let data: [Item] let reload: () -> () let select: (Item) -> () } by @DAlooG // 2017 @ GDG Dev Fest -
Command pattern struct Command<T> { init(file: StaticString = #file, line:
Int = #line, action: (T) -> ()) { ... } func perform(with value: T) { action(value) } } by @DAlooG // 2017 @ GDG Dev Fest -
Command usage struct Props { struct Item { ... }
let data: [Item] let reload: Command<Void> let select: Command<Item> } by @DAlooG // 2017 @ GDG Dev Fest -
Dispatch closure class ViewController: UIViewController { struct Props { ...
} enum Action { case reload, select(Item) } var props: Props var dispatch: (Action) -> () } by @DAlooG // 2017 @ GDG Dev Fest -
How to design nice Props? → eliminate impossible states with
types → avoid any sort of identifiers → use flatten props by @DAlooG // 2017 @ GDG Dev Fest -
How to eliminate impossible states? struct Props { let isLoading:
Bool let login: Command? let cancel Command? let setUsername: Command<String>? let setPassword: Command<String>? } by @DAlooG // 2017 @ GDG Dev Fest -
How to eliminate impossible states? enum Props { case input(setUsername:
Command<String>, setPassword: Command<String>, login: Command) case loading(cancel: Command) } by @DAlooG // 2017 @ GDG Dev Fest -
How to avoid identifiers? struct Props { struct Item {
let name: String let id: String } let data: [Item] let select: Command<Item> by @DAlooG // 2017 @ GDG Dev Fest -
How to avoid identifiers? struct Props { struct Item {
let name: String let select: Command } let data: [Item] } by @DAlooG // 2017 @ GDG Dev Fest -
How to render complex props? → use enums for modeling
value exclusivity → use nested types → use phantom types by @DAlooG // 2017 @ GDG Dev Fest -
enum Props { case input(Input); struct Input { let username,
password: Field; struct Field { let value: String let update: Command<String> } let status: Status; enum Status { case invalid, valid(login: Command) } } case loading(cancel: Command) case cancelling } by @DAlooG // 2017 @ GDG Dev Fest -
enum UIKitProps { let loginVisible: Bool let loginText: String let
loginUpdate: Command<String>? ... init(from props: Props) { ... } } by @DAlooG // 2017 @ GDG Dev Fest -
class ViewController: UIViewController { var props: Props { didSet {
uiProps = UIKitProps(from: props) guard isViewLoaded else { return } view.setNeedsLayout() }} by @DAlooG // 2017 @ GDG Dev Fest -
class ViewController: UIViewController { override viewWillLayoutSubviews() { login.isHidden = uiProps.loginIsHidden
... } @IBAction func login() { uiProps.loginAction?.perform() } } by @DAlooG // 2017 @ GDG Dev Fest -
How data-driven controllers can be tested? → snapshot tests →
interactive storybooks → record/replay by @DAlooG // 2017 @ GDG Dev Fest -
How to setup a snapshot tests? class ViewControllerTests: FBSnapshotTestCase {
func testLoading() { ... } } by @DAlooG // 2017 @ GDG Dev Fest -
let vc = ViewController() let window = SnapshotWindow.iPhoneX.landscape vc.props =
.loading window.rootViewController = vc window.makeKeyAndVisible() FBSnapshotVerifyView(window) by @DAlooG // 2017 @ GDG Dev Fest -
Pros of snapshot testing → strong regression tracking → easy
to review UI related Pull Requests → UI can be fully developed without rest of an app by @DAlooG // 2017 @ GDG Dev Fest -
Cons of snapshot testing → a lot of images →
single bug can break half of images → bad noise/signal ratio → code required for change by @DAlooG // 2017 @ GDG Dev Fest -
How to setup interactive storybook? → develop UI in a
separate framework → run playground → setup props → use LiveView API by @DAlooG // 2017 @ GDG Dev Fest -
//UI.playground import PlaygroundSupport @testable import AppUI let vc = ViewController()
let window = SnapshotWindow.iPhoneX.landscape vc.props = .loading window.rootViewController = vc window.makeKeyAndVisible() PlaygroundPage.current.liveView = window by @DAlooG // 2017 @ GDG Dev Fest -
Interactive storybook 2.0 → web based UI for editing props
→ Props should be Codable → WebSockets for syncing between client and host. by @DAlooG // 2017 @ GDG Dev Fest -
How to record and replay props? → attach Recorder to
view controller. → save history by some trigger → send it back to developers → replay by @DAlooG // 2017 @ GDG Dev Fest -
struct Recorder<Props: Codable>: Codable { struct Event: Codable { let
date: Date, let props: Props } var history: [Event] func record(props: Props, date: Date = Date()) { history.append(Event(date: date, props: props)) } func replay(with render: @escaping (Props) -> ()) { ... } by @DAlooG // 2017 @ GDG Dev Fest -
Additional Unit testing → converting from Props to UIKitProps →
invariants of Props by @DAlooG // 2017 @ GDG Dev Fest -
How to connect one view controller with another? → using
storyboard (UI driven) → using some coordinator, pattern (Context driven) by @DAlooG // 2017 @ GDG Dev Fest -
Storyboard driven transition struct Props { let data: [Item]; struct
Item { let name: String let select: Command<DetailsViewController> } } by @DAlooG // 2017 @ GDG Dev Fest -
When we should use ui driven transition? → several view
controllers forms single use case. → we like to keep UI code in the UI :) by @DAlooG // 2017 @ GDG Dev Fest -
Coordinator driven transition struct Props { let data: [Item]; struct
Item { let name: String let select: Command } } by @DAlooG // 2017 @ GDG Dev Fest -
What about composition of view controllers? class MasterViewController: UIViewController {
struct Props { let itemListProps: ItemListViewController.Props let selectedItemProps: DetailsViewController.Props? } } by @DAlooG // 2017 @ GDG Dev Fest -
What about animations? class ViewController: UIViewController { func render(props: Props,
from oldProps: Props, animated: Bool) { ... } } by @DAlooG // 2017 @ GDG Dev Fest -
Some hints for animations: → use new property animator API.
→ use view controller transitioning delegates by @DAlooG // 2017 @ GDG Dev Fest -
Where I can learn more? → our production code: https://github.com/aol-
public/OneMobileSDK-controls-ios → my mail: [email protected] → my twitter: @DAlooG → ask questions now by @DAlooG // 2017 @ GDG Dev Fest