`RandomNumberViewModel` with materialization.
This file contains 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
final class RandomNumberViewModel: ObservableObject { | |
private var subscriptions = Set<AnyCancellable>() | |
// MARK: - Inputs | |
let randomNumberPing = PassthroughSubject<Void, Never>() | |
// MARK: - Outputs | |
@Published var dataLoadState = DataLoadState<Int, RandomDotOrgError>.initial /// (1) Boxing our output into a `DataLoadState`. | |
var modalShown: Binding<Bool> { /// (2) We’ll want to expose a binding to our sample view (below) to signal | |
/// to SwiftUI whether or a sheet with random number (or an error) should be shown. | |
Binding( | |
get: { [weak self] in | |
guard let self = self else { return false } | |
switch self.dataLoadState { | |
case .initial, .loading: | |
return false | |
case .loaded, .error: | |
return true | |
} | |
}, | |
set: { [weak self] _ in self?.dataLoadState = .initial } | |
) | |
} | |
init() { | |
let materializedRandomNumbers = randomNumberPing | |
.map { | |
randomNumberPublisher() | |
.materialize() /// (3) To be implemented. We’ll need to lift | |
/// `randomNumberPublisher`’s output and failures into a different shape. | |
} | |
.switchToLatest() | |
.receive(on: DispatchQueue.main) | |
.share() /// (4) Since there will be _two_ subscribers below—the second and third | |
/// variadic arguments to `Publishers.MergeMany`—we’ll want to make sure we `share`. This effectively | |
/// treats the publisher as a reference type, so we don’t fire off two network requests | |
/// (one per subscription). | |
Publishers.MergeMany( /// (5) This portion is dense. It might take staring at it for a few to click, and | |
/// that’s okay (!). To start, we’ll lean on `Publishers.MergeMany` to merge the three publishers | |
/// backing the `.loading`, `.loaded`, and `.error` states (`.initial` is covered by the `@Published` | |
/// property’s initial value and `modalShown`’s `set`). If it helps, I wrote a [note explaining variadic | |
/// merging](https://jasdev.me/notes/merge-many) in my technical diary. | |
randomNumberPing | |
.map { _ in .loading }^, /// (6) Every ping we receive, in turn, kicks off a fetch. So, we can | |
/// map `randomNumberPing` inputs directly to a `.loading` state. The `^` here may seem weird, | |
/// but, it’s to save us repeating `.eraseToAnyPublisher()` multiple times in this expression, | |
/// since each of the arguments to `Publisher.MergeMany.init` must be of the same type. I’ve | |
/// added more details on the operator over at `(12)`. | |
materializedRandomNumbers | |
.values() /// (8) To be implemented. | |
.map(DataLoadState.loaded)^, /// (9) Lifting value events up into the `.loaded` case. | |
materializedRandomNumbers | |
.errors() /// (10) To be implemented. | |
.compactMap { | |
($0 as? RandomDotOrgError) | |
.map(DataLoadState.error) /// (11) Lifting error events up into the `.error` case. | |
}^ | |
) | |
.assign(to: \.dataLoadState, onWeak: self) | |
.store(in: &subscriptions) | |
} | |
} | |
// MARK: - Helpers | |
postfix operator ^ | |
postfix func ^ <Publisher: Combine.Publisher>(_ publisher: Publisher) /// (12) I know, I know. Operators are #holy-war. | |
/// Still, if you’re open to them, this is a single character shorthand for `.eraseToAnyPublisher()`. I walked | |
/// through its worth in more detail in an [earlier note](https://jasdev.me/notes/postfix-erasure). | |
-> AnyPublisher<Publisher.Output, Publisher.Failure> { | |
publisher.eraseToAnyPublisher() | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment