Skip to content

Instantly share code, notes, and snippets.

@embassem
Forked from JaviSoto/DataLoadState.swift
Created April 14, 2020 11:54
Show Gist options
  • Select an option

  • Save embassem/ac652fd1f5e63851e20c90d2675a1972 to your computer and use it in GitHub Desktop.

Select an option

Save embassem/ac652fd1f5e63851e20c90d2675a1972 to your computer and use it in GitHub Desktop.

Revisions

  1. @JaviSoto JaviSoto revised this gist Apr 21, 2016. 1 changed file with 5 additions and 0 deletions.
    5 changes: 5 additions & 0 deletions ReactiveCocoaExtensions.swift
    Original file line number Diff line number Diff line change
    @@ -1,6 +1,11 @@
    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
  2. @JaviSoto JaviSoto revised this gist Apr 20, 2016. 1 changed file with 21 additions and 0 deletions.
    21 changes: 21 additions & 0 deletions SampleViewModel.swift
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,21 @@
    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)
    }
    }
  3. @JaviSoto JaviSoto created this gist Apr 20, 2016.
    137 changes: 137 additions & 0 deletions DataLoadState.swift
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,137 @@
    //
    // 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)
    }
    }
    }
    }
    27 changes: 27 additions & 0 deletions ReactiveCocoaExtensions.swift
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,27 @@
    import ReactiveCocoa

    extension SignalProducerType {
    @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
    }
    }
    }
    }