Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
`RandomNumberViewModel` with materialization.
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