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.