Re-organizing chained observables - swift

I have a pretty hefty chunk of chained Rx observables that are fired when a tableviews row is selected via table.rx.modelSelected.
I'd like to be able to break this logic up, because I'm currently having to execute business logic in flatMapLatest, because it's "Step 1" to the process (which feels wrong), and I have to execute more business logic in the subsequent subscribe ("Step 2"). Here's the code I'm using:
locationsTable.rx.modelSelected(Location.self)
.flatMapLatest { [weak self] location -> Observable<[JobState]?> in
guard let hubs = self?.viewModel.userInfo.authorizedHubLocations else { return .empty() }
guard let hub = hubs.first(where: { $0.locationId == location.id }) else { return .empty() }
guard let hubToken = hub.hubToken else { return .empty() }
// save data in db
self?.databaseService.persistHub(hubResult: hub, location: location)
// make network call for the 2nd step (the subscribe)
let networkService = NetworkService(plugins: [AuthPlugin(token: hubToken)])
return networkService.jobStates(locationId: location.id)
}
.subscribe(onNext: { [weak self] jobState in
if let jobState = jobState {
self?.databaseService.persistJobStates(jobStates: jobState)
}
NavigationService.renderScreenB()
}, onError: { error in
Banner.showBanner(type: .error, title: "Whoops", message: "Something went wrong.")
}).disposed(by: disposeBag)
This code currently works, but it feels dirty. Any advice on how to clean this up would be greatly appreciated.

You have several separate and distinct bits of logic and side-effects and you are trying to stuff them all into a single flatMap. I suggest breaking them up into their component parts.
Also, your error logic isn't correct. If your network service emits an error your "Whoops" banner will display, but it will also break your chain and the user won't be able to select a different location. My code below fixes this problem.
The functions below are all free functions. Since they are not tied to a specific view controller, they can be used and tested independently. Also notice that these functions encompass all the logic and only the logic of the system. This allows you to test the logic free of side-effects and promotes good architecture. Also notice that they return Drivers. You can be sure that none of these functions will emit an error which would break the chain and the view controller's behavior.
/// Emits hubs that need to be persisted.
func hubPersist(location: ControlEvent<Location>, userInfo: UserInfo) -> Driver<(location: Location, hub: Hub)> {
let hub = getHub(location: location, userInfo: userInfo)
.asDriver(onErrorRecover: { _ in fatalError("no errors are possible") })
return Driver.combineLatest(location.asDriver(), hub) { (location: $0, hub: $1) }
}
/// Values emitted by this function are used to make the network request.
func networkInfo(location: ControlEvent<Location>, userInfo: UserInfo) -> Driver<(NetworkService, Int)> {
let hub = getHub(location: location, userInfo: userInfo)
return Observable.combineLatest(hub, location.asObservable())
.compactMap { (hub, location) -> (NetworkService, Int)? in
guard let hubToken = hub.hubToken else { return nil }
return (NetworkService(plugins: [AuthPlugin(token: hubToken)]), location.id)
}
.asDriver(onErrorRecover: { _ in fatalError("no errors are possible") })
}
/// shared logic used by both of the above. Testing the above will test this by default.
func getHub(location: ControlEvent<Location>, userInfo: UserInfo) -> Observable<Hub> {
return location
.compactMap { location -> Hub? in
let hubs = userInfo.authorizedHubLocations
return hubs.first(where: { $0.locationId == location.id })
}
}
The function below is a wrapper around your network request that makes errors more usable.
extension NetworkService {
func getJobStates(locationId: Int) -> Driver<Result<[JobState], Error>> {
return jobStates(locationId: locationId)
.map { .success($0 ?? []) }
.asDriver(onErrorRecover: { Driver.just(.failure($0)) })
}
}
Here is your view controller code using all of the above. It consists almost exclusively of side effects. The only logic are a couple of guards to check for success/failure of the network request.
func viewDidLoad() {
super.viewDidLoad()
hubPersist(location: locationsTable.rx.modelSelected(Location.self), userInfo: viewModel.userInfo)
.drive(onNext: { [databaseService] location, hub in
databaseService?.persistHub(hubResult: hub, location: location)
})
.disposed(by: disposeBag)
let jobStates = networkInfo(location: locationsTable.rx.modelSelected(Location.self), userInfo: viewModel.userInfo)
.flatMapLatest { networkService, locationId in
return networkService.getJobStates(locationId: locationId)
}
jobStates
.drive(onNext: { [databaseService] jobStates in
guard case .success(let state) = jobStates else { return }
databaseService?.persistJobStates(jobStates: state)
})
.disposed(by: disposeBag)
jobStates
.drive(onNext: { jobStates in
guard case .success = jobStates else { return }
NavigationService.renderScreenB()
})
.disposed(by: disposeBag)
jobStates
.drive(onNext: { jobStates in
guard case .failure = jobStates else { return }
Banner.showBanner(type: .error, title: "Whoops", message: "Something went wrong.")
})
.disposed(by: disposeBag)
}
FYI, the above code uses Swift 5/RxSwift 5.

Related

Swift: Recursive async func with completion handler that does not get called

I need to fetch large amounts of data from an endpoint in an async way. The API endpoint serves a predefined amount of data at a time. After the first request I must check to see if I get a "next" url from the response and visit that link in order to continue the download. This recursive behaviour continues until all available data has been served, in other words paging functionality (HAL links). At this point I have implemented a func that download recursively, however: problem is that the final completion handler does not seem to get called.
Demo code: The ThingsApi is a class that encapsulates the actual API call. The important thing is that this class has an initial url and during recursion will get specific url's to visit asynchronously. I call the downloadThings() func and need to get notified when it is finished. It works if I leave recursion out of the equation. But when recursion is in play then nothing!
I have created a simplified version of the code that illustrate the logic and can be pasted directly into the Playground. The currentPage and pages var's are just there to demo the flow. The last print() statement does not get called. Leave the currentPage += 1 to experience the problem and set currentPage += 6 to avoid recursion. Clearly I am missing out of some fundamental concept here. Anyone?
import UIKit
let pages = 5
var currentPage = 0
class ThingsApi {
var url: URL?
var next: URL?
init(from url: URL) {
self.url = url
}
init() {
self.url = URL(string: "https://whatever.org")
}
func get(completion: #escaping (Data?, HTTPURLResponse?, Error?) -> Void) {
// *** Greatly simplified
// Essentially: use URLSession.shared.dataTask and download data async.
// When done, call the completion handler.
// Simulate that the download will take 1 second.
sleep(1)
completion(nil, nil, nil)
}
}
func downloadThings(url: URL? = nil, completion: #escaping (Bool, Error?, String?) -> Void) {
var thingsApi: ThingsApi
if let url = url {
// The ThingsApi will use the next url (retrieved from previous call).
thingsApi = ThingsApi(from: url)
} else {
// The ThingsApi will use the default url.
thingsApi = ThingsApi()
}
thingsApi.get(completion: { (data, response, error) in
if let error = error {
completion(false, error, "We have nothing")
} else {
// *** Greatly simplified
// Parse the data and save to db.
// Simulate that the thingsApi.next will have a value 5 times.
currentPage += 1
if currentPage <= pages {
thingsApi.next = URL(string: "https://whatever.org?page=\(currentPage)")
}
if let next = thingsApi.next {
// Continue downloading things recursivly.
downloadThings(url: next) { (success, error, feedback) in
guard success else {
completion(false, error, "failed")
return
}
}
} else {
print("We are done")
completion(true, nil, "done")
print("I am sure of it")
}
}
})
}
downloadThings { (success, error, feedback) in
guard success else {
print("downloadThings() failed")
return
}
// THIS DOES NOT GET EXECUTED!
print("All your things have been downloaded")
}
It seems like this is simply a case of "you forgot to call it yourself" :)
In this if statement right here:
if let next = thingsApi.next {
// Continue downloading things recursivly.
downloadThings(url: next) { (success, error, feedback) in
guard success else {
completion(false, error, "failed")
return
}
}
} else {
print("We are done")
completion(true, nil, "done")
print("I am sure of it")
}
Think about what happens on the outermost call to downloadThings, and execution goes into the if branch, and the download is successful. completion is never called!
You should call completion after the guard statement!

Swift service call, handling response

I am writing the iOS application using swift 4.2. I am making a service call to logout user.
I need to know where to use main thread (DispatchQueue.main.async).
Here is my code:
private func handleLogoutCellTap() {
logoutUseCase?.logout() { [weak self] (result) in
guard let self = self else { return }
switch result {
case let (.success(didLogout)):
didLogout ? self.handleSuccessfullLogout() : self.handleLogoutError(with: nil)
case let (.failure(error)):
self.handleLogoutError(with: error)
}
}
}
logoutUseCase?.logout() makes a service call and returns #escaping completion. Should I use DispatchQueue.main.async on this whole handleLogoutCellTap() function or just in a handling segment?
Move the control to main thread wherever you're updating the UI after receiving the response of logout.
If handleSuccessfullLogout() and handleLogoutError(with:) methods perform any UI operation, you can embed the whole switch statement in DispatchQueue.main.async, i,e.
private func handleLogoutCellTap() {
logoutUseCase?.logout() { [weak self] (result) in
guard let self = self else { return }
DispatchQueue.main.async { //here.....
switch result {
//rest of the code....
}
}
}
}

How to wait until get the response from component under test that use Alamofire? - Xcode

I have a login view controller that user Almofire library to get the response. I do the unit test on that controller but the test always fail. I think because take time to response.
My test case:
override func setUp() {
super.setUp()
continueAfterFailure = false
let vc = UIStoryboard(name: "Main", bundle: nil)
controllerUnderTest = vc.instantiateViewController(withIdentifier: "LoginVC") as! LoginViewController
controllerUnderTest.loadView()
}
override func tearDown() {
// Put teardown code here. This method is called after the invocation of each test method in the class.
controllerUnderTest = nil
super.tearDown()
}
func testLoginWithValidUserInfo() {
controllerUnderTest.email?.text = "raghad"
controllerUnderTest.pass?.text = "1234"
controllerUnderTest.loginButton?.sendActions(for: .touchUpInside)
XCTAssertEqual(controllerUnderTest.lblValidationMessage?.text , "logged in successfully")
}
I try to use:
waitForExpectations(timeout: 60, handler: nil)
But I got this error:
caught "NSInternalInconsistencyException"
almofire function in login presenter :
func sendRequest(withParameters parameters: [String : String]) {
Alamofire.request(LOGINURL, method: .post, parameters: parameters).validate ().responseJSON { response in
debugPrint("new line : \(response)" )
switch response.result {
case .success(let value):
let userJSON = JSON(value)
self.readResponse(data: userJSON)
case .failure(let error):
print("Error \(String(describing: error))")
self.delegate.showMessage("* Connection issue ")
}
self.delegate.removeLoadingScreen()
//firebase log in
Auth.auth().signIn(withEmail: parameters["email"]!, password: parameters["pass"]!) { [weak self] user, error in
//guard let strongSelf = self else { return }
if(user != nil){
print("login with firebase")
}
else{
print("eroor in somthing")
}
if(error != nil){
print("idon now")
}
// ...
}
}
}
func readResponse(data: JSON) {
switch data["error"].stringValue {
case "true":
self.delegate.showMessage("* Invalid user name or password")
case "false":
if data["state"].stringValue=="0" {
self.delegate.showMessage("logged in successfully")
}else {
self.delegate.showMessage("* Inactive account")
}
default:
self.delegate.showMessage("* Connection issue")
}
}
How can I solve this problem? :(
Hi #Raghad ak, welcome to Stack Overflow 👋.
Your guess about the passage of time preventing the test to succeed is correct.
Networking code is asynchronous. After the test calls .sendActions(for: .touchUpInside) on your login button it moves to the next line, without giving the callback a chance to run.
Like #ajeferson's answer suggests, in the long run I'd recommend placing your Alamofire calls behind a service class or just a protocol, so that you can replace them with a double in the tests.
Unless you are writing integration tests in which you'd be testing the behaviour of your system in the real world, hitting the network can do you more harm than good. This post goes more into details about why that's the case.
Having said all that, here's a quick way to get your test to pass. Basically, you need to find a way to have the test wait for your asynchronous code to complete, and you can do it with a refined asynchronous expectation.
In your test you can do this:
expectation(
for: NSPredicate(
block: { input, _ -> Bool in
guard let label = input as? UILabel else { return false }
return label.text == "logged in successfully"
}
),
evaluatedWith: controllerUnderTest.lblValidationMessage,
handler: .none
)
controllerUnderTest.loginButton?.sendActions(for: .touchUpInside)
waitForExpectations(timeout: 10, handler: nil)
That expectation will run the NSPredicate on a loop, and fulfill only when the predicate returns true.
You have to somehow signal to your tests that are safe to proceed (i.e. expectation is fulfilled). The ideal approach would be decouple that Alamofire code and mock its behavior when testing. But just to answer your question, you might want to do the following.
In your view controller:
func sendRequest(withParameters parameters: [String : String], completionHandler: (() -> Void)?) {
...
Alamofire.request(LOGINURL, method: .post, parameters: parameters).validate ().responseJSON { response in
...
// Put this wherever appropriate inside the responseJSON closure
completionHandler?()
}
}
Then in your tests:
func testLoginWithValidUserInfo() {
controllerUnderTest.email?.text = "raghad"
controllerUnderTest.pass?.text = "1234"
controllerUnderTest.loginButton?.sendActions(for: .touchUpInside)
let expectation = self.expectation(description: "logged in successfully)
waitForExpectations(timeout: 60, handler: nil)
controllerUnderTest.sendRequest(withParameters: [:]) {
expectation.fulfill()
}
XCTAssertEqual(controllerUnderTest.lblValidationMessage?.text , "logged in successfully")
}
I know you have some intermediate functions between the button click and calling the sendRequest function, but this is just for you to get the idea. Hope it helps!

Structuring a View Model Using RxSwift

My view models are fundamentally flawed because those that use a driver will complete when an error is returned and resubscribing cannot be automated.
An example is my PickerViewModel, the interface of which is:
// MARK: Picker View Modelling
/**
Configures a picker view.
*/
public protocol PickerViewModelling {
/// The titles of the items to be displayed in the picker view.
var titles: Driver<[String]> { get }
/// The currently selected item.
var selectedItem: Driver<String?> { get }
/**
Allows for the fetching of the specific item at the given index.
- Parameter index: The index at which the desired item can be found.
- Returns: The item at the given index. `nil` if the index is invalid.
*/
func item(atIndex index: Int) -> String?
/**
To be called when the user selects an item.
- Parameter index: The index of the selected item.
*/
func selectItem(at index: Int)
}
An example of the Driver issue can be found within my CountryPickerViewModel:
init(client: APIClient, location: LocationService) {
selectedItem = selectedItemVariable.asDriver().map { $0?.name }
let isLoadingVariable = Variable(false)
let countryFetch = location.user
.startWith(nil)
.do(onNext: { _ in isLoadingVariable.value = true })
.flatMap { coordinate -> Observable<ItemsResponse<Country>> in
let url = try client.url(for: RootFetchEndpoint.countries(coordinate))
return Country.fetch(with: url, apiClient: client)
}
.do(onNext: { _ in isLoadingVariable.value = false },
onError: { _ in isLoadingVariable.value = false })
isEmpty = countryFetch.catchError { _ in countryFetch }.map { $0.items.count == 0 }.asDriver(onErrorJustReturn: true)
isLoading = isLoadingVariable.asDriver()
titles = countryFetch
.map { [weak self] response -> [String] in
guard let `self` = self else { return [] }
self.countries = response.items
return response.items.map { $0.name }
}
.asDriver(onErrorJustReturn: [])
}
}
The titles drive the UIPickerView, but when the countryFetch fails with an error, the subscription completes and the fetch cannot be retried manually.
If I attempt to catchError, it is unclear what observable I could return which could be retried later when the user has restored their internet connection.
Any justReturn error handling (asDriver(onErrorJustReturn:), catchError(justReturn:)) will obviously complete as soon as they return a value, and are useless for this issue.
I need to be able to attempt the fetch, fail, and then display a Retry button which will call refresh() on the view model and try again. How do I keep the subscription open?
If the answer requires a restructure of my view model because what I am trying to do is not possible or clean, I would be willing to hear the better solution.
Regarding ViewModel structuring when using RxSwift, during intensive work on a quite big project I've figured out 2 rules that help keeping solution scalable and maintainable:
Avoid any UI-related code in your viewModel. It includes RxCocoa extensions and drivers. ViewModel should focus specifically on business logic. Drivers are meant to be used to drive UI, so leave them for ViewControllers :)
Try to avoid Variables and Subjects if possible. AKA try to make everything "flowing". Function into function, into function and so on and, eventually, in UI. Of course, sometimes you need to convert non-rx events into rx ones (like user input) - for such situations subjects are OK. But be afraid of subjects overuse - otherwise your project will become hard to maintain and scale in no time.
Regarding your particular problem. So it is always a bit tricky when you want retry functionality. Here is a good discussion with RxSwift author on this topic.
First way. In your example, you setup your observables on init, I also like to do so. In this case, you need to accept the fact that you DO NOT expect a sequence that can fail because of error. You DO expect sequence that can emit either result-with-titles or result-with-error. For this, in RxSwift we have .materialize() combinator.
In ViewModel:
// in init
titles = _reloadTitlesSubject.asObservable() // _reloadTitlesSubject is a BehaviorSubject<Void>
.flatMap { _ in
return countryFetch
.map { [weak self] response -> [String] in
guard let `self` = self else { return [] }
self.countries = response.items
return response.items.map { $0.name }
}
.materialize() // it IS important to be inside flatMap
}
// outside init
func reloadTitles() {
_reloadTitlesSubject.onNext(())
}
In ViewController:
viewModel.titles
.asDriver(onErrorDriveWith: .empty())
.drive(onNext: [weak self] { titlesEvent in
if let titles = titlesEvent.element {
// update UI with
}
else if let error = titlesEvent.error {
// handle error
}
})
.disposed(by: bag)
retryButton.rx.tap.asDriver()
.drive(onNext: { [weak self] in
self?.viewModel.reloadTitles()
})
.disposed(by: bag)
Second way is basically what CloackedEddy suggests in his answer. But can be simplified even more to avoid Variables. In this approach you should NOT setup your observable sequence in viewModel's init, but rather return it anew each time:
// in ViewController
yourButton.rx.tap.asDriver()
.startWith(())
.flatMap { [weak self] _ in
guard let `self` = self else { return .empty() }
return self.viewModel.fetchRequest()
.asDriver(onErrorRecover: { error -> Driver<[String]> in
// Handle error.
return .empty()
})
}
.drive(onNext: { [weak self] in
// update UI
})
.disposed(by: disposeBag)
I would shift some responsibilities to the view controller.
One approach would be to have the view model produce an Observable which as a side effect updates the view model properties. In the following code example, the view controller remains in charge of the view bindings, as well as triggering the refresh in viewDidLoad() and via a button tap.
class ViewModel {
let results: Variable<[String]> = Variable([])
let lastFetchError: Variable<Error?> = Variable(nil)
func fetchRequest() -> Observable<[String]> {
return yourNetworkRequest
.do(onNext: { self.results.value = $0 },
onError: { self.lastFetchError.value = $0 })
}
}
class ViewController: UIViewController {
let viewModel = ViewModel()
let disposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
viewModel.results
.asDriver()
.drive(onNext: { yourLabel.text = $0 /* .reduce(...) */ })
.disposed(by: disposeBag)
viewModel.lastFetchError
.asDriver()
.drive(onNext: { yourButton.isHidden = $0 == nil })
.disposed(by: disposeBag)
yourButton.rx.tap
.subscribe(onNext: { [weak self] in
self?.refresh()
})
.disposed(by: disposeBag)
// initial attempt
refresh()
}
func refresh() {
// trigger the request
viewModel.fetchRequest()
.subscribe()
.disposed(by: disposeBag)
}
}
All answers are good, but i want to mentioned about CleanArchitectureRxSwift. This framework really help me to find the way how rx can be applied to my code. The part about "backend" mobile programming (request, parsers, etc) can be omitted, but work with viewModel/viewController has really interesting things.

rxswift error handle issue

I have a BehaviorSubject named createObservable in my view model. And my view controller subscribe it.
viewModel!.createObservable.subscribe(onNext: {[unowned self] (obj:PassbookModelType?) -> Void in
if let _ = obj{
self.dismissVC()
}
}, onError: { (error) -> Void in
print(error)
}).addDisposableTo(self.dispose)
I have a function named saveObject() also in the view model. If I click the navigation bar right item it will be emitted. And there is an error will send to createObservable's observer.
func saveObject(){
```````
```````
if condition {
createObservable.on(Event.Next(model))
createObservable.onCompleted()
}else{
createObservable.onError(MyError.someError)
}
}
The problem is that if the error happened the createObservable will be closed, so I won't receive any Next event in the future. I tried to use retry(), but it seems will cause deadlock, view controller can't response any touch event any more. So can some one tell me how to fix this issue? Thanks a lot
viewModel!.createObservable.retry().subscribe(onNext: {[unowned self] (obj:PassbookModelType?) -> Void in
if let _ = obj{
self.dismissVC()
}
}, onError: { (error) -> Void in
print(error)
}).addDisposableTo(self.dispose)
I suggest to make the type of createObservable PublishSubject<Observable<PassbookModelType>>, instead of BehaviorSubject<PassbookModelType?> which, I guess, accidentally flattens two Rx streams conceptually separatable each other: the saveObject process itself (an one-shot process) and starting the saveObject process initiated by user action repeatedly. I've written a short example to demonstrate it.
let createObservable = PublishSubject<Observable<Int>>()
override func viewDidLoad() {
super.viewDidLoad()
createObservable.flatMap {
$0.map { obj in
print("success: \(obj)")
}
.catchError { err in
print("failure: \(err)")
return empty()
}
}.subscribe()
}
// Simulates an asynchronous proccess to succeed.
#IBAction func testSuccess(sender: UIView!) {
let oneShot = PublishSubject<Int>()
createObservable.onNext(oneShot)
callbackAfter3sec { res in
oneShot.onNext(1)
oneShot.onCompleted()
}
}
// Simulates an asynchronous process to fail.
#IBAction func testFailure(sender: UIView!) {
let oneShot = PublishSubject<Int>()
createObservable.onNext(oneShot)
callbackAfter3sec { res in
oneShot.onError(NSError(domain: "Error", code: 1, userInfo: nil))
}
}
func callbackAfter3sec(completion: Int -> ()) {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, Int64(NSEC_PER_SEC * 3)), dispatch_get_main_queue()) {
completion(2)
}
}
There is an important merit with that: If the one-shot process would become in the Rx style (for example, like as callbackAfter3sec() -> Observable<Int>) in the future, there were no need to re-write the use-side code like in the viewDidLoad above. There is an only one change to do is to pass an Observable<> object to createObservable.onNext(...).
Sorry for my poor English skill. I hope this makes sense to you.

Categories