How can I select all item at once using RxSwift operator? - swift

Let's say I have a table view to present products.
Here is my viewModel:
ProductViewModel.swift
var selectedObserver: AnyObserver<Product>
var state: Driver<Set<Product>>
var selectedSubject = PublishSubject<Product>()
self.selectedObserver = selectedSubject.asObserver()
self.state =
selectedSubject.asObservable()
.scan(Set()) { (acc: Set<Product>, item: Product) in
var acc = acc
if acc.contains(item) {
acc.remove(item)
} else {
acc.insert(item)
}
return acc
}
.startWith(Set())
.asDriver(onErrorJustReturn: Set())
self.isSelectedAll = Driver
.combineLatest(
self.state.map { $0.count },
envelope.map { $0.products.count })
.debug()
.map { $0.0 == $0.1 }
As you see, every time I select an object, I will scan it into the state observable, so the cell can then observe the state change.
Here is the RxSwift binding between cell and viewModel:
ProductViewController.swift
self.viewModel.deliveries
.drive(self.tableView.rx.items(cellIdentifier: DeliveryTableViewCellReusableIdentifier, cellType: DeliveryTableViewCell.self)) { (_, item, cell) in
cell.bind(to: self.viewModel.state, as: item)
cell.configureWith(product: item)
}
.disposed(by: disposeBag)
self.tableView.rx.modelSelected(Product.self)
.asDriver()
.drive(self.viewModel.selectedObserver)
.disposed(by: disposeBag)
ProductCell.swift
func bind(to state: Driver<Set<Product>>, as item: Product) {
state.map { $0.contains(item) }
.drive(self.rx.isSelected)
.disposed(by: rx.reuseBag)
}
Well, so far so good.
Now my question is how can I make a select all action e.g. tapping a select all button, so that all product will be somehow scan into states?

There is, of course, more ways to do this. One that comes to my mind is having two different events for single selection vs. select all, unifying them in one enum, eg. SelectionEvent, merging them and passing that to the scan so that in the scan method you can differentiate between them.
Rough example, following your code:
var selectedObserver: AnyObserver<Product>
var state: Driver<Set<Product>>
var selectedSubject = PublishSubject<Product>()
var selectedAllSubject = PublishSubject<Product>() // Added
var selectedAllObserverObserver: AnyObserver<Void> // Added
self.selectedObserver = selectedSubject.asObserver()
self.selectedAllObserverObserver = selectedAllSubject.asObserver()
enum SelectionEvent {
case product(Product)
case all([Product])
}
self.state = Observable.of(
selectedSubject.map { SelectionEvent.product($0) },
// I figured envelope is observable containing all products.
selectedAllSubject.withLatestFrom(envelope.map { $0.products }).map { SelectionEvent.all($0) }
).merge()
.scan(Set()) { (acc: Set<Product>, event: SelectionEvent) in
var acc = acc
// now you can differentitate between events
switch event {
case .product:
if acc.contains(item) {
acc.remove(item)
} else {
acc.insert(item)
}
case .all(let all):
acc = all
}
return acc
}
.startWith(Set())
.asDriver(onErrorJustReturn: Set())
I hope this helps.

Related

Combine array of object from different endpoint so it can also pagination

I have 2 different endpoint:
The first one have a pagination.
The second one doesn't have pagination
I mapping the object from the first and second endpoint so they have the same object when i display it and limit only 10 item.
The Question is..
Is that possible to combine the API called so i can use pagination with different endpoint? so the result is
Merge the object into 1
Sort by date
Limit the item only 10 item
So far i can't figure it out how to combine an API, this is my service setup
func getMessageList(page: Int) -> Single<[Message]> {
return platformBanking.getMessageList(token: sessionStore.token, page: page, pageSize: 10)
}
func getMoInbox() -> Single<[Message]> {
return Single.create { single in
MOInbox.sharedInstance.getInboxMessages { inboxMessages, accountMeta in
var messages: [Message] = []
inboxMessages.forEach { entry in
let message: Message = .init()
message.title = entry.notificationTitle
message.subtitle = entry.notificationSubTitle
message.body = entry.notificationBody
message.messageType = !(entry.campaignID?.isEmpty ?? false) ? 5 : 1
message.imageName = entry.notificationMediaURL ?? ""
message.date = entry.receivedDate?.string(withFormat: "dd MMM") ?? ""
message.isRead = entry.isRead
message.action = entry.deepLinkURL ?? ""
messages.append(message)
}
single(.success(messages))
}
return Disposables.create()
}
}
This is in my ViewModel
var filterMessages: [Message] = []
private var page: Int = 1
private var isLoading: Bool = false
private var endOfMessage: Bool = false
private func getMessageInboxList() {
var inboxMessages: [Message] = []
isLoading = true
Single.zip(manageMessages.getMessageList(page: page), manageMessages.getMoInbox())
.subscribe(onSuccess: { [weak self] firstMessage, secondMessage in
inboxMessages.append(contentsOf: firstMessage)
inboxMessages.append(contentsOf: secondMessage)
self?.processMessages(messages: inboxMessages)
}).disposed(by: disposedBag)
}
private func processMessages(messages: [Message]) {
self.messages.append(contentsOf: messages)
self.filterMessages = self.messages.sorted(by: { $0.date > $1.date })
eventShowHideLoadingIndicator.onNext(false)
if messages.count < 10 {
endOfMessage = true
}
eventMessagesDataUpdated.onNext(())
isLoading = false
}
This is a function to called pagination in viewModel, when i try paginate i just realize i make a duplicate item from getMoInbox API called. but still combining the object and limiting by 10 item i still can't find the answer.
func loadMoreMessageInbox() {
guard !endOfMessage, !isLoading, selectedIndex == 0 else { return }
page = page + 1
getMessageInboxList()
}
Please help me guys.
This requires a state machine. There are a number of different libraries that you could use (a partial list is at the bottom.)
Here is an example using the cycle function from my library.
enum Input {
case nextPageRequested // emit this to `input` when you are ready for the next page.
case pageReceived(Int, [Message]) // this is emitted with the page results.
}
struct State<T> {
var pages: [Int: [T]] = [:] // stores the pages as they come in. The MoMessages will be stored in page 0
}
func example(input: Observable<Input>, messageManager: MessageManager) -> Observable<[Message]> {
Single.zip(messageManager.getMoInbox(), messageManager.getMessageList(page: 1))
.asObservable()
.flatMap { moMessages, page1Messages in
// create state machine initialized with the moMessages and page1Messages
cycle(
input: input,
initialState: State(pages: [0: moMessages, 1: page1Messages]),
reduce: { state, input in
// when a new page is received, store it
if case let .pageReceived(page, messages) = input {
state.pages[page] = messages
}
},
reaction: reaction(
request: { state, input in
// when a new page is requested, figure out what page number you need and return it (otherwise return nil)
guard case .nextPageRequested = input else { return nil }
return state.pages.keys.max() + 1
},
effect: { page in
// get the page you need
messageManager.getMessageList(page: page)
.map { Input.pageReceived(page, $0) }
.asObservable()
}
)
)
}
.map { state in
// sort the messages in the pages and return them
state.pages.values.flatMap { $0 }.sorted(by: { $0.date > $1.date })
}
}
Here's that promised list:
My CLE library contains a state machine system.
RxFeedback is the OG tool, developed by the initial designer of RxSwift.
RxState is part of the RxSwiftCommunity.

Observable extension with generic type

Context
I want to wrap the Alamofire.upload into an observable and having info regarding the upload progress.
For that I have created a custom UploadElement that is an enum representing either the progress and the value or the result. So far I have:
enum UploadElement<Result> where Result: Codable {
case progress(Double)
case response(Result)
}
private func buildUploadRequest(url: URL, parts: [Data]) -> Observable<UploadRequest> {
let uploadRequest = manager.upload(
multipartFormData: { multipartFormData in /* build multipart */ },
to: url
)
return Observable.just(uploadRequest)
}
func upload<Result: Codable>(url: URL, parts: [Data]) -> Observable<UploadElement<Result>> {
buildUploadRequest(url: url, parts: parts)
.flatMap { request in
Observable<UploadElement<Result>>.create { observer in
request.response { response in
do {
observer.on(.next(.response(/* decode here */)))
observer.on(.completed)
} catch let error {
observer.on(.error(error))
}
}.uploadProgress { progress in
observer.on(.next(.progress(progress.fractionCompleted)))
}
.resume()
return Disposable.create { request.cancel() }
}
}
}
Now I would like to have an extension on an Observable<UploadEment<Result>> to have a nicer way to be notified.
Basically it would be:
service.upload(url: ..., parts: ...)
.progress { progress in /* */ }
.result { result in /* */ }
.subscribe()
.dispose(by: disposeBag)
To do that I tried:
extension ObservableType where Element == UploadElement<Resource> {
func progress(progressCompletion: #escaping (Double) -> Void) -> Self {
return self.do(onNext: { element in
switch element {
case .progress(let progress): progressCompletion(progress)
case .response: return
}
})
}
func result(resultCompletion: #escaping (Result) -> Void) -> Self {
return self.do(onNext: { element in
switch element {
case .response(let result): resultCompletion(result)
case .progress: return
}
})
}
}
I tried multiple variation of that but the errors that I get are:
Cannot find 'Result in scope'
Reference to generic type ... required argument
Is it possible to achieve something like that?
You just need to move the where clause from class scope down to function scope (shown below).
That said, I don't think breaking out of the monad like this in the middle of a stream is "a nicer way to be notified".
Better would be to break your Observable into two streams and subscribe to each of them:
extension ObservableType {
func progress<Resource>() -> Observable<Double> where Element == UploadElement<Resource> {
self.compactMap { element in
switch element {
case let .progress(progress):
return progress
case .response:
return nil
}
}
}
func result<Resource>() -> Observable<Resource> where Element == UploadElement<Resource> {
self.compactMap { element in
switch element {
case .progress:
return nil
case let .response(resource):
return resource
}
}
}
}
With the above you can now do something like this:
let response = service.upload(url: ..., parts: ...)
.share()
response
.progress()
.subscribe(onNext: { progress in /*...*/ })
.disposed(by: disposeBag)
response
.result()
.subscribe(onNext: { result in /*...*/ })
.dispose(by: disposeBag)
Now you don't have any empty subscribes.
I found something that is working:
extension ObservableType {
func progress<O: Codable>(progressCompletion: #escaping (Double) -> Void) -> Observable<UploadElement<O>> where Element == UploadElement<O> {
return self.do(onNext: { element in
if case .progress(let progress) = element {
progressCompletion(progress)
}
})
}
func response<O: Codable>(responseCompletion: #escaping (O) -> Void) -> Observable<UploadElement<O>> where Element == UploadElement<O> {
return self.do(onNext: { element in
if case .response(let response) = element {
responseCompletion(response)
}
})
}
}
Now I can use the "planned" api:
service.update(data: /* ... */)
.progress { progress in /* */ }
.response { result in /* */ }
.subscribe(
onError: { error in /* */ }
)
.dispose(by: disposeBag)
However as Daniel mentioned this might not be the "nicer way of being notified".

How to best create a publisher aggregate of #Published values in Combine?

Given an hierarchical structure of #OberservableObjects - I often find myself in a situation where I need a publisher which provides some kind of updated aggregate of the entire structure (the example below calculates a sum, but it could be anything)
Below is the solution I have come up with - which kinda works, but also not... :)
Problem #1: It looks way to complicated - and I feel I am missing something...
Problem #2: It does not work as the $foo publisher on top does emit changes to foo before foo changes, which are then not present in the second self.$foo publisher (which shows the old state).
Sometimes I need the aggregate in sync with swiftUI view updates - so that I need to utilize the #Published value and no separate publisher that emits during didSet of the variable.
I did not find a good solution... So how would you guys resolve this?
class Foo:ObservableObject {
#Published var bar:Int = 0
}
class Foobar:ObservableObject {
#Published var foo:[Foo] = []
var sumPublisher:AnyPublisher<Int,Never> {
// Whenever the foo array or one of the foo.bar values change
//
$foo
.map { fooArray in
Publishers.MergeMany( fooArray.map { foo in foo.$bar } )
}
.switchToLatest()
// Calclulate a new sum by collecting and reducing all foo.bar values.
//
.map { [unowned self] _ in
self.$foo // <--- in case of a foo change, this is still the unchanged foo, therefore not correct.
.map { fooArray -> AnyPublisher<Int,Never> in
Publishers.MergeMany( fooArray.map { foo in foo.$bar.first() } )
.collect()
.map { barArray -> Int in
barArray.reduce(0, { $0 + $1 })
}
.eraseToAnyPublisher()
}
.switchToLatest()
}
.switchToLatest()
.removeDuplicates()
.eraseToAnyPublisher()
}
}
Problem #2 : #Published fire signals on "willSet" and not "didSet".
You can use this extension :
extension Published.Publisher {
var didSet: AnyPublisher<Value, Never> {
self.receive(on: RunLoop.main).eraseToAnyPublisher()
}
}
and
self.$foo.didSet
.map { _ in
//...//
}
Problem #1 :
Maybe so :
class Foobar:ObservableObject {
#Published var foo:[Foo] = []
#Published var sum = 0
var cancellable: AnyCancellable?
init() {
cancellable =
sumPublisher
.sink {
self.sum = $0
}
}
var sumPublisher: AnyPublisher<Int,Never> {
let firstPublisher = $foo.didSet
.flatMap {array in
array.publisher
.flatMap { $0.$bar.didSet }
.map { _ -> [Foo] in
return self.foo
}
}
.eraseToAnyPublisher()
let secondPublisher = $foo.didSet
.dropFirst(1)
return Publishers.Merge(firstPublisher, secondPublisher)
.map { barArray -> Int in
return barArray
.map {$0.bar}
.reduce(0, { $0 + $1 })
}
.removeDuplicates()
.eraseToAnyPublisher()
}
}
And to test :
struct FooBarView: View {
#StateObject var fooBar = Foobar()
var body: some View {
VStack {
HStack {
Button("Change list") {
fooBar.foo = (1 ... Int.random(in: 5 ... 9)).map { _ in Int.random(in: 1 ... 9) }.map(Foo.init)
}
Text(fooBar.sum.description)
Button("Change element") {
let idx = Int.random(in: 0 ..< fooBar.foo.count)
fooBar.foo[idx].bar = Int.random(in: 1 ... 9)
}
}
List(fooBar.foo, id: \.bar) { foo in
Text(foo.bar.description)
}
.onAppear {
fooBar.foo = [1, 2, 3, 8].map(Foo.init)
}
}
}
}
EDIT :
If you really prefer to use #Published (the willSet publisher), it sends the new value of bar therefore you could deduce the new value of foo (the array) :
var sumPublisher: AnyPublisher<Int, Never> {
let firstPublisher = $foo
.flatMap { array in
array.enumerated().publisher
.flatMap { index, value in
value.$bar
.map { (index, $0) }
}
.map { index, value -> [Foo] in
var newArray = array
newArray[index] = Foo(bar: value)
return newArray
}
}
.eraseToAnyPublisher()
let secondPublisher = $foo
.dropFirst(1)
return Publishers.Merge(firstPublisher, secondPublisher)
.map { barArray -> Int in
barArray
.map { $0.bar }
.reduce(0, { $0 + $1 })
}
.removeDuplicates()
.eraseToAnyPublisher()
}
By far, the easiest approach here is to use a struct instead of a class with #Published:
struct Foo {
var bar: Int = 0
}
Then you can simply create a computed property:
class Foobar: ObservableObject {
#Published var foo: [Foo] = []
var sum: Int {
foo.map(\.bar).reduce(0, +)
}
// ...
}
For SwiftUI views, you wouldn't even need to make it a Publisher - when foo changes, because it's #Published, it will cause the View to access sum again, which would give it the recomputed value.
If you insist on it being a Publisher, it's still easy to do, since foo itself changes when any of its values Foo change (since they are value-type structs):
var sumPublisher: AnyPublisher<Int, Never> {
self.$foo
.map { $0.map(\.bar).reduce(0, +) }
.eraseToAnyPublisher()
}
Sometimes, it's not possible to change a class into a struct for whatever reason (maybe each class has its own life cycle that self-updates). Then you'd need to manually keep track of all the additions/removals of Foo objects in the array (via willSet or didSet), and subscribe to changes in their foo.bar.

How to use Combine collect method after a map

I would like to use the collect method of Combine to split an array of objects into Array of multiples Arrays which would correspond to Rows in a Collection View (eg: [Item, Item, Item, Item, Item] would become [[Item, Item, Item], [Item, Item]] and so on)
My data is coming from two publisher that I'm chaining to merge my data into a single type consumed by my view.
Here is my code :
APIClient().send(APIEndpoints.searchMovies(for: text)).flatMap { response -> AnyPublisher<APIResponseList<TVShow>, Error> in
movies = response.results.map { SearchItemViewModel(movie: $0)}
return APIClient().send(APIEndpoints.searchTVShows(for: text))
}
.map { response -> [SearchItemViewModel] in
tvShows = response.results.map { SearchItemViewModel(tvShow: $0)}
let concatItems = tvShows + movies
return concatItems.sorted { $0.popularity > $1.popularity }
}
.collect(3)
.sink(receiveCompletion: { (completion) in
switch completion {
case .failure:
self.state = .error
self.items = []
case .finished:
break
}
}, receiveValue: { (response) in
self.state = .data
self.items = response
})
.store(in: &disposables)
My problem currently is that inside my sink receiveValue closure, the response parameter doesn't equal the expected result, it just group all my items into an array like this : [Item, Item, Item, Item, Item] -> [[Item, Item, Item, Item, Item]]
It seems that the collect method is not working as expected, any idea about how I could fix this ?
it just group all my items into an array like this : [Item, Item, Item, Item, Item]
Pass that through a flatMap and generate its Sequence publisher. Now the Item objects will arrive one at a time and collect(3) will work as you expect.
Example:
var storage = Set<AnyCancellable>()
let head = Just([1,2,3,4,5]) // this is the same as your `.map`
head
.flatMap { $0.publisher }
.collect(3)
.sink{print($0)} // prove that it works: [1, 2, 3], then [4, 5]
.store(in: &storage)
Create this extension:
extension Array {
func split(numItems:Int) -> [[Element]] {
var i = 0
var ret = [[Element]]()
var current = [Element]()
while i < self.count {
current.append(self[i])
i += 1
if i % numItems == 0 {
ret.append(current)
current = []
}
}
if current.count > 0 {
ret.append(current)
}
return ret
}
}
Now, you should be able to do this:
APIClient().send(APIEndpoints.searchMovies(for: text)).flatMap { response -> AnyPublisher<APIResponseList<TVShow>, Error> in
movies = response.results.map { SearchItemViewModel(movie: $0)}
return APIClient().send(APIEndpoints.searchTVShows(for: text))
}
.map { response -> [SearchItemViewModel] in
tvShows = response.results.map { SearchItemViewModel(tvShow: $0)}
let concatItems = tvShows + movies
var sorted = concatItems.sorted { $0.popularity > $1.popularity }
return sorted.split(numItems:3)
}
.sink(receiveCompletion: { (completion) in
switch completion {
case .failure:
self.state = .error
self.items = []
case .finished:
break
}
}, receiveValue: { (response) in
self.state = .data
self.items = response
})
.store(in: &disposables)

Subscribing to fetch a nested array

I have an object and its properties as following:
class Section {
var cards: [MemberCard]
init(card: [MemberCard]) {
}
}
class MemberCard {
var name: String
var address: String?
init(name: String) {
self.name = name
}
}
I'm subscribing to a RxStream of type Observable<[Section]>. Before I subscribe I would to want flat map this function.
where the flat map would perform the following actions:
let sectionsStream : Observable<[Section]> = Observable.just([sections])
sectionsStream
.flatMap { [weak self] (sections) -> Observable<[Section]> in
for section in sections {
for card in section.cards {
}
}
}.subscribe(onNext: { [weak self] (sections) in
self?.updateUI(memberSections: sections)
}).disposed(by: disposeBag)
func getAddressFromCache(card: MemberCard) -> Observable<MemberCard> {
return Cache(id: card.name).flatMap ({ (card) -> Observable<MemberCard> in
asyncCall{
return Observable.just(card)
}
}
}
How would the flatmap look like when it comes to converting Observable<[Section]> to array of [Observable<MemberCard>] and back to Observable<[Section]>?
Technically, like that -
let o1: Observable<MemberCard> = ...
let o2: Observable<Section> = omc.toList().map { Section($0) }
let o2: Observable<[Section]> = Observable.concat(o2 /* and all others */).toList()
But I do not think it is an optimal solution, at least because there is no error handling for the case when one or more cards cannot be retrieved. I would rather build something around aggregation with .scan() operator as in https://github.com/maxvol/RaspSwift
Here you go:
extension ObservableType where E == [Section] {
func addressedCards() -> Observable<[Section]> {
return flatMap {
Observable.combineLatest($0.map { getAddresses($0.cards) })
}
.map {
$0.map { Section(cards: $0) }
}
}
}
func getAddresses(_ cards: [MemberCard]) -> Observable<[MemberCard]> {
return Observable.combineLatest(cards
.map {
getAddressFromCache(card: $0)
.catchErrorJustReturn($0)
})
}
If one of the caches emits an error, the above will return the MemberCard unchanged.
I have a couple of other tips as well.
In keeping with the functional nature of Rx, your Section and MemberCard types should either be structs or (classes with lets instead of vars).
Don't use String? unless you have a compelling reason why an empty string ("") is different than a missing string (nil). There's no reason why you should have to check existence and isEmpty every time you want to see if the address has been filled in. (The same goes for arrays and Dictionaries.)
For this code, proper use of combineLatest is the key. It can turn an [Observable<T>] into an Observable<[T]>. Learn other interesting ways of combining Observables here: https://medium.com/#danielt1263/recipes-for-combining-observables-in-rxswift-ec4f8157265f