SlideShare a Scribd company logo
1 of 64
Download to read offline
SwifTEA UI
kevin o’neillemail: kevin@oneill.id.au twitter: @kevinoneill
kevin o’neillemail: kevin@oneill.id.au twitter: @kevinoneill
About Me
kevin o’neillemail: kevin@oneill.id.au twitter: @kevinoneill
Swift, Go, Typescript, Python
I write software
kevin o’neillemail: kevin@oneill.id.au twitter: @kevinoneill
I play music
kevin o’neillemail: kevin@oneill.id.au twitter: @kevinoneill
I ride mountain bikes*
SwifTEA UI
kevin o’neillemail: kevin@oneill.id.au twitter: @kevinoneill
kevin o’neillemail: kevin@oneill.id.au twitter: @kevinoneill
The Elm Architecture (TEA)
kevin o’neillemail: kevin@oneill.id.au twitter: @kevinoneill
(Model, Message) -> (Model, Command)
kevin o’neillemail: kevin@oneill.id.au twitter: @kevinoneill
Model
kevin o’neillemail: kevin@oneill.id.au twitter: @kevinoneill
Message
kevin o’neillemail: kevin@oneill.id.au twitter: @kevinoneill
Command
kevin o’neillemail: kevin@oneill.id.au twitter: @kevinoneill
TEA vs FLUX (Redux)
kevin o’neillemail: kevin@oneill.id.au twitter: @kevinoneill
(Model, Message) -> Model
kevin o’neillemail: kevin@oneill.id.au twitter: @kevinoneill
Lacks Side Effect Handling
kevin o’neillemail: kevin@oneill.id.au twitter: @kevinoneill
SwifTEA UI
kevin o’neillemail: kevin@oneill.id.au twitter: @kevinoneill
Quick Demo
kevin o’neillemail: kevin@oneill.id.au twitter: @kevinoneill
WeeDux
kevin o’neillemail: kevin@oneill.id.au twitter: @kevinoneill
(Model, Message) -> (Model*, Command)
kevin o’neillemail: kevin@oneill.id.au twitter: @kevinoneill
struct ApplicationModel: Codable {
enum CodingKeys: String, CodingKey {
case categories
case quotes
}
struct Cache {
typealias Images = [String: Resource<UIImage>]
var images: Images = [:]
var log: [ApplicationMessage] = []
}
typealias Categories = Resource<[QuoteCategory]>
typealias Quotes = [String: Resource<Quote>]
var categories: Categories = .placeholder
var quotes: Quotes = [:]
var cache: Cache = Cache()
}
Model
kevin o’neillemail: kevin@oneill.id.au twitter: @kevinoneill
enum QuoteMessage: ApplicationMessage {
case categoriesLoading
case categoriesLoadingFailed(error: QuoteServiceError)
case categoriesLoaded(categories: [QuoteCategory])
case quoteLoading(category: String)
case quoteLoadingFailed(category: String, error: QuoteServiceError)
case quoteLoaded(category: String, quote: Quote)
}
Quote Message
kevin o’neillemail: kevin@oneill.id.au twitter: @kevinoneill
private let categoriesLoading = ApplicationReducer(path: .categories) { state,
message in
guard
let message = message as? QuoteMessage,
case .categoriesLoading = message
else { return }
state = .loading
}
.categoriesLoading Handler
kevin o’neillemail: kevin@oneill.id.au twitter: @kevinoneill
func createApplicationMessageHandler() -> ApplicationMessageHandler {
quoteMessageHandler <> imageMessageHandler <> logger
}
Application Message Handler
kevin o’neillemail: kevin@oneill.id.au twitter: @kevinoneill
Ergonomics
kevin o’neillemail: kevin@oneill.id.au twitter: @kevinoneill
private let categoriesLoading = ApplicationReducer(path: .categories) { state,
message in
guard
let message = message as? QuoteMessage,
case .categoriesLoading = message
else { return }
state = .loading
}
state is an inout parameter
kevin o’neillemail: kevin@oneill.id.au twitter: @kevinoneill
private let categoriesLoading = ApplicationReducer(path: .categories) { state,
message in
guard
let message = message as? QuoteMessage,
case .categoriesLoading = message
else { return }
state = .loading
}
KeyPaths for model access
kevin o’neillemail: kevin@oneill.id.au twitter: @kevinoneill
typealias ApplicationProgram = Program<

ApplicationEnvironment, ApplicationModel, ApplicationMessage>
typealias ApplicationCommand = Command<

ApplicationEnvironment, ApplicationMessage>
typealias ApplicationMessageHandler = MessageHandler<

ApplicationEnvironment, ApplicationModel, ApplicationMessage>
typealias ApplicationReducer = Reducer<

ApplicationModel, ApplicationMessage>
Typealias for the win
kevin o’neillemail: kevin@oneill.id.au twitter: @kevinoneill
private let categoriesLoading = ApplicationReducer(path: .categories) { state,
message in
guard
let message = message as? QuoteMessage,
case .categoriesLoading = message
else { return }
state = .loading
}
Reduced Noise
kevin o’neillemail: kevin@oneill.id.au twitter: @kevinoneill
SwiftUI
Declarative Views + Reactive State
kevin o’neillemail: kevin@oneill.id.au twitter: @kevinoneill
SwifTEA UI

WeeDux + SwiftUI
kevin o’neillemail: kevin@oneill.id.au twitter: @kevinoneill
Four Components



Store, Dispatcher

StoreContainer, StateContainer
kevin o’neillemail: kevin@oneill.id.au twitter: @kevinoneill
public final class Store<Environment, Model, Message>: ObservableObject {
public final class Dispatcher: ObservableObject { … }
@Published public private(set) var model: Model
public let dispatcher: Dispatcher
private var subscription: Cancellable?
public init(program: Program<Environment, Model, Message>) {
model = program.read()
dispatcher = Dispatcher(program: program)
subscription = program
.receive(on: RunLoop.main)
.sink { [weak self] value in
self?.model = value
}
}
deinit {
subscription?.cancel()
}
}
Store
kevin o’neillemail: kevin@oneill.id.au twitter: @kevinoneill
public final class Store<Environment, Model, Message>: ObservableObject {
public final class Dispatcher: ObservableObject { … }
@Published public private(set) var model: Model
public let dispatcher: Dispatcher
private var subscription: Cancellable?
public init(program: Program<Environment, Model, Message>) {
model = program.read()
dispatcher = Dispatcher(program: program)
subscription = program
.receive(on: RunLoop.main)
.sink { [weak self] value in
self?.model = value
}
}
deinit {
subscription?.cancel()
}
}
Tracks & Publishes Program Updates
kevin o’neillemail: kevin@oneill.id.au twitter: @kevinoneill
public final class Store<Environment, Model, Message>: ObservableObject {
public final class Dispatcher: ObservableObject { … }
@Published public private(set) var model: Model
public let dispatcher: Dispatcher
private var subscription: Cancellable?
public init(program: Program<Environment, Model, Message>) {
model = program.read()
dispatcher = Dispatcher(program: program)
subscription = program
.receive(on: RunLoop.main)
.sink { [weak self] value in
self?.model = value
}
}
deinit {
subscription?.cancel()
}
}
Always Delivers on Main
kevin o’neillemail: kevin@oneill.id.au twitter: @kevinoneill
public final class Dispatcher: ObservableObject {
private let program: Program<Environment, Model, Message>
public func send(_ message: Message) {
program.dispatch(message)
}
public func send(_ command: WeeDux.Command<Environment, Message>) {
program.execute(command)
}
init(program: Program<Environment, Model, Message>) {
self.program = program
}
}
Dispatcher
kevin o’neillemail: kevin@oneill.id.au twitter: @kevinoneill
public final class Dispatcher: ObservableObject {
private let program: Program<Environment, Model, Message>
public func send(_ message: Message) {
program.dispatch(message)
}
public func send(_ command: WeeDux.Command<Environment, Message>) {
program.execute(command)
}
init(program: Program<Environment, Model, Message>) {
self.program = program
}
}
Send Messages and Commands
kevin o’neillemail: kevin@oneill.id.au twitter: @kevinoneill
Containers

Separating Views from State Location
kevin o’neillemail: kevin@oneill.id.au twitter: @kevinoneill
struct StoreContainer<Environment, State, Message, Content>: View where Content: View {
@ObservedObject private var store: Store<Environment, State, Message>
private let content: () -> Content
var body: some View {
content()
.environmentObject(self.store)
.environmentObject(self.store.dispatcher)
}
init(for store: Store<Environment, State, Message>, content: @escaping () -> Content) {
self.store = store
self.content = content
}
}
StoreContainer
kevin o’neillemail: kevin@oneill.id.au twitter: @kevinoneill
struct StoreContainer<Environment, State, Message, Content>: View where Content: View {
@ObservedObject private var store: Store<Environment, State, Message>
private let content: () -> Content
var body: some View {
content()
.environmentObject(self.store)
.environmentObject(self.store.dispatcher)
}
init(for store: Store<Environment, State, Message>, content: @escaping () -> Content) {
self.store = store
self.content = content
}
}
Shares the Store and Dispatcher
kevin o’neillemail: kevin@oneill.id.au twitter: @kevinoneill
struct StateContainer<Environment, State, Message, Props, Content>: View 

where Content: View {
typealias Dispatcher = Store<Environment, State, Message>.Dispatcher
@EnvironmentObject private var store: Store<Environment, State, Message>
@EnvironmentObject private var dispatcher: Dispatcher
private let read: (State) -> Props
private let render: (Props, Dispatcher) -> Content
public init(read: @escaping (State) -> Props, 

render: @escaping (Props, Dispatcher) -> Content) {
self.read = read
self.render = render
}
public var body: some View {
render(read(store.model), dispatcher)
}
}
State Container
kevin o’neillemail: kevin@oneill.id.au twitter: @kevinoneill
struct StateContainer<Environment, State, Message, Props, Content>: View 

where Content: View {
typealias Dispatcher = Store<Environment, State, Message>.Dispatcher
@EnvironmentObject private var store: Store<Environment, State, Message>
@EnvironmentObject private var dispatcher: Dispatcher
private let render: (Props, Dispatcher) -> Content
private let read: (State) -> Props
public init(read: @escaping (State) -> Props, 

render: @escaping (Props, Dispatcher) -> Content) {
self.read = read
self.render = render
}
public var body: some View {
render(read(store.model), dispatcher)
}
}
Captures Store and Dispatcher
kevin o’neillemail: kevin@oneill.id.au twitter: @kevinoneill
struct StateContainer<Environment, State, Message, Props, Content>: View 

where Content: View {
typealias Dispatcher = Store<Environment, State, Message>.Dispatcher
@EnvironmentObject private var store: Store<Environment, State, Message>
@EnvironmentObject private var dispatcher: Dispatcher
private let read: (State) -> Props
private let render: (Props, Dispatcher) -> Content
public init(read: @escaping (State) -> Props, 

render: @escaping (Props, Dispatcher) -> Content) {
self.read = read
self.render = render
}
public var body: some View {
render(read(store.model), dispatcher)
}
}
Converts State to Some Type
kevin o’neillemail: kevin@oneill.id.au twitter: @kevinoneill
struct StateContainer<Environment, State, Message, Props, Content>: View 

where Content: View {
typealias Dispatcher = Store<Environment, State, Message>.Dispatcher
@EnvironmentObject private var store: Store<Environment, State, Message>
@EnvironmentObject private var dispatcher: Dispatcher
private let read: (State) -> Props
private let render: (Props, Dispatcher) -> Content
public init(read: @escaping (State) -> Props, 

render: @escaping (Props, Dispatcher) -> Content) {
self.read = read
self.render = render
}
public var body: some View {
render(read(store.model), dispatcher)
}
}
Renders the View with Some Type
kevin o’neillemail: kevin@oneill.id.au twitter: @kevinoneill
Ergonomics
kevin o’neillemail: kevin@oneill.id.au twitter: @kevinoneill
extension StateContainer {
public init(_ keypath: KeyPath<State, Props>,
render: @escaping (Props, Dispatcher) -> Content) {
self.init(read: { $0[keyPath: keypath] }, render: render)
}
public init<A, B>(_ a: KeyPath<State, A>,
_ b: KeyPath<State, B>,
render: @escaping ((A, B), Dispatcher) -> Content)
where Props == (A, B) {
self.init(read: { ($0[keyPath: a], $0[keyPath: b]) }, render: render)
}
…
}
KeyPaths
kevin o’neillemail: kevin@oneill.id.au twitter: @kevinoneill
typealias ApplicationStoreContainer<Content> = 

StoreContainer<ApplicationEnvironment, ApplicationModel, ApplicationMessage, Content>
where Content: View



typealias ApplicationStateContainer<Props, Content> =
StateContainer<ApplicationEnvironment, ApplicationModel, ApplicationMessage, Props, Content>
where Content: View
Typealias’ !!
kevin o’neillemail: kevin@oneill.id.au twitter: @kevinoneill
Why are Store and Dispatcher Split?
kevin o’neillemail: kevin@oneill.id.au twitter: @kevinoneill
A Couple of Patterns
kevin o’neillemail: kevin@oneill.id.au twitter: @kevinoneill
Container / ViewModel / Body
kevin o’neillemail: kevin@oneill.id.au twitter: @kevinoneill
struct CategoryList: View {
struct ViewModel { .. }
var body: some View {
ApplicationStateContainer(.categories) { categories, dispatcher in
CategoryListBody(model: ViewModel(model: categories, dispatcher: dispatcher))
}
}
}
Container
kevin o’neillemail: kevin@oneill.id.au twitter: @kevinoneill
struct ViewModel {
private let dispatcher: ApplicationStore.Dispatcher
let categories: ApplicationModel.Categories

func refresh(force _: Bool = false) {
dispatcher.send(QuoteCommands.refreshCategories)
}
init(categories: ApplicationModel.Categories, dispatcher: ApplicationStore.Dispatcher) {
self.categories = categories
self.dispatcher = dispatcher
}
}
View Model
kevin o’neillemail: kevin@oneill.id.au twitter: @kevinoneill
struct CategoryListBody: View {
let model: CategoryList.ViewModel
var body: some View {
ResourcePanel(model.categories) { categories in
List {
ForEach(categories, id: .id) { category in
NavigationLink(destination: QuoteView(category: category)) {
Text(category.title)
}
}
}
}
.padding()
.navigationBarTitle("Quote of the day")
}
}
Body
kevin o’neillemail: kevin@oneill.id.au twitter: @kevinoneill
Not an @ObservedObject or
ObservableObject in sight
kevin o’neillemail: kevin@oneill.id.au twitter: @kevinoneill
@State is still useful for local data
kevin o’neillemail: kevin@oneill.id.au twitter: @kevinoneill
Asyc Operations are Commands
kevin o’neillemail: kevin@oneill.id.au twitter: @kevinoneill
.background(
ResourcePanel(self.model.background, style: .hidden) {
Image(uiImage: $0)
.resizable()
.aspectRatio(contentMode: ContentMode.fill)
}
)
Image Loading
kevin o’neillemail: kevin@oneill.id.au twitter: @kevinoneill
private func refreshBackground(quote: Quote, force _: Bool = false) {
guard let image = images[quote.background] else {
return dispatcher.send(ImageCommands.load(location: quote.background))
}
switch image {
case .failed, .placeholder:
return dispatcher.send(ImageCommands.load(location: quote.background))
default:
break
}
}
Check availability in the Model
kevin o’neillemail: kevin@oneill.id.au twitter: @kevinoneill
var body: some View {
ApplicationStateContainer(.quotes, .cache.images) { props, dispatcher in
QuoteViewBody(model:ViewModel(

category: self.category,
quotes: props.0,
images: props.1,
dispatcher: dispatcher))
}
}
The Container Binds
kevin o’neillemail: kevin@oneill.id.au twitter: @kevinoneill
static func load(location: String) -> ApplicationCommand {
ApplicationCommand { _, publish in
guard let url = URL(string: location) else {
return publish(ImageMessage.failed(location: location, message: "invalid url"))
}
let task = URLSession.shared.dataTask(with: url) { (data, response, error) -> Void in
…

guard let image = UIImage(data: data) else {
return publish(ImageMessage.failed(location: location, message: … }
return publish(ImageMessage.loaded(location: location, image: image))
}
publish(ImageMessage.loading(location: location))
task.resume()
}
The Command Loads the Image
kevin o’neillemail: kevin@oneill.id.au twitter: @kevinoneill
private let loaded = ApplicationReducer(path: .cache.images) { state, message in
guard
let message = message as? ImageMessage,
case let .loaded(location, image) = message
else { return }
state[location] = .available(image)
}
The Event Handler Updates the State
kevin o’neillemail: kevin@oneill.id.au twitter: @kevinoneill
A Subtle Implementation

Anti-Pattern
kevin o’neillemail: kevin@oneill.id.au twitter: @kevinoneill
Commands should interact with the
outside world via the Environment
kevin o’neillemail: kevin@oneill.id.au twitter: @kevinoneill
Summary
kevin o’neillemail: kevin@oneill.id.au twitter: @kevinoneill
Find the code at:

https://github.com/weegigs/qotd
kevin o’neillemail: kevin@oneill.id.au twitter: @kevinoneill
Questions

More Related Content

Similar to SwifTEA UI - Unidirectional data flow with SwiftUI and WeeDux

create-netflix-clone-05-client-model.pdf
create-netflix-clone-05-client-model.pdfcreate-netflix-clone-05-client-model.pdf
create-netflix-clone-05-client-model.pdfShaiAlmog1
 
Firebase & SwiftUI Workshop
Firebase & SwiftUI WorkshopFirebase & SwiftUI Workshop
Firebase & SwiftUI WorkshopPeter Friese
 
A tour through Swift attributes
A tour through Swift attributesA tour through Swift attributes
A tour through Swift attributesMarco Eidinger
 
Embracing the Lollipop
Embracing the LollipopEmbracing the Lollipop
Embracing the LollipopSonja Kesic
 
Advanced #6 clean architecture
Advanced #6  clean architectureAdvanced #6  clean architecture
Advanced #6 clean architectureVitali Pekelis
 
Say bye to Fragments with Conductor & Kotlin
Say bye to Fragments with Conductor & KotlinSay bye to Fragments with Conductor & Kotlin
Say bye to Fragments with Conductor & KotlinMiquel Beltran Febrer
 
Building Cloud Castles
Building Cloud CastlesBuilding Cloud Castles
Building Cloud CastlesBen Scofield
 
Slaying Sacred Cows: Deconstructing Dependency Injection
Slaying Sacred Cows: Deconstructing Dependency InjectionSlaying Sacred Cows: Deconstructing Dependency Injection
Slaying Sacred Cows: Deconstructing Dependency InjectionTomer Gabel
 
A resource oriented framework using the DI/AOP/REST triangle
A resource oriented framework using the DI/AOP/REST triangleA resource oriented framework using the DI/AOP/REST triangle
A resource oriented framework using the DI/AOP/REST triangleAkihito Koriyama
 
Taming Core Data by Arek Holko, Macoscope
Taming Core Data by Arek Holko, MacoscopeTaming Core Data by Arek Holko, Macoscope
Taming Core Data by Arek Holko, MacoscopeMacoscope
 
Making the most out of kubernetes audit logs
Making the most out of kubernetes audit logsMaking the most out of kubernetes audit logs
Making the most out of kubernetes audit logsLaurent Bernaille
 
.NET Fest 2018. Антон Молдован. One year of using F# in production at SBTech
.NET Fest 2018. Антон Молдован. One year of using F# in production at SBTech.NET Fest 2018. Антон Молдован. One year of using F# in production at SBTech
.NET Fest 2018. Антон Молдован. One year of using F# in production at SBTechNETFest
 
Djangocon 2014 angular + django
Djangocon 2014 angular + djangoDjangocon 2014 angular + django
Djangocon 2014 angular + djangoNina Zakharenko
 
Moderne App-Architektur mit Dagger2 und RxJava
Moderne App-Architektur mit Dagger2 und RxJavaModerne App-Architektur mit Dagger2 und RxJava
Moderne App-Architektur mit Dagger2 und RxJavainovex GmbH
 
Selenium tests, the Object Oriented way
Selenium tests, the Object Oriented waySelenium tests, the Object Oriented way
Selenium tests, the Object Oriented wayimalittletester
 
The state of hooking into Drupal - DrupalCon Dublin
The state of hooking into Drupal - DrupalCon DublinThe state of hooking into Drupal - DrupalCon Dublin
The state of hooking into Drupal - DrupalCon DublinNida Ismail Shah
 
When dynamic becomes static: the next step in web caching techniques
When dynamic becomes static: the next step in web caching techniquesWhen dynamic becomes static: the next step in web caching techniques
When dynamic becomes static: the next step in web caching techniquesWim Godden
 
Content Driven Zend_Acl in the Model Layer
Content Driven Zend_Acl in the Model LayerContent Driven Zend_Acl in the Model Layer
Content Driven Zend_Acl in the Model LayerJeroen Keppens
 
Connect.Tech- Swift Memory Management
Connect.Tech- Swift Memory ManagementConnect.Tech- Swift Memory Management
Connect.Tech- Swift Memory Managementstable|kernel
 

Similar to SwifTEA UI - Unidirectional data flow with SwiftUI and WeeDux (20)

create-netflix-clone-05-client-model.pdf
create-netflix-clone-05-client-model.pdfcreate-netflix-clone-05-client-model.pdf
create-netflix-clone-05-client-model.pdf
 
Firebase & SwiftUI Workshop
Firebase & SwiftUI WorkshopFirebase & SwiftUI Workshop
Firebase & SwiftUI Workshop
 
A tour through Swift attributes
A tour through Swift attributesA tour through Swift attributes
A tour through Swift attributes
 
Embracing the Lollipop
Embracing the LollipopEmbracing the Lollipop
Embracing the Lollipop
 
Advanced #6 clean architecture
Advanced #6  clean architectureAdvanced #6  clean architecture
Advanced #6 clean architecture
 
Say bye to Fragments with Conductor & Kotlin
Say bye to Fragments with Conductor & KotlinSay bye to Fragments with Conductor & Kotlin
Say bye to Fragments with Conductor & Kotlin
 
Building Cloud Castles
Building Cloud CastlesBuilding Cloud Castles
Building Cloud Castles
 
Slaying Sacred Cows: Deconstructing Dependency Injection
Slaying Sacred Cows: Deconstructing Dependency InjectionSlaying Sacred Cows: Deconstructing Dependency Injection
Slaying Sacred Cows: Deconstructing Dependency Injection
 
Modern android development
Modern android developmentModern android development
Modern android development
 
A resource oriented framework using the DI/AOP/REST triangle
A resource oriented framework using the DI/AOP/REST triangleA resource oriented framework using the DI/AOP/REST triangle
A resource oriented framework using the DI/AOP/REST triangle
 
Taming Core Data by Arek Holko, Macoscope
Taming Core Data by Arek Holko, MacoscopeTaming Core Data by Arek Holko, Macoscope
Taming Core Data by Arek Holko, Macoscope
 
Making the most out of kubernetes audit logs
Making the most out of kubernetes audit logsMaking the most out of kubernetes audit logs
Making the most out of kubernetes audit logs
 
.NET Fest 2018. Антон Молдован. One year of using F# in production at SBTech
.NET Fest 2018. Антон Молдован. One year of using F# in production at SBTech.NET Fest 2018. Антон Молдован. One year of using F# in production at SBTech
.NET Fest 2018. Антон Молдован. One year of using F# in production at SBTech
 
Djangocon 2014 angular + django
Djangocon 2014 angular + djangoDjangocon 2014 angular + django
Djangocon 2014 angular + django
 
Moderne App-Architektur mit Dagger2 und RxJava
Moderne App-Architektur mit Dagger2 und RxJavaModerne App-Architektur mit Dagger2 und RxJava
Moderne App-Architektur mit Dagger2 und RxJava
 
Selenium tests, the Object Oriented way
Selenium tests, the Object Oriented waySelenium tests, the Object Oriented way
Selenium tests, the Object Oriented way
 
The state of hooking into Drupal - DrupalCon Dublin
The state of hooking into Drupal - DrupalCon DublinThe state of hooking into Drupal - DrupalCon Dublin
The state of hooking into Drupal - DrupalCon Dublin
 
When dynamic becomes static: the next step in web caching techniques
When dynamic becomes static: the next step in web caching techniquesWhen dynamic becomes static: the next step in web caching techniques
When dynamic becomes static: the next step in web caching techniques
 
Content Driven Zend_Acl in the Model Layer
Content Driven Zend_Acl in the Model LayerContent Driven Zend_Acl in the Model Layer
Content Driven Zend_Acl in the Model Layer
 
Connect.Tech- Swift Memory Management
Connect.Tech- Swift Memory ManagementConnect.Tech- Swift Memory Management
Connect.Tech- Swift Memory Management
 

More from Kevin O'Neill

Building Hypermedia API's - YOW! Night - March 2013
Building Hypermedia API's - YOW! Night - March 2013Building Hypermedia API's - YOW! Night - March 2013
Building Hypermedia API's - YOW! Night - March 2013Kevin O'Neill
 
Hypermedia for the iOS developer - Swipe 2012
Hypermedia for the iOS developer - Swipe  2012Hypermedia for the iOS developer - Swipe  2012
Hypermedia for the iOS developer - Swipe 2012Kevin O'Neill
 
Swipe 2011 - iOS Gems
Swipe 2011 - iOS GemsSwipe 2011 - iOS Gems
Swipe 2011 - iOS GemsKevin O'Neill
 
YOW Mobile Night 2011 - The realestate.com.au mobile story
YOW Mobile Night 2011 - The realestate.com.au mobile storyYOW Mobile Night 2011 - The realestate.com.au mobile story
YOW Mobile Night 2011 - The realestate.com.au mobile storyKevin O'Neill
 

More from Kevin O'Neill (6)

Deploying the Graph
Deploying the GraphDeploying the Graph
Deploying the Graph
 
A Slice of Scala
A Slice of Scala A Slice of Scala
A Slice of Scala
 
Building Hypermedia API's - YOW! Night - March 2013
Building Hypermedia API's - YOW! Night - March 2013Building Hypermedia API's - YOW! Night - March 2013
Building Hypermedia API's - YOW! Night - March 2013
 
Hypermedia for the iOS developer - Swipe 2012
Hypermedia for the iOS developer - Swipe  2012Hypermedia for the iOS developer - Swipe  2012
Hypermedia for the iOS developer - Swipe 2012
 
Swipe 2011 - iOS Gems
Swipe 2011 - iOS GemsSwipe 2011 - iOS Gems
Swipe 2011 - iOS Gems
 
YOW Mobile Night 2011 - The realestate.com.au mobile story
YOW Mobile Night 2011 - The realestate.com.au mobile storyYOW Mobile Night 2011 - The realestate.com.au mobile story
YOW Mobile Night 2011 - The realestate.com.au mobile story
 

SwifTEA UI - Unidirectional data flow with SwiftUI and WeeDux

  • 1. SwifTEA UI kevin o’neillemail: kevin@oneill.id.au twitter: @kevinoneill
  • 2. kevin o’neillemail: kevin@oneill.id.au twitter: @kevinoneill About Me
  • 3. kevin o’neillemail: kevin@oneill.id.au twitter: @kevinoneill Swift, Go, Typescript, Python I write software
  • 4. kevin o’neillemail: kevin@oneill.id.au twitter: @kevinoneill I play music
  • 5. kevin o’neillemail: kevin@oneill.id.au twitter: @kevinoneill I ride mountain bikes*
  • 6. SwifTEA UI kevin o’neillemail: kevin@oneill.id.au twitter: @kevinoneill
  • 7. kevin o’neillemail: kevin@oneill.id.au twitter: @kevinoneill The Elm Architecture (TEA)
  • 8. kevin o’neillemail: kevin@oneill.id.au twitter: @kevinoneill (Model, Message) -> (Model, Command)
  • 9. kevin o’neillemail: kevin@oneill.id.au twitter: @kevinoneill Model
  • 10. kevin o’neillemail: kevin@oneill.id.au twitter: @kevinoneill Message
  • 11. kevin o’neillemail: kevin@oneill.id.au twitter: @kevinoneill Command
  • 12. kevin o’neillemail: kevin@oneill.id.au twitter: @kevinoneill TEA vs FLUX (Redux)
  • 13. kevin o’neillemail: kevin@oneill.id.au twitter: @kevinoneill (Model, Message) -> Model
  • 14. kevin o’neillemail: kevin@oneill.id.au twitter: @kevinoneill Lacks Side Effect Handling
  • 15. kevin o’neillemail: kevin@oneill.id.au twitter: @kevinoneill SwifTEA UI
  • 16. kevin o’neillemail: kevin@oneill.id.au twitter: @kevinoneill Quick Demo
  • 17. kevin o’neillemail: kevin@oneill.id.au twitter: @kevinoneill WeeDux
  • 18. kevin o’neillemail: kevin@oneill.id.au twitter: @kevinoneill (Model, Message) -> (Model*, Command)
  • 19. kevin o’neillemail: kevin@oneill.id.au twitter: @kevinoneill struct ApplicationModel: Codable { enum CodingKeys: String, CodingKey { case categories case quotes } struct Cache { typealias Images = [String: Resource<UIImage>] var images: Images = [:] var log: [ApplicationMessage] = [] } typealias Categories = Resource<[QuoteCategory]> typealias Quotes = [String: Resource<Quote>] var categories: Categories = .placeholder var quotes: Quotes = [:] var cache: Cache = Cache() } Model
  • 20. kevin o’neillemail: kevin@oneill.id.au twitter: @kevinoneill enum QuoteMessage: ApplicationMessage { case categoriesLoading case categoriesLoadingFailed(error: QuoteServiceError) case categoriesLoaded(categories: [QuoteCategory]) case quoteLoading(category: String) case quoteLoadingFailed(category: String, error: QuoteServiceError) case quoteLoaded(category: String, quote: Quote) } Quote Message
  • 21. kevin o’neillemail: kevin@oneill.id.au twitter: @kevinoneill private let categoriesLoading = ApplicationReducer(path: .categories) { state, message in guard let message = message as? QuoteMessage, case .categoriesLoading = message else { return } state = .loading } .categoriesLoading Handler
  • 22. kevin o’neillemail: kevin@oneill.id.au twitter: @kevinoneill func createApplicationMessageHandler() -> ApplicationMessageHandler { quoteMessageHandler <> imageMessageHandler <> logger } Application Message Handler
  • 23. kevin o’neillemail: kevin@oneill.id.au twitter: @kevinoneill Ergonomics
  • 24. kevin o’neillemail: kevin@oneill.id.au twitter: @kevinoneill private let categoriesLoading = ApplicationReducer(path: .categories) { state, message in guard let message = message as? QuoteMessage, case .categoriesLoading = message else { return } state = .loading } state is an inout parameter
  • 25. kevin o’neillemail: kevin@oneill.id.au twitter: @kevinoneill private let categoriesLoading = ApplicationReducer(path: .categories) { state, message in guard let message = message as? QuoteMessage, case .categoriesLoading = message else { return } state = .loading } KeyPaths for model access
  • 26. kevin o’neillemail: kevin@oneill.id.au twitter: @kevinoneill typealias ApplicationProgram = Program<
 ApplicationEnvironment, ApplicationModel, ApplicationMessage> typealias ApplicationCommand = Command<
 ApplicationEnvironment, ApplicationMessage> typealias ApplicationMessageHandler = MessageHandler<
 ApplicationEnvironment, ApplicationModel, ApplicationMessage> typealias ApplicationReducer = Reducer<
 ApplicationModel, ApplicationMessage> Typealias for the win
  • 27. kevin o’neillemail: kevin@oneill.id.au twitter: @kevinoneill private let categoriesLoading = ApplicationReducer(path: .categories) { state, message in guard let message = message as? QuoteMessage, case .categoriesLoading = message else { return } state = .loading } Reduced Noise
  • 28. kevin o’neillemail: kevin@oneill.id.au twitter: @kevinoneill SwiftUI Declarative Views + Reactive State
  • 29. kevin o’neillemail: kevin@oneill.id.au twitter: @kevinoneill SwifTEA UI
 WeeDux + SwiftUI
  • 30. kevin o’neillemail: kevin@oneill.id.au twitter: @kevinoneill Four Components
 
 Store, Dispatcher
 StoreContainer, StateContainer
  • 31. kevin o’neillemail: kevin@oneill.id.au twitter: @kevinoneill public final class Store<Environment, Model, Message>: ObservableObject { public final class Dispatcher: ObservableObject { … } @Published public private(set) var model: Model public let dispatcher: Dispatcher private var subscription: Cancellable? public init(program: Program<Environment, Model, Message>) { model = program.read() dispatcher = Dispatcher(program: program) subscription = program .receive(on: RunLoop.main) .sink { [weak self] value in self?.model = value } } deinit { subscription?.cancel() } } Store
  • 32. kevin o’neillemail: kevin@oneill.id.au twitter: @kevinoneill public final class Store<Environment, Model, Message>: ObservableObject { public final class Dispatcher: ObservableObject { … } @Published public private(set) var model: Model public let dispatcher: Dispatcher private var subscription: Cancellable? public init(program: Program<Environment, Model, Message>) { model = program.read() dispatcher = Dispatcher(program: program) subscription = program .receive(on: RunLoop.main) .sink { [weak self] value in self?.model = value } } deinit { subscription?.cancel() } } Tracks & Publishes Program Updates
  • 33. kevin o’neillemail: kevin@oneill.id.au twitter: @kevinoneill public final class Store<Environment, Model, Message>: ObservableObject { public final class Dispatcher: ObservableObject { … } @Published public private(set) var model: Model public let dispatcher: Dispatcher private var subscription: Cancellable? public init(program: Program<Environment, Model, Message>) { model = program.read() dispatcher = Dispatcher(program: program) subscription = program .receive(on: RunLoop.main) .sink { [weak self] value in self?.model = value } } deinit { subscription?.cancel() } } Always Delivers on Main
  • 34. kevin o’neillemail: kevin@oneill.id.au twitter: @kevinoneill public final class Dispatcher: ObservableObject { private let program: Program<Environment, Model, Message> public func send(_ message: Message) { program.dispatch(message) } public func send(_ command: WeeDux.Command<Environment, Message>) { program.execute(command) } init(program: Program<Environment, Model, Message>) { self.program = program } } Dispatcher
  • 35. kevin o’neillemail: kevin@oneill.id.au twitter: @kevinoneill public final class Dispatcher: ObservableObject { private let program: Program<Environment, Model, Message> public func send(_ message: Message) { program.dispatch(message) } public func send(_ command: WeeDux.Command<Environment, Message>) { program.execute(command) } init(program: Program<Environment, Model, Message>) { self.program = program } } Send Messages and Commands
  • 36. kevin o’neillemail: kevin@oneill.id.au twitter: @kevinoneill Containers
 Separating Views from State Location
  • 37. kevin o’neillemail: kevin@oneill.id.au twitter: @kevinoneill struct StoreContainer<Environment, State, Message, Content>: View where Content: View { @ObservedObject private var store: Store<Environment, State, Message> private let content: () -> Content var body: some View { content() .environmentObject(self.store) .environmentObject(self.store.dispatcher) } init(for store: Store<Environment, State, Message>, content: @escaping () -> Content) { self.store = store self.content = content } } StoreContainer
  • 38. kevin o’neillemail: kevin@oneill.id.au twitter: @kevinoneill struct StoreContainer<Environment, State, Message, Content>: View where Content: View { @ObservedObject private var store: Store<Environment, State, Message> private let content: () -> Content var body: some View { content() .environmentObject(self.store) .environmentObject(self.store.dispatcher) } init(for store: Store<Environment, State, Message>, content: @escaping () -> Content) { self.store = store self.content = content } } Shares the Store and Dispatcher
  • 39. kevin o’neillemail: kevin@oneill.id.au twitter: @kevinoneill struct StateContainer<Environment, State, Message, Props, Content>: View 
 where Content: View { typealias Dispatcher = Store<Environment, State, Message>.Dispatcher @EnvironmentObject private var store: Store<Environment, State, Message> @EnvironmentObject private var dispatcher: Dispatcher private let read: (State) -> Props private let render: (Props, Dispatcher) -> Content public init(read: @escaping (State) -> Props, 
 render: @escaping (Props, Dispatcher) -> Content) { self.read = read self.render = render } public var body: some View { render(read(store.model), dispatcher) } } State Container
  • 40. kevin o’neillemail: kevin@oneill.id.au twitter: @kevinoneill struct StateContainer<Environment, State, Message, Props, Content>: View 
 where Content: View { typealias Dispatcher = Store<Environment, State, Message>.Dispatcher @EnvironmentObject private var store: Store<Environment, State, Message> @EnvironmentObject private var dispatcher: Dispatcher private let render: (Props, Dispatcher) -> Content private let read: (State) -> Props public init(read: @escaping (State) -> Props, 
 render: @escaping (Props, Dispatcher) -> Content) { self.read = read self.render = render } public var body: some View { render(read(store.model), dispatcher) } } Captures Store and Dispatcher
  • 41. kevin o’neillemail: kevin@oneill.id.au twitter: @kevinoneill struct StateContainer<Environment, State, Message, Props, Content>: View 
 where Content: View { typealias Dispatcher = Store<Environment, State, Message>.Dispatcher @EnvironmentObject private var store: Store<Environment, State, Message> @EnvironmentObject private var dispatcher: Dispatcher private let read: (State) -> Props private let render: (Props, Dispatcher) -> Content public init(read: @escaping (State) -> Props, 
 render: @escaping (Props, Dispatcher) -> Content) { self.read = read self.render = render } public var body: some View { render(read(store.model), dispatcher) } } Converts State to Some Type
  • 42. kevin o’neillemail: kevin@oneill.id.au twitter: @kevinoneill struct StateContainer<Environment, State, Message, Props, Content>: View 
 where Content: View { typealias Dispatcher = Store<Environment, State, Message>.Dispatcher @EnvironmentObject private var store: Store<Environment, State, Message> @EnvironmentObject private var dispatcher: Dispatcher private let read: (State) -> Props private let render: (Props, Dispatcher) -> Content public init(read: @escaping (State) -> Props, 
 render: @escaping (Props, Dispatcher) -> Content) { self.read = read self.render = render } public var body: some View { render(read(store.model), dispatcher) } } Renders the View with Some Type
  • 43. kevin o’neillemail: kevin@oneill.id.au twitter: @kevinoneill Ergonomics
  • 44. kevin o’neillemail: kevin@oneill.id.au twitter: @kevinoneill extension StateContainer { public init(_ keypath: KeyPath<State, Props>, render: @escaping (Props, Dispatcher) -> Content) { self.init(read: { $0[keyPath: keypath] }, render: render) } public init<A, B>(_ a: KeyPath<State, A>, _ b: KeyPath<State, B>, render: @escaping ((A, B), Dispatcher) -> Content) where Props == (A, B) { self.init(read: { ($0[keyPath: a], $0[keyPath: b]) }, render: render) } … } KeyPaths
  • 45. kevin o’neillemail: kevin@oneill.id.au twitter: @kevinoneill typealias ApplicationStoreContainer<Content> = 
 StoreContainer<ApplicationEnvironment, ApplicationModel, ApplicationMessage, Content> where Content: View
 
 typealias ApplicationStateContainer<Props, Content> = StateContainer<ApplicationEnvironment, ApplicationModel, ApplicationMessage, Props, Content> where Content: View Typealias’ !!
  • 46. kevin o’neillemail: kevin@oneill.id.au twitter: @kevinoneill Why are Store and Dispatcher Split?
  • 47. kevin o’neillemail: kevin@oneill.id.au twitter: @kevinoneill A Couple of Patterns
  • 48. kevin o’neillemail: kevin@oneill.id.au twitter: @kevinoneill Container / ViewModel / Body
  • 49. kevin o’neillemail: kevin@oneill.id.au twitter: @kevinoneill struct CategoryList: View { struct ViewModel { .. } var body: some View { ApplicationStateContainer(.categories) { categories, dispatcher in CategoryListBody(model: ViewModel(model: categories, dispatcher: dispatcher)) } } } Container
  • 50. kevin o’neillemail: kevin@oneill.id.au twitter: @kevinoneill struct ViewModel { private let dispatcher: ApplicationStore.Dispatcher let categories: ApplicationModel.Categories
 func refresh(force _: Bool = false) { dispatcher.send(QuoteCommands.refreshCategories) } init(categories: ApplicationModel.Categories, dispatcher: ApplicationStore.Dispatcher) { self.categories = categories self.dispatcher = dispatcher } } View Model
  • 51. kevin o’neillemail: kevin@oneill.id.au twitter: @kevinoneill struct CategoryListBody: View { let model: CategoryList.ViewModel var body: some View { ResourcePanel(model.categories) { categories in List { ForEach(categories, id: .id) { category in NavigationLink(destination: QuoteView(category: category)) { Text(category.title) } } } } .padding() .navigationBarTitle("Quote of the day") } } Body
  • 52. kevin o’neillemail: kevin@oneill.id.au twitter: @kevinoneill Not an @ObservedObject or ObservableObject in sight
  • 53. kevin o’neillemail: kevin@oneill.id.au twitter: @kevinoneill @State is still useful for local data
  • 54. kevin o’neillemail: kevin@oneill.id.au twitter: @kevinoneill Asyc Operations are Commands
  • 55. kevin o’neillemail: kevin@oneill.id.au twitter: @kevinoneill .background( ResourcePanel(self.model.background, style: .hidden) { Image(uiImage: $0) .resizable() .aspectRatio(contentMode: ContentMode.fill) } ) Image Loading
  • 56. kevin o’neillemail: kevin@oneill.id.au twitter: @kevinoneill private func refreshBackground(quote: Quote, force _: Bool = false) { guard let image = images[quote.background] else { return dispatcher.send(ImageCommands.load(location: quote.background)) } switch image { case .failed, .placeholder: return dispatcher.send(ImageCommands.load(location: quote.background)) default: break } } Check availability in the Model
  • 57. kevin o’neillemail: kevin@oneill.id.au twitter: @kevinoneill var body: some View { ApplicationStateContainer(.quotes, .cache.images) { props, dispatcher in QuoteViewBody(model:ViewModel(
 category: self.category, quotes: props.0, images: props.1, dispatcher: dispatcher)) } } The Container Binds
  • 58. kevin o’neillemail: kevin@oneill.id.au twitter: @kevinoneill static func load(location: String) -> ApplicationCommand { ApplicationCommand { _, publish in guard let url = URL(string: location) else { return publish(ImageMessage.failed(location: location, message: "invalid url")) } let task = URLSession.shared.dataTask(with: url) { (data, response, error) -> Void in …
 guard let image = UIImage(data: data) else { return publish(ImageMessage.failed(location: location, message: … } return publish(ImageMessage.loaded(location: location, image: image)) } publish(ImageMessage.loading(location: location)) task.resume() } The Command Loads the Image
  • 59. kevin o’neillemail: kevin@oneill.id.au twitter: @kevinoneill private let loaded = ApplicationReducer(path: .cache.images) { state, message in guard let message = message as? ImageMessage, case let .loaded(location, image) = message else { return } state[location] = .available(image) } The Event Handler Updates the State
  • 60. kevin o’neillemail: kevin@oneill.id.au twitter: @kevinoneill A Subtle Implementation
 Anti-Pattern
  • 61. kevin o’neillemail: kevin@oneill.id.au twitter: @kevinoneill Commands should interact with the outside world via the Environment
  • 62. kevin o’neillemail: kevin@oneill.id.au twitter: @kevinoneill Summary
  • 63. kevin o’neillemail: kevin@oneill.id.au twitter: @kevinoneill Find the code at:
 https://github.com/weegigs/qotd
  • 64. kevin o’neillemail: kevin@oneill.id.au twitter: @kevinoneill Questions