Skip to content

Instantly share code, notes, and snippets.

@a-voronov
Last active August 31, 2020 11:08
Show Gist options
  • Save a-voronov/aab3ba20d1807e71b7b2ecacec13be01 to your computer and use it in GitHub Desktop.
Save a-voronov/aab3ba20d1807e71b7b2ecacec13be01 to your computer and use it in GitHub Desktop.
Loading Status FSM 🚥
/// Represents loading status, consisting of multiple steps.
/// All values' types can be specified depending on the problem.
enum AnyLoadingStatus<Loading, Loaded, Failed, Error: Swift.Error> {
case idle
case loading(Loading)
case loaded(Loaded)
case failed(Failed, Error)
}
extension AnyLoadingStatus {
var isIdle: Bool {
guard case .idle = self else { return false }
return true
}
var isLoading: Bool {
guard case .loading = self else { return false }
return true
}
var isLoaded: Bool {
guard case .loaded = self else { return false }
return true
}
var isFailed: Bool {
guard case .failed = self else { return false }
return true
}
var isStarted: Bool {
return !isIdle
}
var isFinished: Bool {
return isLoaded || isFailed
}
}
extension AnyLoadingStatus: Equatable where Loading: Equatable, Loaded: Equatable, Failed: Equatable, Error: Equatable {}
extension AnyLoadingStatus: Hashable where Loading: Hashable, Loaded: Hashable, Failed: Hashable, Error: Hashable {}
extension AnyLoadingStatus where Loading == Void {
static var loading: AnyLoadingStatus {
return .loading(())
}
}
extension AnyLoadingStatus where Loaded == Void {
static var loaded: AnyLoadingStatus {
return .loaded(())
}
}
extension AnyLoadingStatus where Failed == Void {
static func failed(_ error: Error) -> AnyLoadingStatus {
return .failed((), error)
}
}
/// Represents just loading status without any data associated with it.
typealias JustLoadingStatus<Error: Swift.Error> = AnyLoadingStatus<Void, Void, Void, Error>
/// Most commonly used variation with the same value type.
/// Can be treated as a FSM.
///
/// - Loading: Optional type that represents two cases:
/// * nil means we're in a loading state without any successfully preceding case:
///
/// `idle -> loading(nil)`
///
/// `idle -> loading(nil) -> failed(nil, err) -> loading(nil)`
///
/// * value means we're in a loading state with previous successfully loaded value:
///
/// `idle -> loading(nil) -> loaded(value) -> loading(value)`
///
/// - Loaded: Always contains currently loaded value.
///
/// * `idle -> loading(nil) -> loaded(value) -> loading(value) -> loaded(newValue)`
///
/// - Failed: Optional type that represents two cases:
///
/// * nil means we're in a failed state without any successfully preceding case:
///
/// `idle -> loading(nil) -> failed(nil, err)`
///
/// `idle -> loading(nil) -> failed(nil, err) -> loading(nil) -> failed(nil, newErr)`
///
/// * value means we're in a failed state with previous successfully loaded value:
///
/// `idle -> loading(nil) -> loaded(value) -> loading(value) -> failed(value, err)`
///
/// `idle -> loading(nil) -> loaded(value) -> loading(value) -> failed(value, err) -> loading(value) -> failed(value, newErr)`
///
typealias LoadingStatus<Value, Error: Swift.Error> = AnyLoadingStatus<Value?, Value, Value?, Error>
extension AnyLoadingStatus where Loading == Loaded?, Failed == Loaded? {
var value: Loaded? {
get {
switch self {
case .idle: return nil
case let .loaded(value): return value
case let .loading(value), let .failed(value, _): return value
}
}
set {
guard let newValue = newValue else { return }
switch self {
case .idle: return
case .loaded: self = .loaded(newValue)
case .loading: self = .loading(newValue)
case .failed(_, let error): self = .failed(newValue, error)
}
}
}
var loadingValue: Loaded? {
guard case let .loading(value) = self else { return nil }
return value
}
var loadedValue: Loaded? {
guard case let .loaded(value) = self else { return nil }
return value
}
var failedValue: Loaded? {
guard case let .failed(value, _) = self else { return nil }
return value
}
var error: Error? {
guard case let .failed(_, error) = self else { return nil }
return error
}
var finishedResult: Result<Loaded, Error>? {
switch self {
case .idle, .loading: return nil
case let .loaded(value): return .success(value)
case let .failed(_, error): return .failure(error)
}
}
func map<U>(_ transform: (Loaded) -> U) -> LoadingStatus<U, Error> {
switch self {
case .idle: return .idle
case let .loading(value): return .loading(value.map(transform))
case let .loaded(value): return .loaded(transform(value))
case let .failed(value, error): return .failed(value.map(transform), error)
}
}
mutating func startLoading(keepingValue: Bool = true) {
guard !isLoading else { return }
self = .loading(keepingValue ? value : nil)
}
mutating func finish(with result: Result<Loaded, Error>, keepingValueIfFailed: Bool = true, force: Bool = false) {
guard isLoading || force else { return }
switch result {
case let .success(value): self = .loaded(value)
case let .failure(error): self = .failed(keepingValueIfFailed ? value : nil, error)
}
}
}
@a-voronov
Copy link
Author

a-voronov commented Aug 14, 2020

Example

enum MyError: Error { case error }

var status: LoadingStatus<Int, MyError> = .idle
status.startLoading()
status.finish(with: .success(42))

print(status.value)
// prints 42

status.startLoading()
status.finish(with: .failure(.error))

print(status.value)
// prints 42
print(status.error)
// prints MyError.error

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment