UISearchBar: Predictive Search ReloadData crash - ios

In my app I've a searchbar to search 3 different type of items. These 3 items have different data sources so on each text change I've to send 3 diffrent network requests. I show these 3 type of items in 3 different sections in UITableView. When response of any request is received I update the data source and call tableView.reloadData. An example function to get one item (User) can be seen below:-
func searchUsers(withName name: String) {
showNetworkActivity()
UserService.search(query: name) { (data, error) in
Queue.Main.execute({
self.hideNetworkActivity()
if error == nil {
self.searchedUsers = data as! [User]
} else {
self.searchedUsers = []
}
self.searchTableView.reloadData()
})
}
}
But randomly my App is crashed due to index out of bound exception. The crash happens when new data is arrived but tableview is busy in reloading according to previously received data. I tried reloading only related section instead of reloading whole table but it causes more crashes. How can I avoid such crashes?
PS: Queue.Main ensures that all the things are being done on Main thread.

Related

Swift TableView Contents Disappear and Flash When Reloaded

I'm building a chat app with a table view of various channels that people can chat on.
If a new message (channel) comes in, I update my data store, and then reload the table view. In this particular instance, if a user accepts an invite to a channel; that channel gets added to the data store.
Every time a new channel gets added, this method gets called:
#objc func processChannels() {
if self.airlockStore.unPinnedChannelDataObjects.isEmpty == true {
return
} else {
let pinnedSection = self.tableViewAdaptor!.sections[0] as! TableViewAdaptorSection<ChannelTableViewCell, ChannelDataObject>
let unPinnedSection = self.tableViewAdaptor!.sections[1] as! TableViewAdaptorSection<ChannelTableViewCell, ChannelDataObject>
unPinnedSection.items = airlockStore.unPinnedChannelDataObjects
pinnedSection.items = airlockStore.pinnedChannelDataObjects
}
updateTableView()
}
And subsequently, this one gets called:
#objc func updateTableView() {
tableViewAdaptor?.tableView.isUserInteractionEnabled = true
tableViewAdaptor?.tableView.reloadData()
}
However, I consistently get a flash / the contents of the table view temporarily disappear before then being displayed in the manner expected.
Have read several posts regarding flashes when loading a table view, but haven't been able to solve this yet.
I was looking at reloading individual rows in the table view, but the problem is; there's no guarantee of where the index in my list of channels a new channel will be added to.
Have you tried not reloading after every update of tableview?
On the other hand, it may not be important but why you set isUserInteractionEnabled = true so many times.
Use this before and after adding/deleting row in table view:
tableview.beginupdates()
// adding/deleting row
tableview.endupdates()

Reloading data from a tableView within a collectionViewCell

I have a tableView inside a collectionViewCell and I get an error when I try to reload the data.
*** Assertion failure in -[UITableView _endCellAnimationsWithContext:], /BuildRoot/Library/Caches/com.apple.xbs/Sources/UIKit_Sim/UIKit-3600.6.21/UITableView.m:1610
I tried using Dispatch.main.async and it seems to get rid of the problem. The only thing is that it doesn't reload the data and nothing changes in the tableView
func cleanItems(completion: #escaping (Bool) -> Void) {
Dispatch.main.async {
cell.tableView.beginUpdates()
// Make changes in the Data Source
cell.tableView.deleteRows(at: selectedItems, with: .fade)
cell.tableView.endUpdates()
// Reloading sections accordingly depending on what the user has deleted
// Do I need to reload data here again? It used to work without Dispatch, but it wasn't stable
cell.tableView.reloadData()
// Updating items with if statements to reload data in Firebase
completion(true)
}
}
This doesn't reload the data at all and nothing seems to change. The good thing is that I don't get a random crash, which was the case before implementing Dispatch.main.async
I've retrieved the numberOfRows in each section to see how many rows there are after ending updates.
print(cell.tableView.numberOfRows(inSection: 1))
and I get the same number of rows that are in the current view.
This is crucial, because if the tableView sections are all zero, the collectionViewCell should disappear. And we never get here in the completion block as it says that the numberOfRows has never changed. Leaving us with a non updated tableView.
I solved this by moving Dispatch.main.async outside the function call.
Dispatch.main.async {
cleanItems(completion: { (success) in
etc.
})
}

How to deal with 'Assertion Failure' Collection View crashes in Firebase DatabaseUI in Swift?

I'm building a screen on my app where profiles will be displayed on a collection view. My data is being retrieved from Firebase and for the first time I'm using Firebase DatabaseUI.
I'm building out a query, then creating a FUICollectionViewDataSource, dequeuing my cells, and then plugging that dataSource into the collectionView.
The functionality of the dataSource works fine as long as I'm toggling between online and offline users (which checks the value of a child key on the query), but when I change the query to be from male to women, or women to men, (a new query itself) I'm crashing with an NSInternalInconsistenencyException.
*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Invalid update: invalid number of items in section 0. The number of items contained in an existing section after the update (1) must be equal to the number of items contained in that section before the update (1), plus or minus the number of items inserted or deleted from that section (1 inserted, 0 deleted) and plus or minus the number of items moved into or out of that section (0 moved in, 0 moved out).'
*** First throw call stack:
(0x18672d1b8 0x18516455c 0x18672d08c 0x1871e502c 0x18cedc14c 0x18c7b1d4c 0x10020f594 0x1002102c8 0x1001eb3e0 0x101609258 0x101609218 0x10160e280 0x1866da810 0x1866d83fc 0x1866062b8 0x1880ba198 0x18c64d7fc 0x18c648534 0x1000c4200 0x1855e95b8)
libc++abi.dylib: terminating with uncaught exception of type NSException
When I'm building out collection views, I'm used to just running commands like this:
collectionView.dataSource = collectionViewDataSource
self.collectionView.reloadData()
But now that calls like above don't work, and I'm not using UICollectionViewDataSource protocol functions, I'm unsure how to go about creating, running, and displaying new queries which will always have differing number of items per section.
What are the functions that I have to run before processing a new query to not deal with these kinds of crashes?
I've tried deleting all the cells from the collectionView's indexPathsForVisibleItems, and then reloading data... I've thrown around collectionView.reloadData() all over the place and still crashes. Not all the time, but the app will definitely crash sooner or later.
How do I go about starting a brand new query on a collectionView and not have to deal with NSInternalInconsistencyExceptions?
Thanks everyone. I love Firebase and would love to figure this out!
-Reezy
When the VC loads, the getUserData() function is called.
When the query is changed, the resetQuery() function is run from a posted notification.
extension BrowseVC {
func getUserData() {
let isOnline = (segmentedControl.selectedSegmentIndex == 0) ? true : false
generateQuery(isOnline: isOnline)
populateDataSource()
}
func resetQuery() {
removeOldData()
getUserData()
}
func removeOldData() {
let indexPaths = collectionView.indexPathsForVisibleItems
collectionView.deleteItems(at: indexPaths)
// collectionView.reloadData()
}
func generateQuery(isOnline: Bool) {
if let selectedShowMe = UserDefaults.sharedInstance.string(forKey: Constants.ProfileKeys.ShowMe) {
let forMen = (selectedShowMe == Constants.ProfileValues.Men) ? true : false
query = FirebaseDB.sharedInstance.buildQuery(forMen: forMen, isOnline: isOnline)
} else {
query = FirebaseDB.sharedInstance.buildQuery(forMen: false, isOnline: isOnline)
}
}
func populateDataSource() {
if let query = query {
collectionViewDataSource = FUICollectionViewDataSource(query: query, view: collectionView, populateCell: { (view, indexPath, snapshot) -> ProfileCVCell in
if let cell = view.dequeueReusableCell(withReuseIdentifier: Constants.CollectionViewCellIdentifiers.ProfileCVCell, for: indexPath) as? ProfileCVCell {
if let userDictionary = snapshot.value as? [String: AnyObject] {
if let user = User(uid: snapshot.key, userDictionary: userDictionary) {
cell.populate(withUser: user)
}
}
return cell
}
return ProfileCVCell()
})
collectionView.dataSource = collectionViewDataSource
collectionView.reloadData()
// collectionView.performBatchUpdates({
// self.collectionView.dataSource = self.collectionViewDataSource
// }, completion: nil)
// collectionView.reloadData()
}
}
}
The reason you're running into this is the FirebaseUI Database data sources assume responsibility for pushing updates to the collection view itself. This way you get animated updates for free and shouldn't ever have to call reloadData.
When you swap out the old data source for a new one, you need to set the data source's collection view to nil so it stops pushing events to the collection view.

Why my cells do not load at the same time?

I'm using background thread for getting my data from the network and main thread for presenting my cells:
dispatch_async(dispatch_get_global_queue(QOS_CLASS_BACKGROUND, 0), {
let _ = self.http.getInfo() { (result) in
self.cellsArray = result
dispatch_async(dispatch_get_main_queue(), {
self.tableView.reloadData()
})
completion(completed: true)
}
})
where self.http.getInfo is a NSURLSession.sharedSession().dataTaskWithRequest method for taking my network infos.
And when I run my app my cells start showing separately as: the first appears, in a second the next one appears, in 2 seconds the next one and etc.
Why they do not appear at the same time? And also, because of the background and main thread using, my UITableView jumps when I implement infinite scroll and append new cells to exist cell array.
How can I fix it?
write self.tableView.reloadData() this statement in completion handler of NSURLSession.sharedSession().dataTaskWithRequest.

Parse + iOS: 101 error when saving after a delete operation

In my iOS app, I have a PFQueryCollectionViewController that displays UserPhoto objects. Tapping on a collection view cell opens a photo detail page where the user can add comments to the photo. This workflow works perfectly with no errors until I delete a UserPhoto either from the collection view controller (using removeObjectsAtIndexPaths) or the detail view controller (using deleteInBackgroundWithBlock).
The scope of the issue is broad. When I attempt to perform any kind of save operation on any existing object (not just UserPhoto) after performing a delete operation, I get:
"[Error]: object not found for update (Code: 101, Version: 1.11.0)"
I've triple checked that I'm attempting to save the correct object, and not the one that was just deleted
I can log out the objects I'm trying to save and they are well-formed.
The object I'm attempting to save exists on the Parse dashboard
The ACL for all objects is Public Read + Write
The save operations that fail after delete work fine otherwise, so there is nothing wrong with the code that does the saving
The only way to restore save functionality after a delete operation is completely quit the app.
I'm dying here. Thanks.
Source Code
First, a simple function for looping through an array of selected photos from the collectionView and calling removeObjectsAtIndexPaths. Although removeObjectsAtIndexPaths is somewhat of a mystery (limited documentation), I have confirmed that it immediately deletes the objects from Parse.
func deleteSelectedPhotos() {
var indexPaths = [NSIndexPath]()
for image in self.selectedImages {
let index = self.objects.indexOf({ (photo: AnyObject) -> Bool in
return image == photo as! PFObject
})
let indexPath = NSIndexPath(forItem: index!, inSection: 0)
indexPaths.append(indexPath)
}
self.removeObjectsAtIndexPaths(indexPaths)
}
And this is the delete code for the photo detail screen:
func deleteUserPhoto() {
currentPhoto.deleteInBackgroundWithBlock { (deleted: Bool, error: NSError?) -> Void in
if let error = error {
print("error deleting photo")
print(error.localizedDescription)
} else {
if deleted == true {
self.delegate?.photoDetailVcDidDeletePhoto(self.currentPhoto)
self.navigationController?.popViewControllerAnimated(true)
}
}
}
}
The delegate method near the bottom is called by the PFQueryCollectionViewController so it can loadObjects and update the UI to reflect the deletion.
I think I resolved this. When I was creating new UserPhoto objects, I was adding them to an array on the parent object—an Album—and saving the Album without a completion block. I wasn't actually using the array (instead just querying UserPhoto objects with a pointer to the Album).
When I removed the code to add the photo to the unused array, I was able to update and create UserPhoto objects again. I suspect that the error was referring to the Album object which was not finished saving(?).
Leaving the Album completely out of the process of updating UserPhotos seems to have done it.

Resources