RxSwift equivalent for switchmap - swift

In RxJS you can use the value from a observable for a new observable. For example:
this.authService.login(username, password).pipe(
switchMap((success: boolean) => {
if(success) {
return this.contactService.getLoggedInContact()
} else {
return of(null)
}
})
).subscribe(contact => {
this.contact = contact
})
But now I have to do a project in Swift and I want to achieve the same thing. I can get the two methods working, but using the result of the first observable for the second observable is something i can't get working. The switchMap pipe is something that does not exist in RxSwift and I cannot find the equivalent.
I've tried mapping the result of the login function to the observable and then flatmapping it, but unfortunately that didn't work.
What is the best way to do this in Swift without using a subscribe in a subscribe?
EDIT I've tried flat map:
APIService.login(email: "username", password: "password")
.flatMapLatest { result -> Observable<Contact> in
if result {
return APIService.getLoggedInContact()
} else {
return .of()
}
}.subscribe(onNext: {result in
print("Logged in contact: \(result)")
}, onError: {Error in
print(Error)
}).disposed(by: disposeBag)
But unfortunately that didn't work, I get an error Thread 1: EXC_BAD_ACCESS (code=1, address=0x13eff328c)
EDIT2:
This is the login function
static func login(email: String, password: String) -> Observable<Bool> {
return Observable<String>.create { (observer) -> Disposable in
Alamofire.request(self.APIBASEURL + "/contact/login", method: .post, parameters: [
"email": email,
"password": password
], encoding: JSONEncoding.default).validate().responseJSON(completionHandler: {response in
if (response.result.isSuccess) {
guard let jsonData = response.data else {
return observer.onError(CustomError.api)
}
let decoder = JSONDecoder()
let apiResult = try? decoder.decode(ApiLogin.self, from: jsonData)
return observer.onNext(apiResult!.jwt)
} else {
return self.returnError(response: response, observer: observer)
}
})
return Disposables.create()
}.map{token in
return KeychainWrapper.standard.set(token, forKey: "authToken")
}
}
This is the getLoggedInContact function
static func getLoggedInContact() -> Observable<Contact> {
return Observable.create { observer -> Disposable in
Alamofire.request(self.APIBASEURL + "/contact/me", method: .get, headers: self.getAuthHeader())
.validate().responseJSON(completionHandler: {response in
if (response.result.isSuccess) {
guard let jsonData = response.data else {
return observer.onError(CustomError.api)
}
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .formatted(.apiNewsDateResult)
let apiResult = try? decoder.decode(Contact.self, from: jsonData)
return observer.onNext(apiResult!)
} else {
return self.returnError(response: response, observer: observer)
}
})
return Disposables.create()
}
}

There is operator flatMapLatest which does exactly the same as switchMap in RxJS.
You can find usage example here

Related

Does Swift task run first or print() first when I tap my UIButton?

I am trying to understand what is going on in my code here.
I have a simple API call to open weahter API and that whenever the user taps the UIButton, it should call the api and get the data back from open weather.
Everything works as intended however, when I have my UIButton pressed, the print statement executed first before the Task closure. I'm trying to understand the race condition here
This is my code in viewController:
#IBAction func callAPIButton(_ sender: UIButton) {
Task {
let weatherData = await weatherManager.fetchWeather(cityName: "Seattle")
}
}
Here's the code for fetching the API:
struct WeatherManager{
let weatherURL = "https://api.openweathermap.org/data/2.5/weather?appid=someAPIKeyHere"
func fetchWeather(cityName: String) -> WeatherModel? {
let urlString = "\(weatherURL)&q=\(cityName)"
let requestResult = performRequest(urlString: urlString)
return requestResult
}
func performRequest(urlString: String) -> WeatherModel? {
var weatherResult : WeatherModel? = nil
if let url = URL(string: urlString){
let session = URLSession(configuration: .default)
let task = session.dataTask(with: url, completionHandler: {
(data, response, error) in
if error != nil {
return
}
if let safeData = data {
weatherResult = parseJSON(weatherData: safeData)
}
})
task.resume()
}
return weatherResult
}
func parseJSON(weatherData: Data) -> WeatherModel?{
let decoder = JSONDecoder()
do {
let decodedData = try decoder.decode(WeatherResponse.self, from: weatherData)
print("this is in decodedData: \(decodedData)")
let temp = decodedData.main.temp
let name = decodedData.name
let weather = WeatherModel(conditionId:300, cityName: name, temperature: temp)
return weather
} catch {
print("Something is wrong here: " + error.localizedDescription)
}
return nil
}
}
Here's my Model:
struct WeatherModel{
let conditionId: Int
let cityName: String
let temperature: Double
var temperatureString: String{
return String(format: "%.1f", temperature)
}
var conditionName: String {
switch conditionId {
case 200...232:
return "cloud.bolt"
case 300...321:
return "cloud.drizzle"
case 500...531:
return "cloud.rain"
case 600...622:
return "cloud.snow"
case 701...781:
return "cloud.fog"
case 800:
return "sun.max"
case 801...804:
return "cloud.bolt"
default:
return "cloud"
}
}
}
Desired result:
This is in weatherData: WeatherResponse(name: "Seattle", weather: [Awesome_Weather_App.WeatherAPI(description: "overcast clouds", icon: "04d")], main: Awesome_Weather_App.MainAPI(temp: 287.81, pressure: 1018.0, humidity: 44.0, temp_min: 284.91, temp_max: 290.42, feels_like: 286.48), sys: Awesome_Weather_App.SysAPI(sunrise: 1.6712886e+09, sunset: 1.6713243e+09))
This is what I am getting instead:
This is in weatherData: nil
this is in decodedData: WeatherResponse(name: "Seattle", weather: [Awesome_Weather_App.WeatherAPI(description: "overcast clouds", icon: "04d")], main: Awesome_Weather_App.MainAPI(temp: 287.81, pressure: 1018.0, humidity: 44.0, temp_min: 284.91, temp_max: 290.42, feels_like: 286.48), sys: Awesome_Weather_App.SysAPI(sunrise: 1.6712886e+09, sunset: 1.6713243e+09))
Thank you in advance
Everything works as intended
No, it doesn't. I don't know why you claim such a thing; your code isn't working at all.
The problem is that you are trying to return weatherResult from performRequest. But performRequest gets its weatherResult value asynchronously, so this attempt is doomed to failure; you will always be returning nil, because the return weatherResult happens before session.dataTask ever even starts to find out what weatherResult is.
You cannot just synchronously return the results of an asynchronous request. You have two basic options for asynchronous requests.
Use the older “completion handler” pattern with Result types:
struct WeatherManager {
let weatherURL = "https://api.openweathermap.org/data/2.5/weather"
let appId = "someAPIKeyHere"
func fetchWeather(
cityName: String,
completion: #escaping (Result<WeatherModel, Error>) -> Void
) {
guard var components = URLComponents(string: weatherURL) else {
completion(.failure(URLError(.badURL)))
return
}
components.queryItems = [
URLQueryItem(name: "appid", value: appId),
URLQueryItem(name: "q", value: cityName)
]
guard let url = components.url else {
completion(.failure(URLError(.badURL)))
return
}
performRequest(url: url, completion: completion)
}
func performRequest(
url: URL,
queue: DispatchQueue = .main,
completion: #escaping (Result<WeatherModel, Error>) -> Void
) {
let session = URLSession.shared // note, do not create a new URLSession for every request or else you will leak; use shared instance
let task = session.dataTask(with: url) { data, response, error in
guard
error == nil,
let data = data,
let response = response as? HTTPURLResponse,
200 ..< 300 ~= response.statusCode
else {
queue.async { completion(.failure(error ?? URLError(.badServerResponse))) }
return
}
do {
let weatherResult = try parseJSON(weatherData: data)
queue.async { completion(.success(weatherResult)) }
} catch {
queue.async { completion(.failure(error)) }
}
}
task.resume()
}
func parseJSON(weatherData: Data) throws -> WeatherModel {
let decoder = JSONDecoder()
let response = try decoder.decode(WeatherResponse.self, from: weatherData)
print("this is in decodedData: \(response)")
return WeatherModel(conditionId: 300, cityName: response.name, temperature: response.main.temp)
}
}
Then, rather than:
let weather = weatherManager.fetchWeather(cityName: …)
You would
weatherManager.fetchWeather(cityName: …) { result in
switch result {
case .failure(let error):
print(error)
case .success(let weather):
// do something with the `weather` object here
}
}
// note, do not do anything with `weather` here, because the above
// runs asynchronously (i.e., later).
Use the newer async-await pattern of Swift concurrency:
struct WeatherManager {
let weatherURL = "https://api.openweathermap.org/data/2.5/weather"
let appId = "someAPIKeyHere"
func fetchWeather(cityName: String) async throws -> WeatherModel {
guard var components = URLComponents(string: weatherURL) else {
throw URLError(.badURL)
}
components.queryItems = [
URLQueryItem(name: "appid", value: appId),
URLQueryItem(name: "q", value: cityName)
]
guard let url = components.url else {
throw URLError(.badURL)
}
return try await performRequest(url: url)
}
func performRequest(url: URL) async throws -> WeatherModel {
let session = URLSession.shared // note, do not create a new URLSession for every request or else you will leak; use shared instance
let (data, response) = try await session.data(from: url)
guard
let response = response as? HTTPURLResponse,
200 ..< 300 ~= response.statusCode
else {
throw URLError(.badServerResponse)
}
return try parseJSON(weatherData: data)
}
func parseJSON(weatherData: Data) throws -> WeatherModel {
let decoder = JSONDecoder()
do {
let response = try decoder.decode(WeatherResponse.self, from: weatherData)
print("this is in decodedData: \(response)")
return WeatherModel(conditionId: 300, cityName: response.name, temperature: response.main.temp)
} catch {
print("Something is wrong here: " + error.localizedDescription)
throw error
}
}
}
And then you can do things like:
Task {
do {
let weather = try await weatherManager.fetchWeather(cityName: …)
// do something with `weather` here
} catch {
print(error)
}
}
Note, a few changes in the above unrelated to the asynchronous nature of your request:
Avoid creating URLSession instances. If you do, you need to remember to invalidate them. Instead, it is much easier to use URLSession.shared, eliminating this annoyance.
Avoid building URLs with string interpolation. Use URLComponents to build safe URLs (e.g., ones that can handle city names like “San Francisco”, with spaces in their names).

Convert Alamofire Completion handler to Async/Await | Swift 5.5, *

I have the current function which works. I'm using it with completion handler:
func getTokenBalances(completion: #escaping (Bool) -> Void) {
guard let url = URL(string: "someApiUrlFromLostandFound") else {
print("Invalid URL")
completion(false)
return
}
AF.request(url, method: .get).validate().responseData(completionHandler: { data in
do {
guard let data = data.data else {
print("Response Error:", data.error as Any)
completion(false)
return
}
let apiJsonData = try JSONDecoder().decode(TokenBalanceClassAModel.self, from: data)
DispatchQueue.main.async {
self.getTokenBalancesModel = apiJsonData.data.items
completion(true)
}
} catch {
print("ERROR:", error)
completion(false)
}
})
}
How can I convert it to the new async/await functionality of swift 5.5?
This is what I've tried:
func getTokenBalances3() async {
let url = URL(string: "someApiUrlFromLostandFound")
let apiRequest = await withCheckedContinuation { continuation in
AF.request(url!, method: .get).validate().responseData { apiRequest in
continuation.resume(returning: apiRequest)
}
}
let task1 = Task {
do {
// Decoder is not asynchronous
let apiJsonData = try JSONDecoder().decode(SupportedChainsClassAModel.self, from: apiRequest.data!)
// Working data -> print(String(apiJsonData.data.items[0].chain_id!))
} catch {
print("ERROR:", error)
}
}
let result1 = await task1.value
print(result1) // values are not printed
}
But I'm not getting the value at the end on the print statement.
I'm kind of lost in the process, I'd like to convert my old functions, with this example it would help a lot.
EDIT:
The Answer below works, but I found my own solution while the Alamofire team implements async:
func getSupportedChains() async throws -> [AllChainsItemsClassAModel] {
var allChains: [AllChainsItemsClassAModel] = [AllChainsItemsClassAModel]()
let url = URL(string: covalentHqUrlConnectionsClassA.getCovalenHqAllChainsUrl())
let apiRequest = await withCheckedContinuation { continuation in
AF.request(url!, method: .get).validate().responseData { apiRequest in
continuation.resume(returning: apiRequest)
}
}
do {
let data = try JSONDecoder().decode(AllChainsClassAModel.self, from: apiRequest.data!)
allChains = data.data.items
} catch {
print("error")
}
return allChains
}
First of all, your structure is wrong. Do not start with your original code and wrap all of it in the continuation block. Just make a version of AF.request itself that's wrapped in a continuation block. For example, the JSON decoding is not something that should be part of what's being wrapped; it is what comes after the result of networking returns to you — it is the reason why you want to turn AF.request into an async function to begin with.
Second, as the error message tells you, resolve the generic, either by the returning into an explicit return type, or by stating the type as part of the continuation declaration.
So, for example, what I would do is just minimally wrap AF.request in an async throws function, where if we get the data we return it and if we get an error we throw it:
func afRequest(url:URL) async throws -> Data {
try await withUnsafeThrowingContinuation { continuation in
AF.request(url, method: .get).validate().responseData { response in
if let data = response.data {
continuation.resume(returning: data)
return
}
if let err = response.error {
continuation.resume(throwing: err)
return
}
fatalError("should not get here")
}
}
}
You'll notice that I didn't need to resolve the generic continuation type because I've declared the function's return type. (This is why I pointed you to my explanation and example in my online tutorial on this topic; did you read it?)
Okay, so the point is, now it is trivial to call that function within the async/await world. A possible basic structure is:
func getTokenBalances3() async {
let url = // ...
do {
let data = try await self.afRequest(url:url)
print(data)
// we've got data! okay, so
// do something with the data, like decode it
// if you declare this method as returning the decoded value,
// you could return it
} catch {
print(error)
// we've got an error! okay, so
// do something with the error, like print it
// if you declare this method as throwing,
// you could rethrow it
}
}
Finally I should add that all of this effort is probably wasted anyway, because I would expect the Alamofire people to be along with their own async versions of all their asynchronous methods, any time now.
Personally I think swallowing errors inside a network call is a bad idea, the UI should receive all errors and make the choice accordingly.
Here is an example of short wrapper around responseDecodable, that produces an async response.
public extension DataRequest {
#discardableResult
func asyncDecodable<T: Decodable>(of type: T.Type = T.self,
queue: DispatchQueue = .main,
dataPreprocessor: DataPreprocessor = DecodableResponseSerializer<T>.defaultDataPreprocessor,
decoder: DataDecoder = JSONDecoder(),
emptyResponseCodes: Set<Int> = DecodableResponseSerializer<T>.defaultEmptyResponseCodes,
emptyRequestMethods: Set<HTTPMethod> = DecodableResponseSerializer<T>.defaultEmptyRequestMethods) async throws -> T {
return try await withCheckedThrowingContinuation({ continuation in
self.responseDecodable(of: type, queue: queue, dataPreprocessor: dataPreprocessor, decoder: decoder, emptyResponseCodes: emptyResponseCodes, emptyRequestMethods: emptyRequestMethods) { response in
switch response.result {
case .success(let decodedResponse):
continuation.resume(returning: decodedResponse)
case .failure(let error):
continuation.resume(throwing: error)
}
}
})
}
}
This is a mix between my Answer and the one that matt provided. There will probably be an easier and cleaner implementation once the Alamofire team implements async but at least for now I'm out of the call backs hell...
func afRequest(url: URL) async throws -> Data {
try await withUnsafeThrowingContinuation { continuation in
AF.request(url, method: .get).validate().responseData { response in
if let data = response.data {
continuation.resume(returning: data)
return
}
if let err = response.error {
continuation.resume(throwing: err)
return
}
fatalError("Error while doing Alamofire url request")
}
}
}
func getSupportedChains() async -> [AllChainsItemsClassAModel] {
var allChains: [AllChainsItemsClassAModel] = [AllChainsItemsClassAModel]()
let url = URL(string: covalentHqUrlConnectionsClassA.getCovalenHqAllChainsUrl())
do {
let undecodedData = try await self.afRequest(url: url!)
let decodedData = try JSONDecoder().decode(AllChainsClassAModel.self, from: undecodedData)
allChains = decodedData.data.items
} catch {
print(error)
}
return allChains
}

How do I use RxSwift with AlamoFire and SwiftyJSON?

I'm trying to learn RxSwift and currently I'm trying to use it in relation to AlamoFire and SwiftyJSON, that is, to observe when JSON has been downloaded so that I can parse it. I have working code for getting JSON:
guard let myURL = URL(string: "https://api.myjson.com/bins/e5gjk") else { return }
var myArray = [People]()
let myObserver = Observable.from(myArray)
Alamofire.request(myURL, method: .get)
.validate()
.responseJSON{ response in
guard response.result.isSuccess else {
print("Error")
return
}
let json = JSON(response.result.value)
for i in 0...json["employees"].count {
let people = People()
people.name = json["employees"][i]["firstName"].stringValue
people.job = json["employees"][i]["job"].stringValue
myArray.append(people)
}
for i in myArray {
print(i.name)
print(i.job)
}
}
myObserver.subscribe(onNext: {
print($0)
}, onError: { error in
print(error)
}, onCompleted: {
print("completed")
}, onDisposed: {
print("disposed")
}).disposed(by: DisposeBag())
As you can see, I have parsed the JSON as well. I guess that the point of RX here would be to use the data in onNext once it has been parsed, correct? Or have I misunderstood its purpose?
Anyway, I have an observer on myArray: let myObserver = Observable.from(myArray). In my head, subscribe onNext should be triggered as soon as myArray gets data, but that's not happening. What happens is that completed is run immediately, then the JSON networking and parsing takes place. The subscription is not triggered when myArray gets new data. Have I missed something or misunderstood RX's purpose?
EDIT
Or wait, should the whole JSON handling and parsing be in the onNext?
you need to create your observer. This should work :
let observer = Observable<People>.create { (observer) -> Disposable in
Alamofire.request(myURL, method: .get)
.validate()
.responseJSON { response in
guard response.result.isSuccess else {
print("Error")
observer.on(.error(response.result.error!))
return
}
let json = JSON(response.result.value)
for i in 0...json["employees"].count {
let people = People()
people.name = json["employees"][i]["firstName"].stringValue
people.job = json["employees"][i]["job"].stringValue
observer.on(.next(people))
myArray.append(people)
}
observer.on(.completed)
for i in myArray {
print(i.name)
print(i.job)
}
}
return Disposables.create()
}
then you can subscribe to your observer of type Observable<People>
observer.subscribe { (event) in
switch event {
case .next(let people):
print(people.job)
print(people.name)
case .error(let error):
print("error \(error.localizedDescription)")
case .completed:
print("completed")
}
}.disposed(by: disposeBag)

RxSwift URLSession request is disposed

I'm learning to use RxSwift and I'm stuck using this simple code. My intention is to take an APIRequest type, as simple as this:
public protocol APIRequest: Encodable {
associatedtype Response: Decodable
var path: String { get }
}
Pass it to the API Client and finally return an Observable of type T.Response, however, I'm constantly seeing cancelled status in my console:
2019-07-01 10:46:04.847: test api request -> subscribed
2019-07-01 10:46:04.855: test api request -> isDisposed
This is my APIClient's code:
func send<T: APIRequest>(_ request: T) -> Observable<T.Response> {
guard let fullURL = endpoint(for: request) else {
return Observable.error(APIError.invalidBaseURL)
}
return Observable<T.Response>.create { observer in
let request = URLRequest(url: fullURL)
let response = URLSession.shared.rx.response(request: request)
.debug("test api request")
return response.subscribe(onNext: { response, data in
if 200..<300 ~= response.statusCode {
guard let responseItems = try? self.jsonDecoder.decode(T.Response.self, from: data) else {
return observer.onError(APIError.decodingFailed)
}
observer.onNext(responseItems)
observer.onCompleted()
}
}, onError: { error in
observer.onError(APIError.other(error))
}, onCompleted: nil,
onDisposed: nil)
}
}
I've been trying to get the results printed to the console with:
apiClient.send(countriesRequest)
.subscribe(onNext: {
print("Success", $0)
}, onError: {
print("Error: ", $0)
}, onCompleted: {
print("Completed!")
})
.disposed(by: disposeBag)
What am I doing wrong and why?
I've tried to recreate your code, and it seem working fine on my device:
func send<T>(_ request: T) -> Observable<Data> {
let request = URLRequest(url: URL(string: "sdf")!)
return Observable.create { obs in
URLSession.shared.rx.response(request: request).debug("r").subscribe(
onNext: { response in
return obs.onNext(response.data)
},
onError: {error in
obs.onError(error)
})
}
}
I'm subscribing to it, and it produces an error
send("qwe").subscribe(
onNext: { ev in
print(ev)
}, onError: { error in
print(error)
}).disposed(by: disposeBag)

How to handle error from api request properly with RxSwift in MVVM?

So, I have a button and will make an API request upon tapping it. When the API request returns an error, if my understanding is correct, the sequence will be terminated and no subsequent action will be recorded. How do I handle this properly so that I can still make another API request when tapping the button.
My thoughts are to have two observables that I can subscribe to in ViewController and on button pressed, one of it will print the success response and one of it will print the error. Just not quite sure how I can achieve that.
PS: In Post.swift, I have purposely set id as String type to fail the response. It should have be an Int type.
Post.swift
import Foundation
struct Post: Codable {
let id: String
let title: String
let body: String
let userId: Int
}
APIClient.swift
class APIClient {
static func request<T: Codable> (_ urlConvertible: URLRequestConvertible, decoder: JSONDecoder = JSONDecoder()) -> Observable<T> {
return Observable<T>.create { observer in
URLCache.shared.removeAllCachedResponses()
let request = AF.request(urlConvertible)
.responseDecodable (decoder: decoder) { (response: DataResponse<T>) in
switch response.result {
case .success(let value):
observer.onNext(value)
observer.onCompleted()
case .failure(let error):
switch response.response?.statusCode {
default:
observer.onError(error)
}
}
}
return Disposables.create {
request.cancel()
}
}
}
}
PostService.swift
class PostService {
static func getPosts(userId: Int) -> Observable<[Post]> {
return APIClient.request(PostRouter.getPosts(userId: userId))
}
}
ViewModel.swift
class LoginLandingViewModel {
struct Input {
let username: AnyObserver<String>
let nextButtonDidTap: AnyObserver<Void>
}
struct Output {
let apiOutput: Observable<Post>
let invalidUsername: Observable<String>
}
// MARK: - Public properties
let input: Input
let output: Output
// Inputs
private let usernameSubject = BehaviorSubject(value: "")
private let nextButtonDidTapSubject = PublishSubject<Void>()
// MARK: - Init
init() {
let minUsernameLength = 4
let usernameEntered = nextButtonDidTapSubject
.withLatestFrom(usernameSubject.asObservable())
let apiOutput = usernameEntered
.filter { text in
text.count >= minUsernameLength
}
.flatMapLatest { _ -> Observable<Post> in
PostService.getPosts(userId: 1)
.map({ posts -> Post in
return posts[0]
})
}
let invalidUsername = usernameEntered
.filter { text in
text.count < minUsernameLength
}
.map { _ in "Please enter a valid username" }
input = Input(username: usernameSubject.asObserver(),
nextButtonDidTap: nextButtonDidTapSubject.asObserver())
output = Output(apiOutput: apiOutput,
invalidUsername: invalidUsername)
}
deinit {
print("\(self) dellocated")
}
}
ViewController
private func configureBinding() {
loginLandingView.usernameTextField.rx.text.orEmpty
.bind(to: viewModel.input.username)
.disposed(by: disposeBag)
loginLandingView.nextButton.rx.tap
.debounce(0.3, scheduler: MainScheduler.instance)
.bind(to: viewModel.input.nextButtonDidTap)
.disposed(by: disposeBag)
viewModel.output.apiOutput
.subscribe(onNext: { [unowned self] post in
print("Valid username - Navigate with post: \(post)")
})
.disposed(by: disposeBag)
viewModel.output.invalidUsername
.subscribe(onNext: { [unowned self] message in
self.showAlert(with: message)
})
.disposed(by: disposeBag)
}
You can do that by materializing the even sequence:
First step: Make use of .rx extension on URLSession.shared in your network call
func networkCall(...) -> Observable<[Post]> {
var request: URLRequest = URLRequest(url: ...)
request.httpMethod = "..."
request.httpBody = ...
URLSession.shared.rx.response(request)
.map { (response, data) -> [Post] in
guard let json = try? JSONSerialization.jsonObject(with: data, options: []),
let jsonDictionary = json as? [[String: Any]]
else { throw ... } // Throw some error here
// Decode this dictionary and initialize your array of posts here
...
return posts
}
}
Second step, materializing your observable sequence
viewModel.networkCall(...)
.materialize()
.subscribe(onNext: { event in
switch event {
case .error(let error):
// Do something with error
break
case .next(let posts):
// Do something with posts
break
default: break
}
})
.disposed(by: disposeBag)
This way, your observable sequence will never be terminated even when you throw an error inside your network call, because .error events get transformed into .next events but with a state of .error.
So I have also found the way to achieve what I wanted, which is assigning the success output and error output into two different observable respectively. By using RxSwiftExt, there are two additional operators, elements() and errors() which can be used on an observable that is materialized to get the element.
Here is how I did it,
ViewModel.swift
let apiOutput = usernameEntered
.filter { text in
text.count >= minUsernameLength
}
.flatMapLatest { _ in
PostService.getPosts(userId: 1)
.materialize()
}
.share()
let apiSuccess = apiOutput
.elements()
let apiError = apiOutput
.errors()
.map { "\($0)" }
Then, just subscribe to each of these observables in the ViewController.
As reference: http://adamborek.com/how-to-handle-errors-in-rxswift/