Using RxAlamofire to create Observable contain the result of network request - ios

I'm trying to work with RxAlamofire to wrap a network request result.
My objective is to fire request, handle JSON response and create and Observable that contain either network operation success or any error occur.
In other place I can call the function that create the Observable and subscribe to it and notify user whether it is success or failure with error message.
My implementation is below:
func discoverMovieList(for url: String, withPagg page: Int) -> Observable<Any> {
let requestUrl = "\(url)&page=\(page)"
return RxAlamofire.json(.get, requestUrl)
.map{ jsonResponse in
self.createOrUpdateMoviesList(from: JSON(jsonResponse))
}
}
How can we correct the code and how we call it from other place to notify the result of the process?

Observable define multiple lifecycle callback, that you provide using the subscribe method. In this instance, usage would be something like this:
discoverMovieList(for: "http://some.url", withPagg: 2)
.subscribe(
onNext: { movies in
uiElement.string = "Movie list received"
},
onError: { error in
uiElement.string = "Something went wrong"
}
)
.disposed(by: disposeBag)
disposeBag is used to hold the subscription, so when disposeBag is released, the subscription is cancelled (in our case, the network call would be aborted).

Related

iOS Combine Start new request only if previous has finished

I have network request that triggers every last cell in switui appearas. Sometimes if user scrolls fast enough down -> up -> request will trigger before first one finishes. Without combine or reactive approach I have completion block and bool value to handle this:
public func load() {
guard !isLoadingPosts else { return }
isLoadingPosts = true
postsDataProvider.loadMorePosts { _ in
self.isLoadingPosts = false
}
}
I was wondering if with combine this can be resolved more elegantly, without the need to use bool value. For example execute request only if previous has finished?
It looks like you want to skip making the call if it's already in progress.
Since you didn't share any of the Combine code you might have, I'll assume that you have a publisher-returning function like this:
func loadMorePosts() -> AnyPublisher<[Post], Error> {
//...
}
Then you can use a subject to initiate a load call, a flatMap(maxPublishers:_:) downstream, with a number of publishers limited to 1:
let loadSubject = PassthroughSubject<Void, Never>()
loadSubject
.flatMap(maxPublishers: .max(1)) {
loadMorePosts()
}
.sink(
receiveCompletion: { _ in },
receiveValue: { posts in
// update posts
})
.store(in: &cancellables)
The above pipeline subscribes to the subject, but if another value arrives before flatMap is ready to receive it, it would simply be dropped.
Then the load function becomes:
func load() {
loadSubject.send(())
}

Rx: How to modify a shared source observable within a retry

Top Level Question:
I want to know how, within a retry, I can modify its source observable if it is an observable shared between multiple subscribers (in this case a BehaviorSubject/Relay).
Solution(s) I have considered:
The suggestion of using defer from this post doesn't seem to naturally port over if the source observable needs to be shared.
Use case (to fully elaborate the question)
Say I have a server connection object that, when initialized, connects to an url. Once it is created, I can also use it to get a data stream for a particular input.
class ServerConnection {
var url: URL
init(url: URL)
func getDataStream(input: String) -> Observable<Data> // the observable also errors when the instance is destroyed.
}
However, one particular url or another may be broken or overloaded. So I may want to obtain the address of a mirror and generate a new ServerConnection object. Let's say I have such a function.
// At any point in time, gets the mirror of the url with the lowest load
func getLowestLoadMirror(url: URL) -> URL {}
Ideally, I want this "mirror url" switching should be an implementation detail. The user of my code may only care about the data they receive. So we would want to encapsulate this logic in a new class:
class ServerConnectionWithMirrors {
private var currentConnection: BehaviorRelay<ServerConnection>
init(startingURL: URL)
func dataStream(for inputParams: String) -> Observable<Data>
}
// usage
let connection = ServerConnectionWithMirrors(startingURL: "www.example.com")
connection.dataStream(for: "channel1")
.subscribe { channel1Data in
// do something with channel1Data
}.disposed(by: disposeBag)
connection.dataStream(for: "channel2")
.subscribe { channel2Data in
// do something with channel2Data
}.disposed(by: disposeBag)
How should I write the dataStream() function for ServerConnectionWithMirrors? I should be using retries, but I need to ensure that the retries, when faced with a particular error (ServerOverLoadedError) update the value on the behaviorRelay.
Here is code that I have so far that demonstrates the crux at what I am trying to do. One problem is that multiple subscribers to the behaviorRelay may all update it in rapid succession when they get an error, where only one update would do.
func dataStream(for inputParams: String) -> Observable<Data> {
self.currentConnection.asObservable()
.flatMapLatest { server in
return server.getDataStream(input: inputParams)
}
.retryWhen { errors in
errors.flatMapLatest { error in
if error is ServerOverLoadedError {
self.currentConnection.accept(ServerConnection(url: getLowestLoadURL()))
} else {
return Observable.error(error)
}
}
}
}
The answer to your top level question:
I want to know how, within a retry, I can modify its source observable if it is an observable shared between multiple subscribers (in this case a BehaviorSubject/Relay).
You cannot modify a retry's source observable from within the retry. (full stop) You cannot do this whether it is shared or not. What you can do is make the source observable in such a way that it naturally updates its data for every subscription.
That is what the question you referred to is trying to explain.
func getData(from initialRequest: URLRequest) -> Observable<Data> {
return Observable.deferred {
var correctRequest = initialRequest
let correctURL = getLowestLoadMirror(url: initialRequest.url!)
correctRequest.url = correctURL
return Observable.just(correctRequest)
}
.flatMapLatest {
getDataFromServer(request: $0)
}
.retryWhen { error in
error
.do(onNext: {
guard $0 is ServerOverloadedError else { throw $0 }
})
}
}
With the above code, every time deferred is retried, it will call its closure and every time its closure is called, the URL will the lowest load will be used.

Emitting progress items while forwarding result in a single observable

I am initiating an operation via a REST API in two steps:
Start operation and return a task id
Poll task with the given id and complete the sequence when the operation returns complete.
The polling the task id will return a 202 which indicates that the operation is still in progress and a 200 when it completes. Any other code is an error.
I need to communicate to the subscribers the response of each step.
Previously, I would have the do operator push the response in between steps to a ReplaySubject.
startReboot()
.do(onNext: { response in
operationStatus.next(response)
)
.flatMap({ response in
// If we could not get the task ID from the response we error
guard let taskID = getTaskIDFromJSON(response) else { return Observable.error(API.serverError) }
return Observable.just(taskID)
})
.flatMap({ taskID in
return pollTask(withID: taskID) // internally, it uses retryWhen to check the api again with a five second delay
})
.do(onNext: { response in
operationStatus.next(response)
})
.subscribe(onError: { _ in
showOperationFailedIcon()
}, onCompleted: {
showOperationCompleteIcon()
})
And somewhere else, a subscriber to the subject would do the following:
operationStatus.subscribe(onNext: { response
showResponse(response)
})
So essentially I am showing the progress of the operation and the response we get from each step at the same time.
At the time I was not familiar with Rx to come up with a cleaner solution. But now that I have familiarized my self with it, it seems to me that there should be a solution where we don't use side effects and contain this to a single final observable. Still, I cannot find a way to do it.
I was thinking about something like this:
let opObs = startReboot()
let pollingObs = pollTask(/* where does the id come from? */)
Observable.concat(opObs, pollingObs)
.subscribe(onNext : { response in
showResponse(response)
}, onError: { _ in
showOperationFailedIcon()
}, onCompleted: {
showOperationCompleteIcon()
})
But that would imply that once opObs is done I would need to save the task id in variable outside of the monad and wrap pollingObs to fetch it when it starts - once again introducing side effects.
Is there an operator or combination of operators that I can use to emit the response of each step to a subscriber and also pass it to another observable?
Something like this should work. Note the use of share() to avoid triggering twice the startReboot sequence.
let opObs = startReboot().share()
let pollingObs = opObs.flatMap {
guard let taskID = getTaskIDFromJSON(response) else { return Observable.error(API.serverError)
return pollTask(withID: taskID)
}
Observable.concat(opObs, pollingObs)
.subscribe(onNext : { response in
showResponse(response)
}, onError: { _ in
showOperationFailedIcon()
}, onCompleted: {
showOperationCompleteIcon()
})

How to cancel multiple networking requests using Moya

I am currently using Moya to structure my networking calls. Per their docs, I have configured it as the following:
enum SomeAPIService {
case endPoint1(with: Object)
case endPoint2(duration: Int)
}
When calling an endpoint (in this case, endPoint1), I do the following:
let provider = MoyaProvider<SomeAPIService>()
provider.request(.endPoint1(object: object)) { (result) in
switch result {
case let .success(moyaResponse):
finished(Result.success(value: moyaResponse.value))
case let .failure(error):
let backendError = BackendError(localizedTitle: "", localizedDescription: "Some error", code: moyaResponse.statusCode)
finished(Result.failure(error: backendError))
}
})
My goal is, upon the user performing an action, cancel all the networking requests that's happening.
Accordingly, Moya does allow one to cancel requests from the discussion here. From the most upvoted comment, someone mentioned let request_1 = MoyaRequestXXXXX and then ruest_1.cancel()
My problem is:
How would I keep pointer to the requests?
provider doesn't have a cancel() function - so how should I be calling it?
Any help is much appreciated.
Edit:
Per the helpful suggestion about using [Cancellable], I did the following:
(1) In my app's singleton instance called Operator, I added var requests = [Cancellable]()
(2) Every API call is added to the requests array as a Cancellable, like so:
let provider = MoyaProvider<SomeAPIService>()
Operator.shared.requests.append(provider as! Cancellable) //causing error
provider.request(.endPoint1(object: object)) { (result) in
//rest of the block omitted
I think I am not getting the syntax correct, and am adding the provider and not the request. However, since the request is itself a block, where would be the place to add the request?
The request method returns a Cancellable. From the documentation we can read:
The request() method returns a Cancellable, which has only one public function, cancel(), which you can use to cancel the request.
So according to this, I made a simple test and call:
var requests: [Cancellable] = []
#objc func doRequests() {
for i in 1...20 {
let request = provider.request(MyApi.someMethod) {
result in
print(result)
}
requests.append(request)
}
requests.forEach { cancellable in cancellable.cancel() } // here I go through the array and cancell each request.
requests.removeAll()
}
I set up a proxy using Charles and it seems to be working as expected. No request was sent - each request was cancelled.
So, the answer to your questions is:
You can keep it in [Cancellable] array.
Go through the array and cancel each request that you want to cancel.
EDIT
The main problem is that you adding the provider to the array and you try to map provider as Cancellable, so that cause the error.
You should add reqest to the array. Below you can see the implementation.
let provider = MoyaProvider<SomeAPIService>()
let request = provider.request(.endPoint1(object: object)) { // block body }
Operator.shared.requests.append(request)
//Then you can cancell your all requests.
I would just cancel the current provider session + tasks:
provider.manager.session.invalidateAndCancel()

Make all endpoints to wait one exact endpoint

I am using Moya to handle HTTP operations and normally I have an refreshToken(). I am checking token if expired or not when a request is about happen but the problem is there can be a scenarios that more than one requests. If they are chained with nested types it is not a problem however, it is not likely all the time.
To be more clear lets say I have request1() and request2() and assume that they execute separate operations and can be triggered anytime(for instance one is called in a viewDidLoad(), other one is called in another viewDidLoad()). when this happens and if the token is expired, my refresh request fails. (statusCode: 400) So, my question is, how can I make provider to wait refresh() operation get done?I mean by provider is other endpoints. I want them to wait refresh() endpoint if it is on.
I will be very appreciated if you suggest a way that will make this easier.
I just set an variable called isTokenRefreshing true when i start the refresh() operation and checked it before making a request. If it was true I stored all the requests in an array and when the refresh() is finished I executed another function which basically makes all the stored requests in a for loop.
If anyone wants to see the code I can share. Just let me know.
EDIT
This where I, NetworkManager, handle all my requests. It is in an Singleton class.
private var awaitingRequests : [NetworkAPI] = []
func makeRequest(_ request: NetworkAPI){
if (Token.sharedInstance.isTokenRefreshing && request.requiresToken) {
self.awaitingRequests.append(request)
return
}
self.provider.request(){ result in ... }
}
func executeWaitedRequests(){
for request in self.awaitingRequests {
self.makeRequest(request)
}
}
NetworkAPI is main enum that I hold my endpoint cases. See the Moya documents if you do not what I am talking about.
And this is where I handle my Token operations.
class Token {
static let sharedInstance = Token()
private init(){}
var isTokenRefreshing: Bool = false
func refresh(_ completion: #escaping ()->()){
self.isTokenRefreshing = true
print("refreshing token")
let queue = DispatchQueue(label: "com.asd.ads.makeRequest", attributes: DispatchQueue.Attributes.concurrent)
queue.sync(flags: .barrier, execute: {
NetworkManager.shared.makeRequest(.refresh(), completionHandler: { (success, error) in
self.isTokenRefreshing = false
if success{
completion()
NetworkManager.shared.executeWaitedRequests()
}
print("refrehing ended!")
})
})
}
}

Resources