I am relatively new to RxSwift, and functional programming for that matter. I have an app where I need to make a sequential set of network calls, each call depends upon output of the other. How I have architected the app, these calls are not made anywhere close to the view or view controllers - they are handled by data provider classes which perform business logic on the response data. As I understand good Rx design, subscribers should stay at the top of chain, not in classes like these that purely perform business and functional logic on the data.
I am creating a contrived and simplified example for illustration from these requests that return Singles:
func loginRequest(_ data: LoginData) -> Single<LoginResponse> {
return Single.create { observer -> Disposable in
let request = LoginRequestWith(data)
networkClient.send(request) { result in
switch result {
case .success(let response):
observer(.success(response))
case .failure(let error):
observer(.error(error))
}
}
return Disposables.create {
networkClient.cancelRequest()
}
}
}
func userRequest(_ data: UserData) -> Single<UserResponse> {
return Single.create { observer -> Disposable in
let request = InfoRequestWith(data)
networkClient.send(request) { result in
switch result {
case .success(let response):
observer(.success(response))
case .failure(let error):
observer(.error(error))
}
}
return Disposables.create {
networkClient.cancelRequest()
}
}
}
func infoRequest(_ data: InfoData) -> Single<InfoResponse> {
return Single.create { observer -> Disposable in
let request = InfoRequestWith(data)
networkClient.send(request) { result in
switch result {
case .success(let response):
observer(.success(response))
case .failure(let error):
observer(.error(error))
}
}
return Disposables.create {
networkClient.cancelRequest()
}
}
}
These requests are working as intended, however I am calling and handling them improperly using subscriptions / dispose bags in the provider classes to get them to work. What I want to do is chain them together using a FlatMap or more appropriate chain of operators. And this is where I get stuck.
func login() -> Observable<InfoData> {
webServices.loginRequest(loginData)
// verify success?
.flatMap { result in
// Now call userRequest passing the response from the first
// and from that infoRequest passing the response from the second
// Which should return the Observable<InfoData>
}
}
Where I am messing up? I get errors around the FlatMap but I understand that these are best to use with Singles. My errors seem to revolve around not mapping the response correctly to a new observable:
webServices.loginRequest(loginData)
.flatMap { response -> PrimitiveSequence<SingleTrait, Result> in
return response
}
Xcode Error: Reference to generic type 'Result' requires arguments in
<...>
And then how do I chain call into the subsequent request after a previous one. Thank you very much for any assistance.
Related
Let's say we have this pseudocode representing a network request call and show/hide an activity indicator, using RxSwift:
func performRequest() {
isLoading.accept(true)
self.network.executeRequest()
.subscribe(onNext: {
self.isLoading.accept(false)
}, onError: {
self.isLoading.accept(false)
})
}
The function executeRequest returns either an Observable or Single.
I am not feeling comfortable with having to write twice the same code, for onNext/onSuccess and onError, basically doing the same.
I am looking for suggestions to minimize/improve turning off the activity indicator, like for example handling all events of the request in a single statement and avoid using the subscribe function. Or maybe there are other suggestions?
I use ActivityIndicator from RxSwift Example app, which makes it really convenient, especially if your loading multiple things in parallel as it maintains a count of running subscriptions and emit false only when this count is equal to 0:
let isLoading = ActivityIndicator()
func performRequests() {
self.network
.executeFirstRequest()
.trackActivity(isLoading)
.subscribe {
// ...
}
self.network
.executeSecondRequest()
.trackActivity(isLoading)
.subscribe {
// ...
}
}
You can use another method to subscribe, which passes Event in case of Observer or SingleEvent in case of Single:
subscribe(on: (Event<T>) -> Void)
subscribe(observer: (SingleEvent<T>) -> Void)
Observer Example:
func performRequest() {
isLoading.accept(true)
self.network.executeRequest().subscribe {
switch $0 {
case let .error(error):
print(error)
case let .next:
print("good")
case .completed:
print("also good")
}
isLoading.accept(false)
}
}
Single Example:
func performRequest() {
isLoading.accept(true)
self.network.executeRequest().subscribe {
switch $0 {
case let .error(error):
print(error)
case let .next:
print("good")
}
isLoading.accept(false)
}
}
I use the following piece of code to generate a cold RxSwift Observable:
func doRequest<T :Mappable>(request:URLRequestConvertible) -> Observable<T> {
let observable = Observable<T>.create { [weak self] observer in
guard let self = self else { return Disposables.create() }
self.session.request(request).validate().responseObject { (response: AFDataResponse<T>) in
switch response.result {
case .success(let obj):
observer.onNext(obj)
observer.onCompleted()
case .failure(let error):
let theError = error as Error
observer.onError(theError)
}
}
return Disposables.create()
}
return observable
}
where Mappable is an ObjectMapper based type, and self.session is an Alamofire's Session object.
I can't find an equivalent to Observable.create {...} in Apple's Combine framework. What I only found is URLSession.shared.dataTaskPublisher(for:) which creates a publisher using Apple's URLSession class.
How can I convert the above observable to an Alamofire Combine's publisher ?
EDIT:
using the solution provided by rob, I ended up with the following:
private let apiQueue = DispatchQueue(label: "API", qos: .default, attributes: .concurrent)
func doRequest<T>(request: URLRequestConvertible) -> AnyPublisher<T, AFError> where T : Mappable {
Deferred { [weak self] () -> Future<T, AFError> in
guard let self = self else {
return Future<T, AFError> { promise in
promise(.failure(.explicitlyCancelled)) }
}
return Future { promise in
self.session
.request(request)
.validate()
.responseObject { (response: AFDataResponse<T>) in
promise(response.result)
}
}
}
.handleEvents(receiveCompletion: { completion in
if case .failure (let error) = completion {
//handle the error
}
})
.receive(on: self.apiQueue)
.eraseToAnyPublisher()
}
EDIT2: I have to remove the private queue since it's not needed, Alamofire does the parsing the decoding on its own, so remove the queue and its usages (.receive(on: self.apiQueue))
You can use Future to connect responseObject's callback to a Combine Publisher. I don't have Alamofire handy for testing, but I think the following should work:
func doRequest<T: Mappable>(request: URLRequestConvertible) -> AnyPublisher<T, AFError> {
return Future { promise in
self.session
.request(request)
.validate()
.responseObject { (response: AFDataResponse<T>) in
promise(response.result)
}
}.eraseToAnyPublisher()
}
Note that this is somewhat simpler than the RxSwift version because promise takes a Result directly, so we don't have to switch over response.result.
A Future is sort of a “lukewarm” publisher. It is like a hot observable because it executes its body immediately and only once, so it starts the Alamofire request immediately. It is also like a cold observable, because every subscriber eventually receives a value or an error (assuming you eventually call promise). The Future only executes its body once, but it caches the Result you pass to promise.
You can create a truly cold publisher by wrapping the Future in a Deferred:
func doRequest<T: Mappable>(request: URLRequestConvertible) -> AnyPublisher<T, AFError> {
return Deferred {
Future { promise in
self.session
.request(request)
.validate()
.responseObject { (response: AFDataResponse<T>) in
promise(response.result) }
}
}.eraseToAnyPublisher()
}
Deferred calls its body to create a new inner Publisher every time you subscribe to it. So each time you subscribe, you'll create a new Future that will immediately start a new Alamofire request. This is useful if you want to use the retry operator, as in this question.
I wanted to fetch data from the server api.
The issues is that all networking frameworks are doing it Async.
So I have issues that return variable return empty Here is my code.
The view controller where I call the function
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
let url = "http://api.musixmatch.com/ws/1.1/track.lyrics.get?track_id=12693365&apikey=63ee7da5e2ee269067ecc42b25590922"
let musixrequest = MusicMatchRequest()
let endResults = musixrequest.gettingLyrics(url: url)
if !endResults.isEmpty{
print("The end results are \(endResults)")
}else{
print("No results found")
}
}
Here is my class where I am trying to fetch the data
public class MusicMatchRequest : NSObject{
public override init(){}
public func gettingLyrics(url : String) -> String {
var endResults = ""
DefaultProvider.request(Route(path:"\(url)")).responseJSON { (response:Response<Any>) in
switch response.result{
case .success(let json):
endResults = String(describing:json)
print(endResults)
case .failure(let error):
print("error: \(error)")
}
}
return endResults
}
}
When I am printing the endRsults from the task it is working It print the results but the var endResults return empty.
Idea how to transfer the data .
I have tried two frameworks
Alamofire
Nikka
In both frameworks it's acting the same .
Solution
I don't exactly know what happens under the hood, but as ANY network operation this also has to be asynchronous (meaning it will take a certain amount of time to fetch the data).
let endResults = musixrequest.gettingLyrics(url: url)
If it's synchronously done on the Main thread, it will block it so the user can't interact with the app, which is pretty bad. Given it's asynchronous in your code you read the value in the very next line immediately, here:
if !endResults.isEmpty {
print("The end results are \(endResults)")
} else {
print("No results found")
}
It's very unlikely that the network operation will finish in one line step time, so you won't have the data there.
What you should do is to pass a completion handler in this method:
public func gettingLyrics(url : String) -> String
and dispatch to main thread like this:
DispatchQueue.main.async {
// do you UI stuff here
}
Change you function to this:
public func gettingLyrics(url : String, completionHandler: (String) -> Void)
and call the completion handler in the success branch:
completionHandler(String(describing:json))
I am using Firebase FirAuth API and before the API return result, Disposables.create() has been returned and it's no longer clickable (I know this might due to no observer.onCompleted after the API was called. Is there a way to wait for it/ listen to the result?
public func login(_ email: String, _ password: String) -> Observable<APIResponseResult> {
let observable = Observable<APIResponseResult>.create { observer -> Disposable in
let completion : (FIRUser?, Error?) -> Void = { (user, error) in
if let error = error {
UserSession.default.clearSession()
observer.onError(APIResponseResult.Failure(error))
observer.on(.completed)
return
}
UserSession.default.user.value = user!
observer.onNext(APIResponseResult.Success)
observer.on(.completed)
return
}
DispatchQueue.main.async {
FIRAuth.auth()?.signIn(withEmail: email, password: password, completion: completion)
}
return Disposables.create()
}
return observable
}
You are correct in your assumption that an onError / onCompletion event terminate the Observable Sequence. Meaning, the sequence won't emit any more events, in any case.
As a sidenote to that, You don't need to do .on(.completed) after .onError() , since onError already terminates the sequence.
the part where you write return Disposables.create() returns a Disposable object, so that observable can later be added to a DisposeBag that would handle deallocating the observable when the DisposeBag is deallocated, so it should return immediately, but it will not terminate your request.
To understand better what's happening, I would suggest adding .debug() statements around the part that uses your Observable, which will allow you to understand exactly which events are happening and will help you understand exactly what's wrong :)
I had the same issue some time ago, I wanted to display an Alert in onError if there was some error, but without disposing of the observable.
I solved it by catching the error and returning an enum with the cases .success(MyType) and .error(Error)
An example:
// ApiResponseResult.swift
enum ApiResponseResult {
case error(Error)
case success(FIRUser)
}
// ViewModel
func login(...) -> Observable<ApiResponseResult> {
let observable = Observable.create { ... }
return observable.catchError { error in
return Observable<ApiResponseResult>.just(.error(error))
}
}
// ViewController
viewModel
.login
.subscribe(onNext: { result in
switch result {
case .error(let error):
// Alert or whatever
break
case .success(let user):
// Hurray
break
}
})
.addDisposableTo(disposeBag)
I'm having some trouble with an HTTP request performed from our iOS app to our API. The problem with this request is that it usually takes 30-40s to complete. I don't need to handle the response for now, so I just need to fire it and forget about it. I don't know if the problem is in my code or in the server, that's why I'm asking here.
I'm using Alamofire and Swift 2.2 and all the other requests are working just fine. This is an screenshot from Charles proxy while I was trying to debug it: Charles screenshot
As you can see, the request that blocks the others is the refreshchannels. When that request fires (#6 and #25), the others are blocked and don't finish until the refreshchannels finishes.
Here is the code that triggers that request and also the APIManager that i've built on top of Alamofire:
// This is the method that gets called when the user enables the notifications in the AppDelegate class
func application(application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: NSData) {
// Recieve the APNSToken and handle it. I've removed it to make it shorter
// This sends a POST to our API to store some data
APIManager().registerForPushNotifications(parametersPush) { (result) in
switch result {
case .Success (let JSON):
// This is the slow call that blocks the other HTTP requests
APIManager().refreshChannels { _ in } // I don't need to handle the response for now
case .Failure: break
}
}
}
And the manager:
//This is my custom manager to handle all the networking inside my app
class APIManager {
typealias CompletionHandlerType = (Result) -> Void
enum Result {
case Success(AnyObject?)
case Failure(NSError)
}
let API_HEADERS = Helper.sharedInstance.getApiHeaders()
let API_DOMAIN = Helper.sharedInstance.getAPIDomain()
//MARK: Default response to a request
func defaultBehaviourForRequestResponse(response: Response<AnyObject, NSError>, completion: CompletionHandlerType) {
print("Time for the request \(response.request!.URL!): \(response.timeline.totalDuration) seconds.")
switch response.result {
case .Success (let JSON):
if let _ = JSON["error"]! {
let error = NSError(domain: "APIError", code: response.response!.statusCode, userInfo: JSON as? [NSObject : AnyObject])
completion(Result.Failure(error))
} else {
completion(Result.Success(JSON))
}
case .Failure (let error):
completion(Result.Failure(error))
}
}
func refreshChannels(completion: CompletionHandlerType) {
Alamofire.request(.PUT, "\(API_DOMAIN)v1/user/refreshchannels", headers: API_HEADERS).responseJSON { response in
self.defaultBehaviourForRequestResponse(response, completion: completion)
}
}
}
Any help will be appreciated. Have a nice day!