RxSwift `ActivityIndicator` Functionality in Combine

Viewed 604

I've been working with RxSwift for a few years now, and am starting to explore Combine with SwiftUI and am having some trouble trying to replicate some functionality from RxSwift in Combine.

On the RxSwift GitHub there is an example in a file called ActivityIndicator.swift.

Basic usage is as follows:

class Foo {
  let activityIndicator = ActivityIndicator()

  lazy var activity = activityIndicator.asDriver()

  var disposeBag = DisposeBag()

  func doSomething() {
    Observable
      .just("this is something")
      .trackActivity(activityIndicator)
      .subscribe()
      .disposed(by: disposeBag)
  }
}

What this does is allow you to then drive off of the activity driver and it will emit boolean values every time something subscribes or a subscription completes.

You can then directly drive something like a UIActivityIndicatorView's isAnimating property using RxCocoa.

I've been trying to figure out how to create something similar to this in Combine but am not having any luck.

Say I have a viewModel that looks like this:

class ViewModel: ObservableObject {
  @Published var isActive = false

  func doSomething() -> AnyPublisher<Void, Never> {
    Just(())
      .delay(for: 2.0, scheduler: RunLoop.main)
      .eraseToAnyPublisher()
  }
}

What I would like to do is create an operator for a Publisher that will function similarly to how the Rx operator worked where I can forward the events from the subscription through the chain, but change the isActive value every time something subscribes/completes/cancels.

In the SwiftUI View I would initiate the doSomething function and sink to it, while also being able to use the published isActive property to show/hide a ProgressView

Something similar to this:

struct SomeView: View {
  let viewModel = ViewModel()

  var body: some View {
    var cancelBag = Set<AnyCancellable>()

    VStack {
      Text("This is text")

      if viewModel.isActive {
        ProgressView()
      }
    }
    .onAppear(perform: {
      viewModel
        .doSomething()
        .sink()
        .store(in: &cancelBag)
    })
  }
}

Is there something that works like this already that I am just completely missing?

If not, how can I go about replicating the RxSwift functionality in Combine?

Thank you in advance for the help.

2 Answers

Hmm... The key to the ActivityIndicator class is the Observable.using(_:observableFactory:) operator. Unfortunately, I don't believe there is an equivalent operator in Combine.

The using operator creates a resource when the Observable is subscribed to, and then disposes the resource when the Observable sends a stop event (complete or error.) This insures the resource's lifetime. In this particular case, the resource just increments an Int value on creation and decrements it on disposal.

I think you could kind of mimic the behavior with something like this:

extension Publisher {
    func trackActivity(_ activityIndicator: CombineActivityIndicator) -> some Publisher {
        return activityIndicator.trackActivity(of: self)
    }
}

final class CombineActivityIndicator {
    var counter = CurrentValueSubject<Int, Never>(0)
    var cancelables = Set<AnyCancellable>()

    func trackActivity<Source: Publisher>(of source: Source) -> some Publisher {
        let sharedSource = source.share()
        counter.value += 1
        sharedSource
            .sink(
                receiveCompletion: { [unowned self] _ in
                    self.counter.value -= 1
                },
                receiveValue: { _ in }
            )
            .store(in: &cancelables)
        return sharedSource
    }

    var asPublisher: AnyPublisher<Bool, Never> {
        counter
            .map { $0 > 0 }
            .eraseToAnyPublisher()
    }
}

However, the above class will heat up the Publisher and you might miss emitted values because of it. Use at your own risk, I do not recommend the above unless you are desperate.

Maybe someone has written a using operator for Publisher and will be willing to share.

Related