Why tableview datasource not changing from Realm Collection observer? - ios

I have the request to realm database
func allContacts() -> Results<RMContact> {
let realm = self.encryptedRealm()
return realm!.objects(RMContact.self).sorted(byKeyPath: "lastActive", ascending: false)
}
And code from presenter
DispatchQueue.main.async {
let contacts = RMContactsManager.shared.allContacts()
self.notificationToken = contacts.observe { [weak self] (changes: RealmCollectionChange) in
guard let tableView = self?.view.tableView else { return }
switch changes {
case .initial:
UIView.performWithoutAnimation {
tableView.reloadData()
}
case .update(_, let deletions, let insertions, let modifications):
UIView.performWithoutAnimation {
tableView.beginUpdates()
tableView.insertRows(at: insertions.map({ IndexPath(row: $0, section: 0) }), with: .automatic)
tableView.deleteRows(at: deletions.map({ IndexPath(row: $0, section: 0)}), with: .automatic)
tableView.reloadRows(at: modifications.map({ IndexPath(row: $0, section: 0) }), with: .automatic)
tableView.endUpdates()
}
case .error(let error):
fatalError("\(error)")
}
}
}
After setting new lastActive value sequence in tableview didn't change
For the first time sorting is actual for the controller, but after setting new values to lastActive property no changes. Is it an observer problem?

The issue seems to be that you are getting changes in terms of insertions, deletions and modifications.
If you will first do the insertion and then deletion, (This is what you are doing right now), It will insert new values and the datasource will be modified so now when you delete the rows, it will not be correct.
Also in your case, where you are sorting the array, generally two elements will be replaced, one being deleted and other will be added.
So what can solve your problem is, first do deletions and then do insertions. Like this:
UIView.performWithoutAnimation {
tableView.beginUpdates()
tableView.deleteRows(at: deletions.map({ IndexPath(row: $0, section: 0)}), with: .automatic)
tableView.insertRows(at: insertions.map({ IndexPath(row: $0, section: 0) }), with: .automatic)
tableView.reloadRows(at: modifications.map({ IndexPath(row: $0, section: 0) }), with: .automatic)
tableView.endUpdates()
}

Related

Table view - empty background space while delete items

While I delete a row from tableview it renders content in a strange way with empty space below and above - see video record and bug image:
deformed table view after call deletion
Probably, it may be caused by constraints changes (top view expanding / collapsing), or incorrect delete operations.
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let contentOffSetY = scrollView.contentOffset.y
switch true {
case contentOffSetY < 0 && searchViewIsHidden && !topMainViewIsHidden:
changeSearchVisibility(false)
case contentOffSetY > 0 && !searchViewIsHidden && !topMainViewIsHidden:
changeSearchVisibility(true)
case contentOffSetY > maxHeaderHeight && searchViewIsHidden && !topMainViewIsHidden:
transformTopView(toBig: false)
case self.contentOffSetY <= 0 && searchViewIsHidden && topMainViewIsHidden:
transformTopView(toBig: true)
default:
break
}
}
// Indexes of items that should be changed from Realm Observer
func updateTableView(_ deletions: [Int],
_ insertions: [Int], _ modifications: [Int]) {
tableView.beginUpdates()
tableView.insertRows(at: insertions.map({ IndexPath(row: $0, section: 0) }),
with: .none)
tableView.deleteRows(at: deletions.map({ IndexPath(row: $0, section: 0)}),
with: .none)
tableView.reloadRows(at: modifications.map({ IndexPath(row: $0, section: 0) }),
with: .none)
tableView.endUpdates()
}
Maybe not a perfect solution but you could try redrawing the entire tableview after deleting a row. With reloadData()
Let me know if this works for you.

EXC_BAD_ACCESS (code=EXC_I386_GPFLT) when deleteRows(at: [IndexPath])

I'm developing an app which has a main tableview in home screen. When I tap on a certain type of cell of the tableview, I do a little animation and then I delete the cell. I tried the behaviour on all simulator. Only on 4,7" and 5,5" I get a crash when I try to delete a certain row, systematically. I can't understand the error, since Xcode only says: EXC_BAD_ACCESS (code=EXC_I386_GPFLT)
This is my code:
tableView.deselectRow(at: indexPath, animated: false)
let incomingReminderTableViewCell = cell as! IncomingReminderTableViewCell
self.tableView.isUserInteractionEnabled = false
incomingReminderTableViewCell.completeReminderAt(indexPath: indexPath) { (indexPath) in
let selectedReminder = self.incomingReminders.reversed()[indexPath.row-1]
if CoreDataManager.sharedInstance.delete(selectedReminder) {
UserNotificationManager.decreaseBadge()
UserNotificationManager.deleteNotificationsWith(identifiers: [selectedReminder.idNotification])
self.incomingReminders = self.incomingReminders.reversed()
self.incomingReminders.remove(at: indexPath.row-1)
self.incomingReminders = self.incomingReminders.reversed()
if self.incomingReminders.isEmpty {
UIView.animate(withDuration: 0.2, animations: {
incomingReminderTableViewCell.alpha = 0.0
}, completion: { (disappeared) in
self.tableView.reloadRows(at: [indexPath], with: .fade )
self.tableView.isUserInteractionEnabled = true
})
} else {
UIView.animate(withDuration: 0.2, animations: {
incomingReminderTableViewCell.alpha = 0.0
}, completion: { (disappeared) in
self.tableView.isUserInteractionEnabled = true
self.tableView.deleteRows(at: [indexPath], with: .right) // HERE THE CRASH
self.incomingReminders = CoreDataManager.sharedInstance.getIncomingReminders()
if self.incomingReminders.count == 5 {
Timer.scheduledTimer(withTimeInterval: 0.5, repeats: false, block: { (timer) in
self.tableView.insertRows(at: [IndexPath(row: self.incomingReminders.count, section: 0)], with: .fade)
timer.invalidate()
})
}
})
}
} else {
self.tableView.isUserInteractionEnabled = true
}
}
When app crash I have indexPath = (row: 3, section: 0), that is right. DataSource has been correctly updated, before deleteRows I have two element in the array and the data in the tableview start at indexPath = (row: 1, section: 0). Can't find a solution.
Not so convinced, but I've solved in this way:
tableView.performBatchUpdates({
tableView.deleteRows(at: [indexPath], with: .right)
}, completion: { (deleted) in
tableView.reloadData()
self.tableView.isUserInteractionEnabled = true
self.incomingReminders = CoreDataManager.sharedInstance.getIncomingReminders()
if self.incomingReminders.count == 5 {
Timer.scheduledTimer(withTimeInterval: 0.5, repeats: false, block: { (timer) in
self.tableView.beginUpdates()
self.tableView.insertRows(at: [IndexPath(row: self.incomingReminders.count, section: 0)], with: .fade)
self.tableView.endUpdates()
timer.invalidate()
})
}
})
The problem is after deleted a certain number of rows in tableview, it remained dirty. In fact, deleting a bunch of rows, scrolling, and then continuing to delete, crash didn't happen. I needed to reload data. I put
tableView.deleteRows(at: [indexPath], with: .right)
as a batch update. And in completion block I called the reload, with other things. All seems ok for now. Hope this can be useful for someone.

How to reload a specific cell with out animation in swift 3?

I am trying to like a post in a tableview, after getting response i need to update like status in my tableview without knowing to user that table is reloading.
let indexPath = IndexPath(item: tag, section: 0)
self.tableCommunity.reloadRows(at: [indexPath], with: .fade)
buddy try using code :-
let indexPath = IndexPath(item: tag, section: 0)
self.tableCommunity.reloadRows(at: [indexPath], with: .none)
Hope it will work.

'NSInternalInconsistencyException' Inserting Item with Collection View Swift 3

So, I am using Realm as a data store, which I'm pretty sure I need to first add content to before inserting an item at index path in a collection view. But I keep getting this all too familiar error:
'NSInternalInconsistencyException', reason: 'attempt to insert item 1 into section -1, but there are only 1 items in section 1 after the update'
Here is my model:
final class Listing: Object {
dynamic var id = ""
dynamic var name = ""
dynamic var item = ""
}
Here is my view controller that conforms to UICollectionView data sources and delegates:
override func viewDidLoad() {
super.viewDidLoad()
// MARK: - Get Listings!
queryListings()
// MARK: - Delegates
self.collectionView.delegate = self
self.collectionView.dataSource = self
}
// MARK: - Query Listings
func queryListings() {
let realm = try! Realm()
let everyListing = realm.objects(Listing.self)
let listingDates = everyArticle.sorted(byKeyPath: "created", ascending: false)
for listing in listingDates {
listing.append(listing)
self.collectionView.performBatchUpdates({
self.collectionView.insertItems(at: [IndexPath(item: self.listing.count, section: 1)])
}, completion: nil)
}
}
Delegates:
// MARK: UICollectionViewDataSource
func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return listing.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath) as! ListingCollectionViewCell
cell.awakeFromNib()
return cell
}
I've tried every permutation of self.listing.count 0, 1, -1 , +1 as well as section 0, 1, -1, +1 and the exception raised is the same plus or minus the section and items that exist. Calling reloadData() doesn't help either.
Anyone solve this with a collection view?
Solved
With Realm, the mindset is different than what I'm accustomed to-- you're manipulating data that effects the table or collection, not the table or collection directly. Sounds obvious, but... anyway, TiM's answer is correct. Here's the collection view version:
// MARK: - Observe Results Notifications
notificationToken = articles.addNotificationBlock { [weak self] (changes: RealmCollectionChange) in
guard (self?.collectionView) != nil else { return }
// MARK: - Switch on State
switch changes {
case .initial:
self?.collectionView.reloadData()
break
case .update(_, let deletions, let insertions, let modifications):
self?.collectionView.performBatchUpdates({
self?.collectionView.insertItems(at: insertions.map({ IndexPath(row: $0, section: 0)}))
self?.collectionView.deleteItems(at: deletions.map({ IndexPath(row: $0, section: 0)}))
self?.collectionView.reloadItems(at: modifications.map({ IndexPath(row: $0, section: 0)}))
}, completion: nil)
break
case .error(let error):
print(error.localizedDescription)
break
}
}
The lines of code for listing in listingDates { listing.append(listing) } seem a bit unsafe. Either you're referring to separate objects named listing (such as a class property), or that's in reference to the same listing object. If listing is a Realm Results object, it shouldn't be possible to call append on it.
In any case, you're probably doing a bit more work than you need to. Realm objects, whether they are Object or Results are live, in that they'll automatically update if the underlying data changes them. As such, it's not necessary to perform multiple queries to update a collection view.
Best practice is to perform the query once, and save the Results object as a property of your view controller. From that point, you can use Realm's Change Notification feature to assign a Swift closure that'll be executed each time the Realm query changes. This can then be used to animate the updates on the collection view:
class ViewController: UITableViewController {
var notificationToken: NotificationToken? = nil
override func viewDidLoad() {
super.viewDidLoad()
let realm = try! Realm()
let results = realm.objects(Person.self).filter("age > 5")
// Observe Results Notifications
notificationToken = results.addNotificationBlock { [weak self] (changes: RealmCollectionChange) in
guard let tableView = self?.tableView else { return }
switch changes {
case .initial:
// Results are now populated and can be accessed without blocking the UI
tableView.reloadData()
break
case .update(_, let deletions, let insertions, let modifications):
// Query results have changed, so apply them to the UITableView
tableView.beginUpdates()
tableView.insertRows(at: insertions.map({ IndexPath(row: $0, section: 0) }),
with: .automatic)
tableView.deleteRows(at: deletions.map({ IndexPath(row: $0, section: 0)}),
with: .automatic)
tableView.reloadRows(at: modifications.map({ IndexPath(row: $0, section: 0) }),
with: .automatic)
tableView.endUpdates()
break
case .error(let error):
// An error occurred while opening the Realm file on the background worker thread
fatalError("\(error)")
break
}
}
}
deinit {
notificationToken?.stop()
}
}

insert row at index path call scroll to top table view?

When i insert row at index path in uitableview, then my tableview scroll to top? Why?
let indexPathForCell = NSIndexPath(forRow: 5, inSection: 1)
tableView.beginUpdates()
tableView.insertRowsAtIndexPaths([indexPathForCell], withRowAnimation: .Automatic)
tableView.endUpdates()
All code that is invoked during the addition of the cell
func buttonDidPressed(button: CheckMarkView) {
let indexPathForCell = NSIndexPath(forRow: 5, inSection: 1)
buttonPressedTag = button.tag
for checkMark in buttons {
if checkMark.tag == buttonPressedTag {
if buttonPressedTag == 4 {
checkMark.show()
checkMark.userInteractionEnabled = false
cellWithCategories["Recomendation"]?.append("slideCell")
tableView.beginUpdates()
tableView.insertRowsAtIndexPaths([indexPathForCell], withRowAnimation: .None)
tableView.endUpdates()
}
checkMark.show()
} else {
if (tableView.cellForRowAtIndexPath(indexPathForCell) != nil) {
cellWithCategories["Recomendation"]?.removeLast()
tableView.beginUpdates()
tableView.deleteRowsAtIndexPaths([indexPathForCell], withRowAnimation: .None)
tableView.endUpdates()
}
checkMark.hide()
checkMark.userInteractionEnabled = true
}
}
}
code for number of rows :
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
let sectionKey = keysForSectionTableView[section]
let numberOfRows = cellWithCategories[sectionKey]
return (numberOfRows?.count)!
}
I don't see any code that will make your table view scroll to top.
But you can try change animation to none. If doesn't work then there is must be some other code, thats causing this issue.
let indexPathForCell = NSIndexPath(forRow: 5, inSection: 1)
tableView.beginUpdates()
tableView.insertRowsAtIndexPaths([indexPathForCell], withRowAnimation: .None)
tableView.endUpdates()

Resources