How do I flatMap two singles? - ios

I have an observable that looks like this:
func getParameters() -> Single<[ParameterModel]?> {
do {
loading.accept(allParameters.isEmpty)
return try parameterService.getParameters()
.do(onSuccess: { [weak self] parameters in
self?.loading.accept(false)
self?.content.accept(parameters?.compactMap { $0 }
.compactMap { TechnicianParameterTableViewCellViewModel(parameter: $0) } ?? [])
})
} catch { return Single.error(error) }
}
The important part is where it compactMaps into a cell. This works as expected. But I also have another observable that looks like this:
func getSingleOrDoubleDoor() -> Single<SingleOrDoubleDoor?> { //SingleOrDoubleDoor is an enum
do {
return try parameterService.getSingleOrDoubleDoor()
} catch {
return Single.error(error)
}
}
Now, I would like to sort of merge getSingleOrDoubleDoor() into getParameters() so that I can access the values of both observables in onSuccess. I want to use the result sort of like this:
.compactMap { TechnicianParameterTableViewCellViewModel(parameter: $0, isSingleOrDouble: $1) } ?? [])
Not being an expert on Rx I still assume that this is done using .flatMap{}. Sort of like:
...return try parameterService.getParameters().flatMap { _ in getSingleOrDoubleDoor() }...
But this gives me an error:
Cannot convert value of type '([ParameterModel]?) -> Single<SingleOrDoubleDoor?>' (aka '(Optional<Array>) -> PrimitiveSequence<SingleTrait, Optional>') to expected argument type '([ParameterModel]?) throws -> PrimitiveSequence<SingleTrait, [ParameterModel]?>'
Tried changing the return expression but it still wouldn't take. Not sure how to make this happen.

Since your methods don't accept parameters, I'll assume they don't depend on each other. If so, you should use the zip method like this:
Single
.zip(getParameters(), getSingleOrDoubleDoor())
.compactMap { TechnicianParameterTableViewCellViewModel(parameter: $0, isSingleOrDouble: $1) } ?? [])
The zip method will trigger the compactMap when both methods return a value. Flatmap has a different purpose, it is usually used when we need to call methods sequentially, meaning the next call needs data from the previous one.

Related

Difference between return of Any vs Void in Async Block

Between these 2 codes
( returns -> Void)
static func dropboxWorkoutList ( userCompletionHandler: #escaping ([String]) -> Void) {
let client = DropboxClientsManager.authorizedClient
var files: [String] = []
_ = client?.files.listFolder(path: "/workouts")
.response { response, error in
if let result = response {
for entry in result.entries {
if let file = entry as? Files.FileMetadata {
let ext = (file.name as NSString).pathExtension
switch ext {
case "txt", "mrc", "zwo":
// print("filename:\(file.name) ext:\(ext) path:\(file.pathLower)")
files.append(file.pathLower!)
default:
continue
}
}
}
files = files.sorted(by: { $0 < $1 } )
// print("Contents of Workout Folder:\(files)")
userCompletionHandler(files)
} else if let error = error {
print(error)
}
}
}
calling it
dropboxFunc.dropboxWorkoutList() { (value) in
print("value:\(value)")
print("value[0] : \(value[0])")
print("value.count : \(value.count)")
}
results in:
value:["/1-01.txt", "/1-17.txt"]
value[0] : /1-01.txt
value.count : 5
however when changing it
from Return -> Void
to Return -> Any
trying to execute the below will have swift telling me:
"Missing return in a closure expected to return 'Any'"
dropboxFunc.dropboxWorkoutList() { (value) in
print("value:\(value)")
print("value[0] : \(value[0])")
print("value.count : \(value.count)")
}
I can only allowed to execute 1 print statement. Just want to understand the difference.
Note: Asked this
Return list of files from function call
and was given this as possible answer
How I can return value from async block in swift
Specifying a closure of ([String]) -> Any means that the closure is going to return something, and it is of type Any. But in your examples, (a) your closures are not returning anything at all; and (b) the dropboxWorkoutList does not appear to need/use an object returned by the closure supplied by the caller, anyway. This is a “completion handler closure” pattern, and completion handler closures almost always have a Void return type.
I actually want to use the return values from dropboxWorkoutList to populate a tableview which I've not coded yet
OK, then I think you may be conflating the closure parameter (what dropboxWorkoutList will be supplying back to the caller) and the closure’s return value (the far less common scenario, where the caller needs to supply dropboxWorkoutList some value based upon the closure’s parameter). In this case, you want the former (the closure parameter), not the latter (the closure’s return value).
You likely do not want to change the closure to be ([String]) -> Any, at all, but rather leave it as ([String]) -> Void. The caller should just take the parameter of the closure and use that. For example:
dropboxFunc.dropboxWorkoutList { values in
self.strings = values // update whatever model object your table view data source is using
self.tableview.reloadData() // and tell the table view to reload the data
}
In short, your question here (and in that other post) was, effectively, “how do I return a value using a closure?”, to which the answer is that you should not think of it as “returning” a value. Rather, in asynchronous routines with completion handler closures, results are supplied as parameter(s) to the closure. Essentially, dropboxWorkoutList is calling a function (in this case, a closure) to say “hey, here is your data”. It is providing data to that closure, not returning data from that closure.

Get count of Observable array before returning it

In my ViewModel file I have an observable array created after applying map on it. Now before returning it I want to check if it has any content or not. If there is nothing in there I want to return it without applying map. Following is my code:
func retrieveDeals(location: CLLocation?) -> Observable<[SaleItem]> {
let specials = nearestFlightSpecials.retrieveNearestFlightSpecials(userLocation: location)
let happyHourDeals = specials.map {
$0.filter { $0.isHappyHour }
}
return happyHourDeals
}
Before I return happyHourDeals I want to check if it contains any element or not. The above array is subscribed in view but I don't want to apply the above logic there. I want to keep it here in ViewModel.
I suspect what you want to do is filter out empty output:
func retrieveDeals(location: CLLocation?) -> Observable<[SaleItem]> {
let specials = nearestFlightSpecials.retrieveNearestFlightSpecials(userLocation: location)
let happyHourDeals = specials.map {
$0.filter { $0.isHappyHour }
}
.filter { !$0.isEmpty } // this is the line you need.
return happyHourDeals
}
Terminology is important here. Observables don't "contain" values. Observables don't return values, they emit events.
Your happyHourDeals will still be returned but with the filter line, it will no longer emit empty arrays. What this means is that whatever is subscribed to the value returned will not be updated if specials.map { $0.filter { $0.isHappyHour } } emits an empty array.

rxswift viewmodel with input output

I am trying to achieve something similar in rxswift example project from RxSwift repo. But in my case there are dependent observables. I couldn't find any solution without using binding in viewmodel
Here is the structure of my viewmodel:
First the definitions of input, output and viewmodel
typealias UserListViewModelInput = (
viewAppearAction: Observable<Void>,
deleteAction: Observable<Int>
)
typealias UserListViewModelOutput = Driver<[User]>
typealias UserListViewModel = (UserListViewModelInput, #escaping UserApi) -> UserListViewModelOutput
Then there is actual implementation which doesn't compile.
let userListViewModel: UserListViewModel = { input, loadUsers in
let loadedUserList = input.viewAppearAction
.flatMapLatest { loadUsers().materialize() }
.elements()
.asDriver(onErrorDriveWith: .never())
let userListAfterDelete = input.deleteAction
.withLatestFrom(userList) { index, users in
users.enumerated().compactMap { $0.offset != index ? $0.element : nil }
}
.asDriver(onErrorJustReturn: [])
let userList = Driver.merge([loadedUserList, userListAfterDelete])
return userList
}
Viewmodel has two job. First load the user list. Second is delete a user at index. The final output is the user list which is downloaded with UserApi minus deleted users.
The problem in here in order the define userList I need to define userListAfterDelete. And in order to define userListAfterDelete I need to define userList.
So is there a way to break this cycle without using binding inside view model? Like a placeholder observable or operator that keeps state?
This is a job for a state machine. What you will see in the code below is that there are two actions that can affect the User array. When the view appears, a new array is downloaded, when delete comes in, a particular user is removed.
This is likely the most common pattern seen in reactive code dealing with state. So common that there are whole libraries that implement some variation of it.
let userListViewModel: UserListViewModel = { input, loadUsers in
enum Action {
case reset([User])
case delete(at: Int)
}
let resetUsers = input.viewAppearAction
.flatMapLatest { loadUsers().materialize() }
.compactMap { $0.element }
.map { Action.reset($0) }
let delete = input.deleteAction.map { Action.delete(at: $0) }
return Observable.merge(resetUsers, delete)
.scan(into: [User](), accumulator: { users, action in
switch action {
case let .reset(newUsers):
users = newUsers
case let .delete(index):
users.remove(at: index)
}
})
.asDriver(onErrorJustReturn: [])
}

RxSwift: Chain Completable to Observable

I'd like to chain a Completable to an observable element. After calling flatMap, onCompleted and onError callbacks don't seem to be called on subscription.
var user = PublishRelay<User>()
func fetchUserInformation(_ userId: String) -> Completable {
return Completable.create { observer in
apiService.fetchInformation(for: userId, completion: { response in
if let name = response?.name {
user.accept(User(name: name))
observer(.completed)
} else {
observer(.error(ServiceError.userInformation))
}
})
return Disposables.create()
}
}
login()
.flatMap{ userId in fetchUserInformation(userId) }
.subscribe(
onCompleted: {
print("Success!") // Not being called at all
},
onError: { error in
print(error) // Not being called at all
}
).disposed(by: disposeBag)
Although fetchUserInformation and observer(.completed) are being called and user information is being successfully fetched, I won't be able to catch onCompleted on subscription (only when preceded by flatMap).
Is there a clean way to achieve this?
Already tried .materialized() just after the flatMap call in order to get an
Observable<Event<Never>>
rather than a
Observable<Never>
It doesn't work either.
The correct solution would be using the ‘andThen’ operator.
someCompletable
.andThen(someObservable)
Edit:
Just read the rest of your code - I'm not sure why you use a Completable at all since it seems you are actually returning some element from that stream.
You'll probably want to use a Single or Plain-ol' observable to relay that value without using an external Relay.
I think that you can do something like this:
login()
.flatMap{ userId -> Observable<Void> in
return fetchUserInformation(userId).andThen(.just(Void()))
}.subscribe(onNext: { _ in
...
}).disposed(by: disposeBag)
As far as I know you can't convert Completable to Observable since the later omits values while Completable does not.
I guess flatMap is returning Observables from Login and then you convert it to Completables and that's why it fails

Return a completable in RxSwift without using a create block

I have a Completable being returned from a simple function.
This is not an async call, so I just need to return a succcessful completion or error depending on a conditional (using Rx here so I can tie into other Rx usages):
func exampleFunc() -> Completable {
if successful {
return Completable.just() // What to do here???
} else {
return Completable.error(SomeErrorType.someError)
}
}
The error case works pretty easily, but am having a block on how to just return a successful completable (without needing to .create() it).
I was thinking I just need to use Completable's .just() or .never(), but just is requiring a parameter, and never doesn't seem to trigger the completion event.
.empty() is the operator I was looking for!
Turns out, I had mixed up the implementations of .never() and .empty() in my head!
.never() emits no items and does NOT terminate
.empty() emits no items but does terminates normally
So, the example code above works like this:
func exampleFunc() -> Completable {
if successful {
return Completable.empty()
} else {
return Completable.error(SomeErrorType.someError)
}
}
Here is the documentation on empty/throw/never operators.
I would be more inclined to do the following:
func example() throws {
// do something
if !successful {
throw SomeErrorType.someError
}
}
Then in order to tie it into other Rx code, I would just use map as in:
myObservable.map { try example() }
But then, mapping over a Completable doesn't work because map's closure only gets called on next events. :-(
I tend to avoid Completable for this very reason, it doesn't seem to play well with other observables. I prefer to use Observable<Void> and send an empty event before the completed...
Something like this:
let chain = Observable<Void>.just()
let foo = chain.map { try example() }
foo.subscribe { event in print(event) }

Resources