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

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?

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.

Reduce array of properties ReactiveSwift

I have an array of Property<Int>, and I need to reduce them to get sum of the last inputs (it's basically an unread notification counter from different SDK's) and put that into new Property<Int>, I tried this
let unseen: Property<Int> = .init(
initial: 0,
then: countExtractor(counters: counters)
)
func countExtractor(counters: [Property<Int>]) -> SignalProducer<Int?, Never> {
SignalProducer { observer, lifetime in
guard !lifetime.hasEnded else {
return
}
let producers = counters.map { $0.producer }
lifetime += SignalProducer<SignalProducer<Int, Never>, Never>(producers)
.flatten(.latest)
.reduce(0) { previous, current in
return (previous ?? 0) + current
}
.start(observer)
}
}
And it's working, but the values are not updating (when I hard code only one property everything is working correctly)
The answer was so simple
let properties = Property<Int>.combineLatest(counters)
let count = properties?.producer.map {
$0.reduce(0, +)
}
guard let count = count else {
return SignalProducer(value: 0)
}
return count

Swift Completion Handler For Loop to be performed once instead of 10 times due to the loop

I I have a loop with a firestore query in it that is repeated 10 times. I need to call the (completion: block) after all the 10 queries completed; Here I have my code so that it performs the (completion: block) per query but this would be too heavy on the server and the user's phone. How can I change below to accomplish what I just described?
static func getSearchedProducts(fetchingNumberToStart: Int, sortedProducts: [Int : [String : Int]], handler: #escaping (_ products: [Product], _ lastFetchedNumber: Int?) -> Void) {
var lastFetchedNumber:Int = 0
var searchedProducts:[Product] = []
let db = Firestore.firestore()
let block : FIRQuerySnapshotBlock = ({ (snap, error) in
guard error == nil, let snapshot = snap else {
debugPrint(error?.localizedDescription)
return
}
var products = snapshot.documents.map { Product(data: $0.data()) }
if !UserService.current.isGuest {
db.collection(DatabaseRef.Users).document(Auth.auth().currentUser?.uid ?? "").collection(DatabaseRef.Cart).getDocuments { (cartSnapshot, error) in
guard error == nil, let cartSnapshot = cartSnapshot else {
return
}
cartSnapshot.documents.forEach { document in
var product = Product(data: document.data())
guard let index = products.firstIndex(of: product) else { return }
let cartCount: Int = document.exists ? document.get(DatabaseRef.cartCount) as? Int ?? 0 : 0
product.cartCount = cartCount
products[index] = product
}
handler(products, lastFetchedNumber)
}
}
else {
handler(products, lastFetchedNumber)
}
})
if lastFetchedNumber == fetchingNumberToStart {
for _ in 0 ..< 10 {
//change the fetching number each time in the loop
lastFetchedNumber = lastFetchedNumber + 1
let productId = sortedProducts[lastFetchedNumber]?.keys.first ?? ""
if productId != "" {
db.collection(DatabaseRef.products).whereField(DatabaseRef.id, isEqualTo: productId).getDocuments(completion: block)
}
}
}
}
as you can see at the very end I am looping 10 times for this query because of for _ in 0 ..< 10 :
if productId != "" {
db.collection(DatabaseRef.products).whereField(DatabaseRef.id, isEqualTo: productId).getDocuments(completion: block)
}
So I need to make the completion: block handler to be called only once instead of 10 times here.
Use a DispatchGroup. You can enter the dispatch group each time you call the async code and then leave each time it's done. Then when everything is finished it will call the notify block and you can call your handler. Here's a quick example of what that would look like:
let dispatchGroup = DispatchGroup()
let array = []
for i in array {
dispatchGroup.enter()
somethingAsync() {
dispatchGroup.leave()
}
}
dispatchGroup.notify(queue: .main) {
handler()
}

Paginate posts to collection view with firebase & swift

I'm trying to paginate posts that users are following to my collection view from my firebase database. Currently only 4 posts are being appended to the collection view and not loading anymore when I scroll down.
I've tried changing the number of posts loaded initially with no luck.
fileprivate func fetchFollowingUserIds() {
guard let uid = Auth.auth().currentUser?.uid else { return }
Database.database().reference().child("user-following").child(uid).observeSingleEvent(of: .value, with: { (snapshot) in
guard let userIdsDictionary = snapshot.value as? [String: Any] else { return }
userIdsDictionary.forEach({ (key, value) in
Database.fetchUserWithUID(uid: key, completion: { (user) in
self.fetchPostsWithUser(user: user)
})
})
}) { (err) in
print("Failed to fetch following user ids:", err)
}
}
var posts = [Post]()
fileprivate func fetchPosts() {
guard let uid = Auth.auth().currentUser?.uid else { return }
Database.fetchUserWithUID(uid: uid) { (user) in
self.fetchPostsWithUser(user: user)
}
}
var isFinishedPaging = false
fileprivate func fetchPostsWithUser(user: User) {
self.collectionView?.refreshControl?.endRefreshing()
let ref = Database.database().reference().child("posts").child(user.uid)
var query = ref.queryOrdered(byChild: "creationDate")
if posts.count > 0 {
let value = posts.last?.creationDate.timeIntervalSince1970
query = query.queryEnding(atValue: value)
}
query.queryLimited(toLast: 4).observeSingleEvent(of: .value, with: { (snapshot) in
guard var allObjects = snapshot.children.allObjects as? [DataSnapshot] else { return }
allObjects.reverse()
if allObjects.count < 4 {
self.isFinishedPaging = true
} else {
self.isFinishedPaging = false
}
if self.posts.count > 0 && allObjects.count > 0 {
allObjects.removeFirst()
}
allObjects.forEach({ (snapshot) in
guard let dictionary = snapshot.value as? [String: Any] else { return }
var post = Post(user: user, dictionary: dictionary)
post.id = snapshot.key
self.posts.append(post)
})
self.collectionView?.reloadData()
}) { (err) in
print(err)
}
}
I simply want it to load more posts when the user scrolls down.
There may be a query issue or a potential logic issue. Lets assume you want to present posts to the user, with the most recent at the top of the list and allow the user to scroll down to see earlier posts.
Let's address both with an example:
We don't have your structure but keeping it super simple, suppose your posts have the following structure with creation dates
post_0
creation_date: "20180101"
post_1
creation_date: "20180102"
post_2
creation_date: "20180103"
post_3
creation_date: "20180104"
post_4
creation_date: "20180105"
post_5
creation_date: "20180106"
post_6
creation_date: "20180107"
post_7
creation_date: "20180108"
Here's your initial query, order by creation date, which will load the last 4 posts from the 5th to the 8th
var query = ref.queryOrdered(byChild: "creationDate")
Then subsequent queries are ordered by creation date but the ending value is not the creation date but the time elapsed since 1970 of the creation date.
let value = posts.last?.creationDate.timeIntervalSince1970
var query = ref.queryOrdered(byChild: "creationDate").queryEnding(atValue: value)
I would guess you just want to load the next 4 earlier posts. So as this sit in the array, they look like this:
20180108
20180107
20180106
20180105
One way to do that is the get the creationDate of the last post from your dataSource (which will be the oldest post)
20180105
Then query by creationDate, endingAt the creation date of the last post, getting 5 total posts, then remove the last one
20180101
20180102
20180103
20180104
20180105
then reversed
20180105
20180104
20180103
20180102
20180101
and remove the first
20180104
20180103
20180102
20180101
something like this
let lastD = self.postsArray.last
self.postsArray = []
let postsRef = self.ref.child("posts")
let queryRef = postsRef.queryOrdered(byChild: "creation_date")
let queryEndingRef = queryRef.queryEnding(atValue: lastD)
let queryLimitedRef = queryEndingRef.queryLimited(toLast: 5)
queryLimitedRef.observeSingleEvent(of: .value, with: { snapshot in
guard var thisArray = snapshot.children.allObjects as? [DataSnapshot] else { return }
thisArray.reverse()
thisArray.removeFirst()
for post in thisArray {
let theDate = post.childSnapshot(forPath: "creation_date").value as! String
self.postsArray.append(theDate)
}
})

How to use the when function in Promisekit loop

I have an array of appointments and I'm trying to grab all of the photos for these appointments from our windows azure blob storage. First, I want to get the list of blobs with the associated appointmentId so I can download and store them properly afterwards.
I'm using PromiseKit but I'm not at all sure about how to use PromiseKit in a loop:
for appointment in appointments {
// Get blobs
}
Here's my code so far. Any help is greatly appreciated!
func getBlobsPromise(appointmentId: Int32) -> Promise<[BlobDownload]> {
return Promise { seal in
var error: NSError?
var blobDownloads = [BlobDownload]()
container = AZSCloudBlobContainer(url: URL(string: containerURL)!, error: &error)
if ((error) != nil) {
print("Error in creating blob container object. Error code = %ld, error domain = %#, error userinfo = %#", error!.code, error!.domain, error!.userInfo)
seal.reject(error!)
}
let prefix: String = "AppointmentFiles/\(appointmentId)"
container?.listBlobsSegmented(with: nil, prefix: prefix, useFlatBlobListing: true, blobListingDetails: AZSBlobListingDetails(), maxResults: 150) { (error : Error?, results : AZSBlobResultSegment?) -> Void in
if error != nil {
seal.reject(error!)
}
for blob in results!.blobs!
{
let blobInfo = blob as! AZSCloudBlob
if blobInfo.blobName.lowercased().contains("jpg") || blobInfo.blobName.lowercased().contains("jpeg") {
let blobDownload: BlobDownload = BlobDownload(appointmentId: Int(jobId), blob: blobInfo)
blobDownloads.append(blobDownload)
}
}
seal.fulfill(blobDownloads)
}
}
}
That returns the blobs as expected but I want to get all of the blobs for all of the appointments before proceeding. Here's what I tried (among other things):
func getBlobsForAllJobs(appointmentIds: [Int32]) -> Promise<[BlobDownload]> {
return Promise { seal in
let count = appointmentIds.count - 1
let promises = (0..<count).map { index -> Promise<[BlobDownload]> in
return getBlobsPromise(agencyCode: agencyCode, appointmentId: appointmentIds[index])
}
when(fulfilled: promises).then({ blobDownloads in
seal.fulfill(blobDownloads)
})
}
}
EDIT 1
I solved this using a DispatchGroup and completion handler. Here's the code in case someone is interested. If there are alternate (better) ways of doing this I'd love to hear them. I'm a c# guy just getting into Swift.
func getBlobsToDownload(appointmentIds: [Int32], completion: #escaping ([BlobDownload]) -> Void) {
var myBlobsToDownload = [BlobDownload]()
let myGroup = DispatchGroup()
for apptId in appointmentIds {
myGroup.enter()
getBlobs(appointmentId: apptId) { (blobDownloads) in
print("Finished request \(apptId)")
print("Blobs fetched from apptId \(apptId) is \(blobDownloads.count)")
for blobDownload in blobDownloads {
myBlobsToDownload.append(blobDownload)
}
myGroup.leave()
}
}
myGroup.notify(queue: .main) {
print("Finished all requests.")
completion(myBlobsToDownload)
}
}