Start / stop animating activityIndicatorView for many dataTaskPublishers

Posted on

Problem

I consume a RestApi with few Combine Publishers and want to visually represent ongoing progress with UIActivityIndicatorView. My current logic uses handleEvents->receiveSubscription closure to start an animation and handleEvents->receiveCancel and sink->completion to stop an animation of the activityIndicatorView.
I ask for a review because it looks strange that a stop animation must be called from two different closures which looks like I am missing something

    let first = URLSession.shared.dataTaskPublisher(for: .init(string: "http://httpbin.org/delay/10")!)
        .tryMap { (data: Data, response: URLResponse) in
            return data
        }
    
    let second = URLSession.shared.dataTaskPublisher(for: .init(string: "http://httpbin.org/delay/5")!)
        .tryMap { (data: Data, response: URLResponse) in
            return data
        }
    
    cancellable = Publishers.Zip(first,second)
        .flatMap { _ in
            first
        }
        .handleEvents(receiveSubscription: { subscription in
            print("receiveCancel: activityIndicatorView.startAnimating()")
        }, receiveCancel: {
            print("receiveCancel: activityIndicatorView.stopAnimating()")
        })
        .sink { _ in
            print("sink completion: activityIndicatorView.stopAnimating()")
        } receiveValue: { _ in }

Solution

You have not missed anything. I was surprised when I learned about this too. Consider implementing the using operator from RxSwift. I have already submitted a pull request to CombineExt with the operator.

Once you have implemented the Using type, you can also implement an ActivityTracker

Then your code sample becomes:

let activityTracker = ActivityTracker()

let first = URLSession.shared.dataTaskPublisher(for: .init(string: "http://httpbin.org/delay/10")!)
    .tryMap { (data: Data, response: URLResponse) in
        return data
    }
    .trackActivity(activityTracker)

let second = URLSession.shared.dataTaskPublisher(for: .init(string: "http://httpbin.org/delay/5")!)
    .tryMap { (data: Data, response: URLResponse) in
        return data
    }
    .trackActivity(activityTracker)

Publishers.Zip(first, second)
    .flatMap { _ in first }
    .sink(
        receiveCompletion: { complete in
            print("Handle error but otherwise don't do anything.")
        },
        receiveValue: { values in
            print("Use data here.")
        }
    )
    .store(in: &cancellables)

activityTracker.isActive
    .sink(receiveValue: { isActive in
        if isActive {
            activityIndicatorView.startAnimating()
        }
        else {
            activityIndicatorView.stopAnimating()
        }
    })
    .store(in: &cancellables)

Your activity tracking stream is now it’s own thing. No need to retain the activity tracker so it doesn’t need to be a property of a class. The publisher chain will retain it and release it when appropriate.

Also, the way your code is constructed, "http://httpbin.org/delay/10" is being called twice. Is that intentional? It seems rather odd.

Leave a Reply

Your email address will not be published. Required fields are marked *