How to use Combine collect method after a map - swift

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)

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.

Asynchronous iteration using Swift Combine

I am trying to do multiple async operations, in sequence, on an array of data. However I am having problems with the return values of map.
Here is the test code:
import Combine
func getLength(_ string: String) -> Future<Int,Error> {
return Future<Int,Error>{ promise in
print("Length \(string.count)")
promise(.success(string.count))
}
}
func isEven(_ int: Int) -> Future<Bool,Error> {
return Future<Bool,Error>{ promise in
print("Even \(int % 2 == 0)")
promise(.success(int % 2 == 0))
}
}
let stringList = ["a","bbb","c","dddd"]
func testStrings(_ strings:ArraySlice<String>) -> Future<Void,Error> {
var remaining = strings
if let first = remaining.popFirst() {
return getLength(first).map{ length in
return isEven(length)
}.map{ even in
return testStrings(remaining)
}
} else {
return Future { promise in
promise(.success(()))
}
}
}
var storage = Set<AnyCancellable>()
testStrings(ArraySlice<String>(stringList)).sink { _ in } receiveValue: { _ in print("Done") }.store(in: &storage)
This generates the following error:
error: MyPlayground.playground:26:11: error: cannot convert return expression of type 'Publishers.Map<Future<Int, Error>, Future<Void, Error>>' to return type 'Future<Void, Error>'
}.map{ even in
I thought we could use map to convert from one publisher type to the other, but it seems it's wrapped inside a Publishers.Map. How do I get rid of this?
Thanks!
Well it seems that this works:
import Combine
func getLength(_ string: String) -> Future<Int,Error> {
return Future<Int,Error>{ promise in
print("Length \(string.count)")
promise(.success(string.count))
}
}
func isEven(_ int: Int) -> Future<Bool,Error> {
return Future<Bool,Error>{ promise in
print("Even \(int % 2 == 0)")
promise(.success(int % 2 == 0))
}
}
let stringList = ["a","bbb","c","dddd"]
func testStrings(_ strings:ArraySlice<String>) -> AnyPublisher<Void,Error> {
var remaining = strings
if let first = remaining.popFirst() {
return getLength(first).flatMap{ length in
return isEven(length)
}.flatMap{ even in
return testStrings(remaining)
}.eraseToAnyPublisher()
} else {
return Future<Void,Error> { promise in
promise(.success(()))
}.eraseToAnyPublisher()
}
}
var storage = Set<AnyCancellable>()
testStrings(ArraySlice<String>(stringList)).sink { _ in } receiveValue: { _ in print("Done") }.store(in: &storage)

Refresh Observable in response to another

I have an observable that emits a list of CNContacts, and I want to reload the list when there is a change to the Contacts database (.CNContactStoreDidChange).
So the observable should emit a value on subscription, and whenever the other observable (the notification) emits a value. That sounds like combining them with withLatestFrom, but it doesn't emit anything.
let myContactKeys = [
CNContactIdentifierKey as CNKeyDescriptor,
CNContactFormatter.descriptorForRequiredKeys(for: .fullName)
]
func fetchContacts(by identifiers: [String],
contactKeys: [CNKeyDescriptor]) -> Observable<Event<[CNContact]>> {
return Observable<[String]>.just(identifiers)
.withLatestFrom(NotificationCenter.default.rx.notification(Notification.Name.CNContactStoreDidChange)) { ids, _ in ids}
.flatMap { ids in
Observable<[CNContact]>.create { observer in
let predicate = CNContact.predicateForContacts(withIdentifiers: ids)
do {
let contacts = try CNContactStore().unifiedContacts(matching: predicate, keysToFetch: contactKeys)
observer.onNext(contacts)
} catch {
observer.onError(error)
}
return Disposables.create()
}
.materialize()
}
.observeOn(MainScheduler.instance)
.share(replay: 1)
.debug()
}
fetchContacts(by: ["123"], contactKeys: myContactKeys)
.subscribe(
onNext: { contacts in
contacts.forEach { print($0.fullName) }
},
onError: { error in
print(error.localizedDescription)
})
.dispose(by: disposeBag)
The problem with your code is that you are starting with Observable<[String]>.just(identifiers) which will emit your identifiers and immediately complete. You don't want it to complete, you want it to continue to emit values whenever the notification comes in.
From your description, it sounds like you want something like the below. It emits whenever the notification fires, and starts with the contacts.
let myContactKeys = [
CNContactIdentifierKey as CNKeyDescriptor,
CNContactFormatter.descriptorForRequiredKeys(for: .fullName)
]
func fetchContacts(by identifiers: [String], contactKeys: [CNKeyDescriptor]) -> Observable<Event<[CNContact]>> {
func update() throws -> [CNContact] {
let predicate = CNContact.predicateForContacts(withIdentifiers: identifiers)
return try CNContactStore().unifiedContacts(matching: predicate, keysToFetch: contactKeys)
}
return Observable.deferred {
NotificationCenter.default.rx.notification(Notification.Name.CNContactStoreDidChange)
.map { _ in }
.map(update)
.materialize()
}
.startWith({ () -> Event<[CNContact]> in
do {
return Event.next(try update())
}
catch {
return Event.error(error)
}
}())
.share(replay: 1)
.debug()
}

Index out of Range exception when using firebase database and storage to download data

Here is my method to retrieve an array of user and post objects from the database.
func getRecentPost(start timestamp: Int? = nil, limit: UInt, completionHandler: #escaping ([(Post, UserObject)]) -> Void) {
var feedQuery = REF_POSTS.queryOrdered(byChild: "timestamp")
if let latestPostTimestamp = timestamp, latestPostTimestamp > 0 {
feedQuery = feedQuery.queryStarting(atValue: latestPostTimestamp + 1, childKey: "timestamp").queryLimited(toLast: limit)
} else {
feedQuery = feedQuery.queryLimited(toLast: limit)
}
// Call Firebase API to retrieve the latest records
feedQuery.observeSingleEvent(of: .value, with: { (snapshot) in
let items = snapshot.children.allObjects
let myGroup = DispatchGroup()
var results: [(post: Post, user: UserObject)] = []
for (index, item) in (items as! [DataSnapshot]).enumerated() {
myGroup.enter()
Api.Post.observePost(withId: item.key, completion: { (post) in
Api.User.observeUser(withId: post.uid!, completion: { (user) in
results.insert((post, user), at: index) //here is where I get my error -> Array index is out of range
myGroup.leave()
})
})
}
myGroup.notify(queue: .main) {
results.sort(by: {$0.0.timestamp! > $1.0.timestamp! })
completionHandler(results)
}
})
}
Here is the call to the method from my view controller. I am currently using texture UI to help with a faster smoother UI.
var firstFetch = true
func fetchNewBatchWithContext(_ context: ASBatchContext?) {
if firstFetch {
firstFetch = false
isLoadingPost = true
print("Begin First Fetch")
Api.Post.getRecentPost(start: posts.first?.timestamp, limit: 8 ) { (results) in
if results.count > 0 {
results.forEach({ (result) in
posts.append(result.0)
users.append(result.1)
})
}
self.addRowsIntoTableNode(newPhotoCount: results.count)
print("First Batch Fetched")
context?.completeBatchFetching(true)
isLoadingPost = false
print("First Batch", isLoadingPost)
}
} else {
guard !isLoadingPost else {
context?.completeBatchFetching(true)
return
}
isLoadingPost = true
guard let lastPostTimestamp = posts.last?.timestamp else {
isLoadingPost = false
return
}
Api.Post.getOldPost(start: lastPostTimestamp, limit: 9) { (results) in
if results.count == 0 {
return
}
for result in results {
posts.append(result.0)
users.append(result.1)
}
self.addRowsIntoTableNode(newPhotoCount: results.count)
context?.completeBatchFetching(true)
isLoadingPost = false
print("Next Batch", isLoadingPost)
}
}
}
In the first section of code, I have debugged to see if I could figure out what is happening. Currently, firebase is returning the correct number of objects that I have limited my query to (8). But, where I have highlighted the error occurring at, the index jumps when it is about to insert the fifth object, index[3] -> 4th object is in array, to index[7]-> 5th object about to be parsed and inserted, when parsing the 5th object.
So instead of going from index[3] to index[4] it jumps to index[7].
Can someone help me understand what is happening and how to fix it?
The for loop has continued on its thread while the observeUser & observePost callbacks are on other threads. Looking at your code, you can probably just get away with appending the object to the results array instead of inserting. This makes sense because you are sorting after the for loop anyway, so why does the order matter?

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

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.