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()
}
}
Related
I have a UICollectionView that I feed data into using UICollectionViewDiffableDataSource. I want to display a scroll scrubber on the trailing edge of it, like I'd get if I implemented the data source methods indexTitlesForCollectionView and indexPathForIndexTitle. But the data source is the diffable data source object, and there's no property or closure on it to supply index titles as of iOS 15.
How are index titles supposed to work with UICollectionViewDiffableDataSource?
You have to create the subclass for your UICollectionViewDiffableDataSource.
final class SectionIndexTitlesCollectionViewDiffableDataSource: UICollectionViewDiffableDataSource<Section, SectionItem> {
private var indexTitles: [String] = []
func setupIndexTitle() {
indexTitles = ["A", "B", "C"]
}
override func indexTitles(for collectionView: UICollectionView) -> [String]? {
indexTitles
}
override func collectionView(_ collectionView: UICollectionView, indexPathForIndexTitle title: String, at index: Int) -> IndexPath {
// your logic how to calculate the correct IndexPath goes here.
guard let index = indexTitles.firstIndex(where: { $0 == title }) else {
return IndexPath(item: 0, section: 0)
}
return IndexPath(item: index, section: 0)
}
}
You can use now this custom diffable data source in your vc instead of regular UICollectionViewDiffableDataSource.
N.B But there is a small trick.
You must have to setup indexTitles once applying snapshot completes, otherwise it may crash.
dataSource?.apply(sectionSnapshot, to: section, animatingDifferences: true, completion: { [weak self] in
self?.dataSource?.setupIndexTitle()
self?.collectionView.reloadData()
})
I have table view that delete some rows with following:
func deleteRows(_ indecies: [Int]) {
guard !indecies.isEmpty else { return }
let indexPathesToDelete: [IndexPath] = indecies.map{ IndexPath(row: $0, section: 0)}
let previousIndex = IndexPath(row: indecies.first! - 1, section: 0)
tableView.deleteRows(at: indexPathesToDelete, with: .none)
tableView.reloadRows(at: [previousIndex], with: .none)
}
In cellForRow i have cell that have "tap" closure like this:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard indexPath.row < presenter.fields.count else { return EmptyCell() }
let field = presenter.fields[indexPath.row]
switch field.cellType {
case .simple:
guard let model = field as? SimpleTextItem else { return EmptyCell() }
let cell = SimpleTextCell()
cell.setup(label: LabelSL.regularSolidGray(), text: model.text, color: Theme.Color.bleakGray)
return cell
case .organization:
guard let model = field as? OrganizationFilterItem else { return EmptyCell() }
let cell = OrganizationFilterCell()
cell.setup(titleText: model.title,
holdingNumberText: model.holdingNumberText,
isChosed: model.isChosed,
isHolding: model.isHolding,
isChild: model.isChild,
bottomLineVisible: model.shouldDrawBottomLine)
cell.toggleControlTapped = {[weak self] in
self?.presenter.tappedItem(indexPath.row)
}
return cell
}
}
When
cell.toggleControlTapped = {[weak self] in
self?.presenter.tappedItem(indexPath.row)
}
Tapped after rows deletion, index is pass is wrong (it's old). For example, i have 10 rows, i delete 2-3-4-5 row, and then i tap on 2 row (it was 6 before deletion). That method pass "6" instead of "2".
Problem actually was solved by adding tableView.reloadData() in deleteRows function, but, as you may assume smoothly animation gone and it look rough and not nice. Why is table still pass old index and how to fix it?
A quite easy solution is to pass the cell in the closure to be able to get the actual index path
Delare it
var toggleControlTapped : ((UITableViewCell) -> Void)?
Call it
toggleControlTapped?(self)
Handle it
cell.toggleControlTapped = {[weak self] cell in
guard let actualIndexPath = self?.tableView.indexPath(for: cell) else { return }
self?.presenter.tappedItem(actualIndexPath.row)
}
Side note: Reuse cells. Creating cells with the default initializer is pretty bad practice.
Be sure to update your data too, indexes are appearing from numberofrows function of table view
I have a CollectionView with sections (2 sections). When I delete a cell from section 1 its deleting very good. But when I delete a cell from section 0 my app is crashing with error like this:
invalid number of items in section 0. The number of items contained in an existing section after the update (5) must be equal to the number of items contained in that section before the update (5), plus or minus the number of items inserted or deleted from that section (0 inserted, 1 deleted) and plus or minus the number of items moved into or out of that section (0 moved in, 0 moved out). with userInfo (null)
Before delete item from collectionView, I delete it item from my data source in performBatchUpdates:
extension MainCollectionViewController: NSFetchedResultsControllerDelegate {
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
collectionView?.performBatchUpdates({ [weak self] in
guard let self = self else { return }
items = controller.fetchedObjects as! [Item]
items2 = items.chunked(into: 5)
self.collectionView?.deleteItems(at: [self.deletedItemIndex!])
})
}
}
extension Array {
func chunked(into size: Int) -> [[Element]] {
return stride(from: 0, to: count, by: size).map {
Array(self[$0 ..< Swift.min($0 + size, count)])
}
}
}
func chunked - is a function that slice array like this (5 items in section):
Before chunked:
[1,2,3,4,5,6,7,8,9,10]
After chunked:
[
[1, 2, 3, 4, 5], // first section in collectionView
[6, 7, 8, 9, 10], // second section in collectionView
]
I populate my items from Core Data to collectionView with this functions:
override func numberOfSections(in collectionView: UICollectionView) -> Int {
print("call numberOfSections")
//3
return items2.count
}
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
print("call numberOfItemsInSection, current section is \(section)")
//4
return items2[section].count
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! CollectionViewCell
let item = items2[indexPath.section][indexPath.row]
cell.itemNameTextLabel.text = item.name
cell.itemImageView.image = UIImage(data: item.image! as Data)
return cell
}
}
Item deleting from collectionView and CoreData when user long press it item (cell). Deleting process is here:
#objc func handleLongPress(gesture: UILongPressGestureRecognizer!) {
if gesture.state != .ended {
return
}
let p = gesture.location(in: self.collectionView)
if let indexPath = self.collectionView?.indexPathForItem(at: p) {
let fetchRequest: NSFetchRequest<Item> = Item.fetchRequest()
fetchRequest.predicate = NSPredicate(format: "name == %#", self.items2[indexPath.section][indexPath.row].name!)
do {
if let context = (UIApplication.shared.delegate as? AppDelegate)?.persistentContainer.viewContext {
let selectedItem = try context.fetch(fetchRequest)[0]
//save deleted item index in var that use it index in performBatchUpdatesBlock
deletedItemIndex = IndexPath(row: indexPath.row, section: indexPath.section)
context.delete(selectedItem)
do {
try context.save()
print("Save!")
} catch let error as NSError {
print("Oh, error! \(error), \(error.userInfo)")
}
}
} catch {
print(error.localizedDescription)
}
}
}
Image of this process:
enter image description here
My project like Apple Books app. I want to repeat deleting book process...
My full code is here (GitHub). Plis, use iPhone SE simulator. My data in items.plist file and automatic saving to CoreData when run app in first time.
Where is mistake in my code?
When you delete from the CollectionView, you should also delete from the underlying data array (items2).
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
collectionView?.performBatchUpdates({ [weak self] in
guard let self = self else { return }
items = controller.fetchedObjects as! [Item]
items2 = items.chunked(into: 5)
for indexPath in self.deletedItemIndex! {
items2[indexPath.section].remove(at: indexPath.row)
}
self.collectionView?.deleteItems(at: [self.deletedItemIndex!])
})
}
Maybe, the problem is that you delete one item of section 0, and after deletion the section 0 still with the same number of itens before deletion, because you "redistribute" the itens after each deletion.
I must be missing something simple but cannot identify what it is. BASIC and FORTRAN sure seemed a lot easier... just not as cool as this stuff is.
App is to display & sort flashcards stored in CoreData based on a predicate. After selecting options which modify the predicate, a UITableView is displayed listing the cards meeting the criteria in abbreviated form. Selecting a card row segues to a 2nd view controller with identical predicate setup in UITableView only more content shown & "Skill Level" can be modified and saved via CoreData on this 2nd view controller. Depending on the fetchRequest predicate, a card may no longer match the criteria and might be excluded from a subsequent fetchRequest. This all seems to function properly.
Selecting "Back" returns to the 1st UITableView controller from the 2nd, via an unwind segue. At this point, the fetchRequest is again executed along with a tableview.reloadData(). On return, flashcards sort in the proper order based on any changes. Flashcards that no longer meet the predicate criteria are properly excluded from the table.
The problem is this - the custom cell contents do not reflect any changes made (stars graphic doesn't change and numeric value of the Skill Level doesn't change when printed from the tableView). TableView sorts correctly and doesn't include items it shouldn't which seems to indicate the updated information was saved and has somehow been taken into consideration by the tableView. TableView just doesn't show it on the screen.
If I select "Back" again, returning to the "Home" or initial viewController & immediately segue to it again the tableView displays properly.
I've tried placing tableView.reloadData() in various locations(with and without .self), moving the fetch & reloads in and out of DispatchQueue.main.async. Tried replacing the control in the custom cell with images but that didn't work either. Can't seem to find anything quite like it on stack overflow or elsewhere on internet.
Any assistance would be greatly appreciated.
Custom Cell:
import UIKit
class CardsTableViewCell: UITableViewCell {
// MARK: Properties
#IBOutlet weak var questionLabel: UILabel!
#IBOutlet weak var difficultyImageView: UIImageView!
#IBOutlet weak var knowItControl: KnowItControl!
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
}
UnwindFromSegue:
#IBAction func unwindToCardsTableViewController(sender: UIStoryboardSegue) {
DispatchQueue.main.async{
// RELOAD THE FLASHCARDS FOR TABLEVIEW
do {
try self.fetchedResultsController.performFetch()
} catch let error as NSError {
print("Fetching error: \(error), \(error.userInfo)")
}
print("\n UNWIND - About to reload table")
self.tableView.reloadData()
}
print("UNWIND SEGUE from DetailedCardsView")
// Considered giving up and just dumping back to Main Screen.
// dismiss(animated: true, completion: nil)
}
Assembling the tableView using extensions in the tableViewController
// MARK: - ConfigureCardsTableViewCell
extension CardsTableViewController {
func configure(cell: UITableViewCell, for indexPath: IndexPath) {
guard let cell = cell as? CardsTableViewCell else { return }
let flashCards = fetchedResultsController.object(at: indexPath)
cell.knowItControl.rating = Int(flashCards.cardKnowIt)
cell.questionLabel.text = flashCards.cardQuestion
// * * * ASSIGN THE IMAGE BASED ON THE levelDifficulty * * *
switch flashCards.levelDifficulty {
case "e"?:
cell.difficultyImageView.image = UIImage(named: "activeLetterE")
case "g"?:
cell.difficultyImageView.image = UIImage(named: "activeLetterG")
case "m"?:
cell.difficultyImageView.image = UIImage(named: "activeLetterM")
default:
cell.difficultyImageView.image = UIImage(named: "activeLetterE")
}
// ************************* END OF ASSIGN IMAGE ****************************************
print("assembling cell \(indexPath.row) *** knowIt - \(Int(flashCards.cardKnowIt))") // Observe what is going on, see if value was updated (it's not).
}
}
// * * * * T A B L E V I E W D A T A S O U R C E * * * *
// MARK: - UITableViewDataSource
extension CardsTableViewController: UITableViewDataSource {
// * * * C O N F I G U R E T H E C E L L V I A A C A L L * * *
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cardCellReuse", for: indexPath)
configure(cell: cell, for: indexPath)
print(" Configuring cell \(indexPath)") // Observe what is going on.
return cell
}
// general tableView functions for number of rows, sections, etc.
}
The viewController also contains the generic NSFetchedResultsControllerDelegate which I believe accurately matches all the examples I've come across.
Screen shots shown here.
1 - Initial load of 1st tableView from "Home" page properly displays content.
2 - Segued to 2nd tableView where changes were made and saved to CoreData.
3 - Returned to 1st tableView using unwind. Cells are sorted correctly but don't contain updated values.
4 - Left 1st tableView via Back and immediately loaded it again and the screen properly displays content. Just wish it did it without having to "go home & return".
NSFetched Results Controller Delegate:
// MARK: - NSFetchedResultsControllerDelegate
extension CardsTableViewController: NSFetchedResultsControllerDelegate {
func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
tableView.beginUpdates()
print("\n **** tableView.beganUpdates called")
}
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
switch type {
case .insert:
tableView.insertRows(at: [newIndexPath!], with: .automatic)
case .delete:
tableView.deleteRows(at: [indexPath!], with: .automatic)
case .update:
let cell = tableView.cellForRow(at: indexPath!) as! CardsTableViewCell
configure(cell: cell, for: indexPath!)
print("case .update was called.")
case .move:
tableView.deleteRows(at: [indexPath!], with: .automatic)
tableView.insertRows(at: [newIndexPath!], with: .automatic)
}
}
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
tableView.endUpdates()
print("\n **** tableView.endUpdates called")
}
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange sectionInfo: NSFetchedResultsSectionInfo, atSectionIndex sectionIndex: Int, for type: NSFetchedResultsChangeType) {
let indexSet = IndexSet(integer: sectionIndex)
switch type {
case .insert:
tableView.insertSections(indexSet, with: .automatic)
case .delete:
tableView.deleteSections(indexSet, with: .automatic)
default: break
}
}
}
When I try to delete an item from a Realm database I am unable to update a UICollection View appropriately.
Lets assume a Realm container children of type List<Child>:
var children = realm.objects(Parent).first!.children
When I want to remove this child from the database by:
try! realm.write {
realm.delete(children[indexPath.row])
}
updating the collectionView by collectionView.deleteItemsAtIndexPaths([indexPath]) gives the following error:
Got error: *** Terminating app due to uncaught exception 'RLMException', reason: 'Object has been deleted or invalidated.'
The only way I get the collectionView updated is by using collectionView.reloadData(), but that is not what I want since the animation of a cell deletion is missing.
However, when I only remove a child from this container at indexPath.row (without removing it from the database) by:
try! realm.write {
children.removeAtIndex(indexPath.row)
}
updating the collectionView with collectionView.deleteItemsAtIndexPaths([indexPath]) works without problems.
What would be the best way to update a UICollectionView after removing an item from the database?
The error you're facing appears when you keep accessing an object, which was already deleted. So, you're storing likely somewhere a reference to your object, which is fine per se, but keep accessing it after it was invalidated.
That could happen e.g. in your custom subclass of UICollectionViewCell. I'd recommend to implement a setter on your cell and pull from that method the property values into your view components. You can even use KVO in your cell to update these. (We've an example based on ReactKit for that up in our repo.) You can't though keep accessing the properties when the object might be already deleted at a later point in time, e.g. if your cell needs to be drawn or layout when it is faded out.
I'd recommend to subscribe to fine-grained notifications for the list you're using to fill your collection view's cells and only propagate updates in that way to the collection view. In that way you can make sure that your items will be removed with a nice animation as requested and it is automatically taken care of. All put together this could look like seen below. Over at our repo, you'll find a complete runnable sample.
class Cell: UICollectionViewCell {
#IBOutlet var label: UILabel!
func attach(object: DemoObject) {
label.text = object.title
}
}
class CollectionViewController: UICollectionViewController {
var notificationToken: NotificationToken? = nil
lazy var realm = try! Realm()
lazy var results: Results<DemoObject> = {
self.realm.objects(DemoObject)
}()
// MARK: View Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
// Observe Notifications
notificationToken = results.addNotificationBlock { [weak self] (changes: RealmCollectionChange) in
guard let collectionView = self?.collectionView else { return }
switch changes {
case .Initial:
// Results are now populated and can be accessed without blocking the UI
collectionView.reloadData()
break
case .Update(_, let deletions, let insertions, let modifications):
// Query results have changed, so apply them to the UITableView
collectionView.performBatchUpdates({
collectionView.insertItemsAtIndexPaths(insertions.map { NSIndexPath(forRow: $0, inSection: 0) })
collectionView.deleteItemsAtIndexPaths(deletions.map { NSIndexPath(forRow: $0, inSection: 0) })
collectionView.reloadItemsAtIndexPaths(modifications.map { NSIndexPath(forRow: $0, inSection: 0) })
}, completion: { _ in })
break
case .Error(let error):
// An error occurred while opening the Realm file on the background worker thread
fatalError("\(error)")
break
}
}
}
deinit {
notificationToken?.stop()
}
// MARK: Helpers
func objectAtIndexPath(indexPath: NSIndexPath) -> DemoObject {
return results[indexPath.row]
}
// MARK: UICollectionViewDataSource
override func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return results.count
}
override func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath) {
let object = objectAtIndexPath(indexPath)
try! realm.write {
realm.delete(object)
}
}
override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let object = objectAtIndexPath(indexPath)
let cell = collectionView.dequeueReusableCellWithReuseIdentifier("Cell", forIndexPath: indexPath) as! Cell
cell.attach(object)
return cell
}
}