Skip to content

Instantly share code, notes, and snippets.

Last active April 9, 2020 18:04
Show Gist options
  • Save jasdev/7736fe314960abdb90c5da539c477469 to your computer and use it in GitHub Desktop.
Save jasdev/7736fe314960abdb90c5da539c477469 to your computer and use it in GitHub Desktop.
`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.
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 {
.materialize() /// (3) To be implemented. We’ll need to lift
/// `randomNumberPublisher`’s output and failures into a different shape.
.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]( in my technical diary.
.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)`.
.values() /// (8) To be implemented.
.map(DataLoadState.loaded)^, /// (9) Lifting value events up into the `.loaded` case.
.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](
-> AnyPublisher<Publisher.Output, Publisher.Failure> {
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment