Last active
January 23, 2019 21:01
-
-
Save acrookston/a0f871d93f88bf06865a2f3c0cc467aa to your computer and use it in GitHub Desktop.
Exploring view/controller state with Loading / Content / Error (LCE)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/// ContentLoader is a lightweight state manager for loading and displaying content and/or errors. | |
/// It allows you to repeatedly make requests and display new content or cached content in the event of an error. | |
/// Commonly known as RemoteData or LCE (Loading / Content / Error). | |
/// Inspired by https://tech.instacart.com/lce-modeling-data-loading-in-rxjava-b798ac98d80 | |
/// | |
final class ContentLoader<T> { | |
init() { } | |
@discardableResult func loader(_ loader: (() -> Void)?) -> ContentLoader<T> { | |
self.loader = loader | |
return self | |
} | |
@discardableResult func update(_ update: (() -> Void)?) -> ContentLoader<T> { | |
self.update = update | |
return self | |
} | |
private var loader: (() -> Void)? | |
private var update: (() -> Void)? | |
var loading: Bool = false | |
var content: T? | |
var error: Error? | |
var hasContent: Bool { return content != nil } | |
var hasError: Bool { return error != nil } | |
func load() { | |
loading = true | |
loader?() | |
} | |
func success(content: T) { | |
self.content = content | |
self.loading = false | |
update?() | |
} | |
func failure(error: Error?) { | |
self.error = error | |
self.loading = false | |
update?() | |
} | |
} | |
// usage psudocode | |
class ViewController { | |
let contentLoader = ContentLoader<JSON>() | |
init { | |
self.contentLoader.loader({ | |
async { | |
loadData { data, error in | |
if let json = JSON(data) { | |
self.contentLoader.success(json) | |
} else { | |
self.contentLoader.failure(error ?? ParsingError()) | |
} | |
} | |
} | |
}).update({ | |
updateTheUI() | |
}).load() | |
} | |
func updateTheUI() { | |
// Handle the state enum to draw correct UI | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import BrightFutures | |
import Result | |
/// ContentLoader is a lightweight state manager for loading and displaying content and/or errors. | |
/// It allows you to repeatedly make requests and display new content or cached content in the event of an error. | |
/// Commonly known as RemoteData or LCE (Loading / Content / Error). | |
/// Inspired by https://tech.instacart.com/lce-modeling-data-loading-in-rxjava-b798ac98d80 | |
/// | |
final class ContentLoader<T> { | |
init() { } | |
private var loader: (() -> Void)? | |
private var update: (() -> Void)? | |
var loading: Bool = false | |
var content: T? | |
var error: Error? | |
var hasContent: Bool { return content != nil } | |
var hasError: Bool { return error != nil } | |
@discardableResult func loader(_ loader: (() -> Void)?) -> ContentLoader<T> { | |
self.loader = loader | |
return self | |
} | |
@discardableResult func onUpdate(_ update: (() -> Void)?) -> ContentLoader<T> { | |
self.update = update | |
return self | |
} | |
func load(loader: (() -> Void)?) { | |
loading = true | |
if let loader = loader { | |
loader() | |
} else if let loader = self.loader { | |
loader() | |
} else { | |
loading = false | |
failure(error: ContentLoaderError.noLoadingMethod) | |
} | |
} | |
func load(future: Future<T, AnyError>) { | |
loading = true | |
future | |
.onSuccess { self.success(content: $0) } | |
.onFailure { self.failure(error: $0.error) } | |
.onComplete { _ in self.loading = false } | |
} | |
func success(content: T) { | |
self.content = content | |
self.loading = false | |
update?() | |
} | |
func failure(error: Error?) { | |
self.error = error | |
self.loading = false | |
update?() | |
} | |
/// Does not cancel any ongoing async work | |
func reset() { | |
error = nil | |
content = nil | |
loading = false | |
} | |
} | |
enum ContentLoaderError: Error { | |
case noLoadingMethod | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Pseudocode | |
// Problems: | |
// - no cached state | |
// - can't display error over existing content | |
// - not easily reusable | |
// Is less verbose, perhaps quicker to understand. | |
class ViewController { | |
enum State { | |
case loading | |
case success(JSON?) | |
case failure(Error?) | |
} | |
var state: State { | |
didSet { updateTheUI() } | |
} | |
init { | |
load() | |
} | |
func load() { | |
state = .loading | |
async { | |
loadData { data, error in | |
if let json = JSON(data) { | |
self.state = .success(json) | |
} else { | |
self.state = .failure(error ?? ParsingError()) | |
} | |
} | |
} | |
} | |
func updateTheUI() { | |
// Handle the state enum to draw correct UI | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment