Skip to content

Instantly share code, notes, and snippets.

@JaviSoto
Last active April 14, 2020 11:54
Show Gist options
  • Save JaviSoto/9ea2205d4216629e2926669b97039c01 to your computer and use it in GitHub Desktop.
Save JaviSoto/9ea2205d4216629e2926669b97039c01 to your computer and use it in GitHub Desktop.
DataLoadState
//
// DataLoadState.swift
// Fabric
//
// Created by Javier Soto on 3/16/16.
// Copyright © 2016 Fabric. All rights reserved.
//
import Foundation
import ReactiveCocoa
import enum Result.NoError
protocol DataLoadStateProtocol {
associatedtype IdentifierType
associatedtype DataType
var dataLoadState: DataLoadState<IdentifierType, DataType> { get }
}
/// Identifier allows us to specify a value that allows us to know something from the data that we're loading before we load it.
enum DataLoadState<Identifier, Data>: DataLoadStateProtocol {
typealias IdentifierType = Identifier
typealias DataType = Data
case Loading(Identifier)
case Failed(Identifier)
case Loaded(Identifier, Data)
var identifier: Identifier {
switch self {
case let .Loading(identifier): return identifier
case let .Failed(identifier): return identifier
case let .Loaded(identifier, _): return identifier
}
}
var loading: Bool {
switch self {
case .Loading: return true
case .Failed, .Loaded: return false
}
}
var success: Bool {
switch self {
case .Loading, .Failed: return false
case .Loaded: return true
}
}
var data: Data? {
switch self {
case .Loading, .Failed: return nil
case let .Loaded(_, data): return data
}
}
var dataLoadState: DataLoadState<IdentifierType, DataType> {
return self
}
func map<NewDataType>(f: Data -> NewDataType) -> DataLoadState<Identifier, NewDataType> {
switch self {
case let .Loading(identifier): return DataLoadState<Identifier, NewDataType>.Loading(identifier)
case let .Failed(identifier): return DataLoadState<Identifier, NewDataType>.Failed(identifier)
case let .Loaded(identifier, data): return DataLoadState<Identifier, NewDataType>.Loaded(identifier, f(data))
}
}
}
func ==<Identifier, DataType where Identifier: Equatable, DataType: Equatable>(lhs: DataLoadState<Identifier, DataType>, rhs: DataLoadState<Identifier, DataType>) -> Bool {
switch (lhs, rhs) {
case let (.Loading(identifier1), .Loading(identifier2)) where identifier1 == identifier2: return true
case let (.Failed(identifier1), .Failed(identifier2)) where identifier1 == identifier2: return true
case let (.Loaded(identifier1, data1), .Loaded(identifier2, data2)) where identifier1 == identifier2 && data1 == data2: return true
default: return false
}
}
/// These extension methods are helpers to create `DataLoadState`s without an `Identifier` (the common case)
extension DataLoadState {
static func loadingData<T>() -> DataLoadState<(), T> {
return DataLoadState<(), T>.Loading(())
}
static func failedLoad<T>() -> DataLoadState<(), T> {
return DataLoadState<(), T>.Failed(())
}
static func loadedData<T>(data: T) -> DataLoadState<(), T> {
return DataLoadState<(), T>.Loaded((), data)
}
}
extension SignalProducerType {
@warn_unused_result(message="Did you forget to call `start` on the producer?")
func materializeToLoadState<IdentifierType>(identifier identifier: IdentifierType, redirectErrorsToObserver errorObserver: Observer<Self.Error, Result.NoError>? = nil) -> SignalProducer<DataLoadState<IdentifierType, Self.Value>, NoError> {
let producer = self
.map { DataLoadState.Loaded(identifier, $0) }
.startWithValue(DataLoadState.Loading(identifier))
if let errorObserver = errorObserver {
return producer.redirectErrorsToObserver(errorObserver, replacementValue: DataLoadState.Failed(identifier))
}
else {
return producer.ignoreErrors(replacementValue: DataLoadState.Failed(identifier))
}
}
func materializeToLoadState(redirectErrorsToObserver errorObserver: Observer<Self.Error, Result.NoError>? = nil) -> SignalProducer<DataLoadState<(), Self.Value>, NoError> {
return self.materializeToLoadState(identifier: ())
}
}
extension SignalProducerType where Value: DataLoadStateProtocol {
@warn_unused_result(message="Did you forget to call `start` on the producer?")
func ignoreLoadingAndErrorsAfterSuccessfulLoad() -> SignalProducer<DataLoadState<Self.Value.IdentifierType, Self.Value.DataType>, Self.Error> {
return SignalProducer { observer, compositeDisposable in
var hasSuccededOnce: Bool = false
self.map { $0.dataLoadState }
.filter { value in
defer {
if value.success {
hasSuccededOnce = true
}
}
return !hasSuccededOnce || value.success
}
.startWithSignal { signal, signalDisposable in
compositeDisposable += signalDisposable
signal.observe(observer)
}
}
}
}
import ReactiveCocoa
extension SignalProducerType {
@warn_unused_result(message="Did you forget to call `start` on the producer?")
public func startWithValue(value: Value) -> SignalProducer<Self.Value, Self.Error> {
return SignalProducer(value: value).concat(self.producer)
}
@warn_unused_result(message="Did you forget to call `start` on the producer?")
public func ignoreErrors(replacementValue replacementValue: Self.Value? = nil, andDo block: (Self.Error -> ())? = nil) -> SignalProducer<Self.Value, NoError> {
return self.flatMapError { error in
log.debug("Ignoring error: \(error)")
block?(error)
return replacementValue.map(SignalProducer.init) ?? .empty
}
}
@warn_unused_result(message="Did you forget to call `start` on the producer?")
public func redirectErrorsToObserver(observer: Observer<Self.Error, NoError>, replacementValue: Self.Value? = nil) -> SignalProducer<Self.Value, NoError> {
return self.flatMapError { error in
observer.sendNext(error)
if let value = replacementValue {
return SignalProducer(value: value)
}
else {
return SignalProducer.empty
}
}
}
}
final class SampleViewModel {
typealias ElementsDataLoadState = DataLoadState<(), [Element]>
struct SampleViewData {
let elements: ElementsDataLoadState
}
let viewData: AnyProperty<SampleViewData>
private let viewDataMutableProperty = MutableProperty<SampleViewData>(SampleViewData(elements: .loadingData()))
let errors: Signal<FabricAPIError, NoError>
private let errorsObserver: Observer<FabricAPIError, NoError>
init(api: AuthenticatedFabricAPI) {
(self.errors, self.errorsObserver) = Signal<FabricAPIError, NoError>.pipe()
self.viewDataMutableProperty <~ api.requestElements()
.materializeToLoadState(redirectErrorsToObserver: self.errorsObserver)
.map(SampleViewData.init)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment