reload UITableView inside closure causes strange behaviour - swift

I am using CloudKit to fetch CKRecords to populate a UITableView.
The operations Array acts as the datasource for the tableView
let predicate = NSPredicate(value: true)
let query = CKQuery(recordType: "Operation", predicate: predicate)
let queryOperation = CKQueryOperation(query: query)
operations = []
queryOperation.recordFetchedBlock = {
record in
self.operations.append(record)
}
queryOperation.queryCompletionBlock = {
cursor, error in
if error == nil {
dispatch_sync(dispatch_get_main_queue(), {self.tableView.reloadData()})
}
}
let database = CKContainer.defaultContainer().privateCloudDatabase
database.addOperation(queryOperation)
If i simply use self.tableView.reload() in the closure the tableView will update but there is a 5-10s delay unless I swipe the tableView where the cells will suddenly show. However when I dispatch on the main thread (as in the code above) the tableView cells will show but sometimes the top cell starts flashing or it renders very strange. I am wondering if anyone has any tips on how to implement the tableView.reloadData() inside this closure?
EDIT: This behaviour only seems to appear when i call the above code from viewDidLoad. If i call the above code after the tableview updates fine.

Related

Is applying NSDiffableDataSourceSnapshot broken?

I have a problem with applying NSDiffableDataSourceSnapshot to UICollectionViewDiffableDataSource. Imagine this situation: I have two items and I want to remove the second one and also I want to reload all other items in that section.
I do it like this:
var snapshot = oldSnapshot
if let item = getterForItemToDelete() {
snapshot.deleteItems([item])
}
snapshot.reloadItems(otherItems)
But then in cell provider of data source app crashes since it tries to get cell for item identifier for which I no longer have data so I have to return nil:
let dataSource = MyDataSource(collectionView: collectionView) { [weak self] collectionView, indexPath, identifier in
guard let viewModel = self?.viewModel.item(for: identifier) else { return nil }
...
}
What is strange is that when I try to debug it and print my items when I apply snapshot, it prints one item:
(lldb) print snapshot.itemIdentifiers(inSection: .mySection)
([App.MyItemIdentifier]) $R0 = 1 value {
[0] = item (item = "id")
}
But, immediately after this, in cell provider I get that I have two items, which I don't
(lldb) print self?.dataSource.snapshot().itemIdentifiers(inSection: .mySection)
([App.MyItemIdentifier]) $R1 = 2 values {
[0] = item (item = "ckufpa58100041ps6gmiu5zl6")
[1] = item (item = "ckufpa58100051ps69yrgaspv")
}
What is even more strange is that this doesn't happen when I have 3 items and I want to delete one and reload the others.
One workaround which solves my problem is to return empty cell instead of nil in cell provider:
let dataSource = MyDataSource(collectionView: collectionView) { [weak self] collectionView, indexPath, identifier in
guard let viewModel = self?.viewModel.item(for: identifier) else {
return collectionView.dequeueReusableCell(withReuseIdentifier: "UICollectionViewCell", for: indexPath)
}
...
}
And the last strange thing, when I do this and then I look to View Hierarchy debugger, there is just one cell, so it seems that the empty cell gets removed.
Does anybody know what could I do wrong or is this just expected behavior? Since I didn't find any mention of providing cells for some sort of optimalizations, animations or something in the documentation.
You shouldn't be removing items from the snapshot. Remove them from your array. Create the dataSource again with the updated array, and call the snapshot with this newly created dataSource. CollectionView will automatically update with the new data. To put it more simply, change the array, then applySnapshot() again.

Core data async fetch ends up on the main thread

I am trying to execute an asynchronous request as part of a search result updater in my app.
I wrote the following code
func updateSearchResults(for searchController: UISearchController) {
guard let text = searchController.searchBar.text else {return}
let threadingContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
threadingContext.parent = self.context
DispatchQueue.global(qos: .userInitiated).async {
let fetchRequest = MyObject.fetchRequest() as NSFetchRequest<MyObject>
fetchRequest.predicate = get_predicate(text)
do {
let objects = try threadingContext.fetch(fetchRequest).map({ object in
return object.objectID
})
}
catch {return}
DispatchQueue.main.async {
// Pass results to the search view controller
}
}
}
but the UI is still slow (even if I don't do any display update), and looking at the Time profiler, I see that my main thread is spending 80% of its time on the following:
So it seems that my request is still being dispatched onto the main thread, which I don't understand. Would anyone see my mistake?
(I tried a few various on the above e.g. using threadingContext.perform but for the same result)
Ok, I understood it, and I should have read Apple's documentation, but basically
If a context’s parent store is another managed object context, fetch and save operations are mediated by the parent context instead of a coordinator.
This is slightly subtle, but my construction would have been useful if the operations performed on the fetch request, rather than the fetch request itself, had been slow.
The solution is to set threadingContext.persistentStoreCoordinator instead.

RealmSwift RealmCollectionChange tableView crash

Hy, I'm trying to couple RealmNotifications with updating a tableView but for some reason this keeps generating multiple crashes on the tableView because of inconsistency between the number of sections that exist and what the realm notification has sent. This is the code I have for observing any changes on the Results<T>:
do {
let realm = try Realm()
sections = realm.objects(AssetSections.self).filter("isEnabled = true AND (assets.#count > 0 OR isLoading = true OR banners.#count > 0 OR tag == 'My Tools')").sorted(byKeyPath: "sort_order", ascending: true)
guard let sections = sections else { return }
// Watch on the asset sections
notificationToken = sections.observe { [weak self] (change: RealmCollectionChange) in
switch change {
case .initial: break
case .error(let error):
self?.handle(error: error)
case .update(_, let deletions, let insertions, let modifications):
self?.updatedModel.onNext((insertions: insertions, modifications: modifications, deletions: deletions))
}
}
} catch { }
The above code occurs on a ViewModel and a ViewController is observing those changes like so:
vm.updatedModel
.subscribe(onNext: { [weak self] (insertions, modifications, deletions) in
guard let `self` = self else { return }
self.tableView.beginUpdates()
self.tableView.insertSections(insertions, animationStyle: .none)
self.tableView.deleteSections(deletions, animationStyle: .none)
self.tableView.reloadSections(modifications, animationStyle: .none)
self.tableView.endUpdates()
})
.disposed(by: disposeBag)
I'm working with sections instead of Rows because this is a tableView with multiple sections and just one row per section.
The crash I'm getting is if I do a pull to refresh which does multiple network calls which in turn makes multiple changes to the objects. The funny thing is I can always almost replicate the crash if I scroll down rapidly during a pull to refresh. The error is the following:
Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'attempt to delete section 14, but there are only 14 sections before the update'
The way I get the numberOfSections for the tableView is the following:
var numberOfSections: Int {
return sections?.count ?? 0
}
My suspicion is that since the notifications are getting delivered on the next runLoop of the Main Thread and since I'm making the thread busy by scrolling and messing with the UI by the time I get a notification and the tableView reacts to it, it's already out of sync. But I'm not exactly sure if this is the problem or if it is how to solve it.
Thank you
Edit
One way to avoid this is just .reloadData() on the tableView but it's a perfomance hit especially on big datasets, and I can't use the default tableView animations. To diminish the perfomance hit of calling .reloadData() multiple times I'm using debounce.
vm.updatedModel
.debounce(1.0, scheduler: MainScheduler.instance)
.subscribe(onNext: { [weak self] (insertions, modifications, deletions) in
guard let `self` = self else { return }
self.tableView.reloadData()
})
.disposed(by: disposeBag)
Great question, and it looks like the Realm documentation does exactly as you do (with rows rather than sections) but doesn't address this. I can't see an obvious answer, but best I can do is these possible workarounds (although tbh, neither is great).
Rework the update code to pull together results, then do a single commit when they are complete.
Make a static copy of sections to use locally, and update it within the subscription. i.e. copy the RealmCollection to an array, which then is not a dynamic view into the realm. This will stay synchronised to your updates.
Best I can do. Otherwise, I can't see how you can guarantee synchronisation between the dynamic query and the notification.

How to display CloudKit RecordType instances in a tableview controller

To my knowledge, the following code (or very close to it) would retrieve one cloudkit instance from the recordtype array...
let pred = NSPredicate(value: true)
let query = CKQuery(recordType: "Stores", predicate: pred)
publicDatabase.performQuery(query, inZoneWithID: nil) { (result, error) in
if error != nil
{
print("Error" + (error?.localizedDescription)!)
}
else
{
if result?.count > 0
{
let record = result![0]
dispatch_async(dispatch_get_main_queue(), { () -> Void in
self.txtDesc.text = record.objectForKey("storeDesc") as? String
self.position = record.objectForKey("storeLocation") as! CLLocation
let img = record.objectForKey("storeImage") as! CKAsset
self.storeImage.image = UIImage(contentsOfFile: img.fileURL.path!)
....(& so on)
However, how and when (physical location in code?) would I query so that I could set each cell to the information of each instance in my DiningType record?
for instance, would I query inside the didreceivememory warning function? or in the cellforRowatIndexPath? or other!
If I am misunderstanding in my above code, please jot it down in the notes, all help at this point is valuable and extremely appreciated.
Without a little more information, I will make a few assumptions about the rest of the code not shown. I will assume:
You are using a UITableView to display your data
Your UITableView (tableView) is properly wired to your viewController, including a proper Outlet, and assigning the tableViewDataSource and tableViewDelegate to your view, and implementing the required methods for those protocols.
Your data (for each cell) is stored in some type of collection, like an Array (although there are many options).
When you call the code to retrieve records from the database (in this case CloudKit) the data should eventually be stored in your Array. When your Array changes (new or updated data), you would call tableView.reloadData() to tell the tableView that something has changed and to reload the cells.
The cells are wired up (manually) in tableView(:cellForRowAtIndexPath:). It calls this method for each item (provided you implemented the tableView(:numberOfRowsInSection:) and numberOfSectionsInTableView(_:)
If you are unfamiliar with using UITableView's, they can seem difficult at first. If you'd like to see a simple example of wiring up a UITableView just let me know!
First, I had to take care of the typical cloudkit requirements: setting up the container, publicdatabase, predicate, and query inputs. Then, I had the public database perform the query, in this case, recordtype of "DiningType". Through the first if statement of the program, if an error is discovered, the console will print "Error" and ending further action. However, if no run-time problem is discovered, each result found to be relatable to the query is appended to the categories array created above the viewdidload function.
var categories: Array<CKRecord> = []
override func viewDidLoad() {
super.viewDidLoad()
func fetchdiningtypes()
{
let container = CKContainer.defaultContainer()
let publicDatabase = container.publicCloudDatabase
let predicate = NSPredicate(value: true)
let query = CKQuery(recordType: "DiningType", predicate: predicate)
publicDatabase.performQuery(query, inZoneWithID: nil) { (results, error) -> Void in
if error != nil
{
print("Error")
}
else
{
for result in results!
{
self.categories.append(result)
}
NSOperationQueue.mainQueue().addOperationWithBlock( { () -> Void in
self.tableView.reloadData()
})
}
}
}
fetchdiningtypes()

Reloading single row in PFTableViewCell - Returns that PFTableViewCell does not have cellForRowMethod

Ok so I've been trying to make a like button for an app I'm working on and I'm using parse as a backend. So far I can update the like button in parse, however, I can only reload the entire tableview not just the single cell. Reloading the entire tableview sometimes causes the tableview to move up or down I believe because of different sized cells.
#IBAction func topButton(sender: UIButton) {
let uuid = UIDevice.currentDevice().identifierForVendor.UUIDString
let hitPoint = sender.convertPoint(CGPointZero, toView: self.tableView)
let hitIndex = self.tableView.indexPathForRowAtPoint(hitPoint)
let object = objectAtIndexPath(hitIndex)
let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: hitIndex!) as TableViewCell
//var indexOfCell:NSIndexPath
//indexOfCell = hitIndex!
if object.valueForKey("likedBy") != nil {
var UUIDarray = object.valueForKey("likedBy")? as NSArray
var uuidArray:[String] = UUIDarray as [String]
if !isStringPresentInArray(uuidArray, str: uuid) {
object.addObject(uuid, forKey: "likedBy")
object.incrementKey("count")
object.saveInBackground()
//self.tableView.reloadData()
}
}
}
I've already tried the cellForRow with the proper indexPath and it always returns that PFTableViewCell does not contain the cellForRowAtIndexPath method. I read on a forum that someone was able to create a custom method to reload the cell and Parse's documentation uses PAPCache to do the like button. Lucky for me the documentation for the like button on Parse's page is in Obj-C and I've coded my app in Swift. Here's the link to the page if you want to see: https://www.parse.com/tutorials/anypic#like. Section 6.1.
TD;LR I'm unsure how to update the cell so the entire tableview does not get reloaded without the cellForRowAtIndexPath method. Maybe Custom method? Maybe PAPCache method?
SOLVED
if object.valueForKey("likedBy") != nil {
var UUIDarray = object.valueForKey("likedBy")? as NSArray
var uuidArray:[String] = UUIDarray as [String]
if !isStringPresentInArray(uuidArray, str: uuid) {
object.addObject(uuid, forKey: "likedBy")
object.incrementKey("count")
object.saveInBackground()
let count = object.valueForKey("count") as Int?
//cell.count.text = "\(count)"
self.tableView.reloadRowsAtIndexPaths([hitIndex!], withRowAnimation: UITableViewRowAnimation.None)
This properly reloads only the cell, however, now my entire tableview always scrolls to the top which is not ideal.
I don't understand what are you trying to with the UUID; hitpoint ;hitIndex, and I don't understand what you want your app to do and I don't have and experience with parse ? I would comment on your question,but I need 50 rep to do this.Sorry that this isn't an answer,but a question .And if you are referring about cellForRowAtIndexPath,you need a UITableView not a cell.