SwiftUI gives us the opportunity to exploit unidirectional data flow within iOS and related platforms. This talk shows how to use WeeDux to drive SwiftUI and explores the ergonomics of the solution.
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
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
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
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
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