UICollectionView Crash on reloadData - ios

UICollectionview is loaded with simple cells and there is a simple int variable for calculating the number of items in the section.
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return viewCount
}
When the value of viewCount (initially at 45) is changed to a lower number (say 12) the app crashes.
This is error message to update the item count:
*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'UICollectionView received layout attributes for a cell with an index path that does not exist: <NSIndexPath: 0x17402fbe0> {length = 2, path = 0 - 12}'
I tried reloading data and invalidating the cache as well as said here. This didn't help. I also tried reloading the indexSet before invalidating the cache. That didn't help either.
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
if textField == countTextField {
viewCount = Int(textField.text!)!
textField.resignFirstResponder()
let indexSet = IndexSet(integer: 0)
// collectionView.reloadSections(indexSet)
collectionView.reloadData()
collectionView.collectionViewLayout.invalidateLayout()
}
return true
}
I use 2 custom UICollectinViewLayout and I also set shouldInvalidateLayout to be true.
Any help?
Update
func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "DemoCell", for: indexPath) as! DemoCollectionViewCell
cell.indexLabel.text = "\(indexPath.item + 1)"
return cell
}
#IBAction func showLayout1(_ sender: Any) {
currentLayout = DemoACollectionViewLayout()
animateLayout()
}
#IBAction func showLayout2(_ sender: Any) {
currentLayout = DemoBCollectionViewLayout()
animateLayout()
}
func animateLayout() {
UIView.animate(withDuration: 0.3) { [weak self] value in
guard let `self` = self else { return }
self.collectionView.collectionViewLayout.invalidateLayout()
self.collectionView.collectionViewLayout = self.currentLayout!
}
}
Here is the sample project.
Update:
I have updated to use an dataObject to reload the UICollectionView but the cache in UICollectionViewLayout is not being cleared when I invalidate.

The cache must be cleared when the number of items for the collectionView has changed.
guard let views = collectionView?.numberOfItems(inSection: 0)
else {
cache.removeAll()
return
}
if views != cache.count {
cache.removeAll()
}
if cache.isEmpty {
//Layout attributes
}

Related

UICollection View "Reload Data" After Scrolling Crash

I have made a collection view with cells arranged in rows in columns using a great tutorial. I have added a button to a toolbar in the main view controller that calls collectionView.reloadData() as I want a user to be able to edit values which will in turn update the datasource and then reload the collection view to show the updates.
Running this on a simulator it works, but if any scrolling takes place it causes this crash *** Terminating app due to uncaught exception 'NSRangeException', reason: '*** -[__NSArrayM objectAtIndex:]: index 0 beyond bounds for empty array'. If no scrolling has taken place then calling collectionView.reloadData() works. I can't find where this empty array is which is causing the crash. I have tried printing all the arrays that are used in the code in the console but none appear to be empty. Have tried commenting out various lines of code to try and narrow down where the problem is, it seems to be something in the override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? block. I have also tried reseting the collection view frame co-ordinates to 0 before reload data is called but that also didn't work. Have been stuck going round in circles for a few days which no luck. Any suggestions as to where I am going wrong would be hugely appreciated! My code so far is below (please excuse the long winded explanation and code);
View Controller
import UIKit
class ViewController: UIViewController {
// variable to contain cell indexpaths sent from collectionViewFlowLayout.
var cellIndexPaths = [IndexPath] ()
#IBOutlet weak var collectionView: UICollectionView!
#IBOutlet weak var collectionViewFlowLayout: UICollectionViewFlowLayout! {
didSet {
collectionViewFlowLayout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize
}
}
#IBAction func editButtonPressed(_ sender: UIButton) {
collectionView.reloadData()
}
#IBAction func doneButtonPressed(_ sender: UIButton) {
}
override func viewDidLoad() {
super.viewDidLoad()
collectionView.delegate = self
collectionView.dataSource = self
}
}
extension ViewController:UICollectionViewDataSource {
func numberOfSections(in collectionView: UICollectionView) -> Int {
2
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
5
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath) as! collectionViewCell
cell.backgroundColor = .green
cell.textLabel.text = "\(indexPath) - AAAABBBBCCCC"
return cell
}
}
extension ViewController:UICollectionViewDelegate, IndexPathDelegate {
func getIndexPaths(indexPathArray: Array<IndexPath>) {
cellIndexPaths = indexPathArray.uniqueValues
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
var cellArray = [UICollectionViewCell] ()
print(cellIndexPaths)
for indexPathItem in cellIndexPaths {
if let cell = collectionView.cellForItem(at: indexPathItem) {
if indexPathItem.section == indexPath.section {
cellArray.append(cell)
}
}
for cells in cellArray {
cells.backgroundColor = .red
Timer.scheduledTimer(withTimeInterval: 0.2, repeats: false) { (timer) in
cells.backgroundColor = .green
}
}
}
}
}
extension ViewController: UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let cell = collectionView.cellForItem(at: indexPath)
if let safeCell = cell {
let cellSize = CGSize(width: safeCell.frame.width, height: safeCell.frame.height)
return cellSize
} else {
return CGSize (width: 300, height: 100)
}
}
}
extension Array where Element: Hashable {
var uniqueValues: [Element] {
var allowed = Set(self)
return compactMap { allowed.remove($0) }
}
}
Flow Layout
import UIKit
protocol IndexPathDelegate {
func getIndexPaths(indexPathArray: Array<IndexPath>)
}
class collectionViewFlowLayout: UICollectionViewFlowLayout {
override var collectionViewContentSize: CGSize {
return CGSize(width: 10000000, height: 100000)
}
override func prepare() {
setupAttributes()
indexItemDelegate()
}
// MARK: - ATTRIBUTES FOR ALL CELLS
private var allCellAttributes: [[UICollectionViewLayoutAttributes]] = []
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
var layoutAttributes = [UICollectionViewLayoutAttributes]()
for rowAttrs in allCellAttributes {
for itemAttrs in rowAttrs where rect.intersects(itemAttrs.frame) {
layoutAttributes.append(itemAttrs)
}
}
return layoutAttributes
}
// MARK: - SETUP ATTRIBUTES
var cellIndexPaths = [IndexPath] ()
private func setupAttributes() {
allCellAttributes = []
var xOffset: CGFloat = 0
var yOffset: CGFloat = 0
for row in 0..<rowsCount {
var rowAttrs: [UICollectionViewLayoutAttributes] = []
xOffset = 0
for col in 0..<columnsCount(in: row) {
let itemSize = size(forRow: row, column: col)
let indexPath = IndexPath(row: row, column: col)
let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
attributes.frame = CGRect(x: xOffset, y: yOffset, width: itemSize.width, height: itemSize.height).integral
rowAttrs.append(attributes)
xOffset += itemSize.width
cellIndexPaths.append(indexPath)
}
yOffset += rowAttrs.last?.frame.height ?? 0.0
allCellAttributes.append(rowAttrs)
}
}
// MARK: - CONVERT SECTIONS TO ROWS, ITEMS TO COLUMNS
private var rowsCount: Int {
return collectionView!.numberOfSections
}
private func columnsCount(in row: Int) -> Int {
return collectionView!.numberOfItems(inSection: row)
}
// MARK: - GET CELL SIZE
private func size(forRow row: Int, column: Int) -> CGSize {
guard let delegate = collectionView?.delegate as? UICollectionViewDelegateFlowLayout,
let size = delegate.collectionView?(collectionView!, layout: self, sizeForItemAt: IndexPath(row: row, column: column)) else {
assertionFailure("Implement collectionView(_,layout:,sizeForItemAt: in UICollectionViewDelegateFlowLayout")
return .zero
}
return size
}
private func indexItemDelegate () {
let delegate = collectionView?.delegate as? IndexPathDelegate
delegate?.getIndexPaths(indexPathArray: cellIndexPaths)
}
}
// MARK: - INDEX PATH EXTENSION
//creates index path with rows and columns instead of sections and items
private extension IndexPath {
init(row: Int, column: Int) {
self = IndexPath(item: column, section: row)
}
}
Collection Cell
import UIKit
class collectionViewCell: UICollectionViewCell {
#IBOutlet weak var textLabel: UILabel!
override func awakeFromNib() {
super.awakeFromNib()
contentView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
contentView.leftAnchor.constraint(equalTo: leftAnchor),
contentView.rightAnchor.constraint(equalTo: rightAnchor),
contentView.topAnchor.constraint(equalTo: topAnchor),
contentView.bottomAnchor.constraint(equalTo: bottomAnchor)
])
}
}
I have managed to get around the issue by using the UICollectionView.reloadSections(sections: IndexSet) method. This doesn't cause any crashes. I loop through all sections and add each section to an IndexSet variable then use that in the reload sections method like this;
var indexSet = IndexSet()
let rowCount = collectionView.numberOfSections
for row in 0..<rowCount {
indexSet = [row]
collectionView.reloadSections(indexSet)
}

collectionView numberOfItemsInSection in UITableviewCell

I made one collectionView inside the tableView cell
I registered all delegates and protocols and in most cases everything works fine
the problem is that the collectionView must dynamically generate cells from the server response
I installed a counter on the top of the collection that shows how many cells there are (also dynamically updated)
if the collection is in the field of view when the screen is loaded, the cells are loaded
if it is necessary to complete the collection, it is empty, although the counter indicates the presence of elements.
I can not quite understand why. Code below
code form VC:
let realPfotoCell = tableView.dequeueReusableCell(withIdentifier:
"RealPhoto", for: indexPath)
as! RealPhotoCarInfoTableViewCell
realPfotoCell.delegate = self
realPfotoCell.model = self.rPhoto
if(self.rPhoto?.data != nil){
realPfotoCell.RPhotoCount.text = String(self.rPhoto.data!.count)
}
cell = realPfotoCell
cell.selectionStyle = UITableViewCell.SelectionStyle.none
}
code from tableViewCell:
var model: RealPhoto!
func setCollectionViewDataSourceDelegate(dataSourceDelegate: UICollectionViewDataSource & UICollectionViewDelegate, forRow row: Int) {
MyCollection.delegate = self
MyCollection.dataSource = self
MyCollection.reloadData()
}
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
MyCollection.reloadData()
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
if (self.model?.data?.count != nil){
print("Photos here")
print(self.model?.data!.count as Any)
} else {
print("No photos")
}
return self.model?.data?.count ?? 0
}
Please make sure the ReloadData() will be call when your server response will come.
//Server Response
MyCollection.delegate = self
MyCollection.dataSource = self
MyCollection.reloadData()
resolved
1st on tableview cell create this function:
func collectionReloadData(){
DispatchQueue.main.async(execute: {
self.collectionView.reloadData()
})
}
then call it from
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { ...
realPfotoCell.collectionReloadData()

Smooth animation in UICollectionView when changing data source

I have a UISegmentControl that I use to switch the datasource for a UICollectionView. The datasources are different types of objects.
For example the objects might look like this
struct Student {
let name: String
let year: String
...
}
struct Teacher {
let name: String
let department: String
...
}
And in the view that contains the CollectionView, there would be code like this:
var students = [Student]()
var teachers = [Teachers]()
... // populate these with data via an API
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
if(segmentControl.titleForSegment(at: segmentControl.selectedSegmentIndex) == "Students") {
return students?.count ?? 0
} else {
return teachers?.count ?? 0
}
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "personCell", for: indexPath) as! PersonCell
if(segmentControl.titleForSegment(at: segmentControl.selectedSegmentIndex)! == "Students") {
cell.title = students[indexPath.row].name
cell.subtitle = students[indexPath.row].year
} else {
cell.title = teachers[indexPath.row].name
cell.subtitle = teachers[indexPath.row].subject
}
return cell
}
#IBAction func segmentChanged(_ sender: AnyObject) {
collectionView.reloadData()
}
This correctly switches between the two datasources, however it does not animate the change. I tried this:
self.collectionView.performBatchUpdates({
let indexSet = IndexSet(integersIn: 0...0)
self.collectionView.reloadSections(indexSet)
}, completion: nil)
But this just crashes (I think this is because performBatchUpdates gets confused about what to remove and what to add).
Is there any easy way to make this work, without having a separate array storing the current items in the collectionView, or is that the only way to make this work smoothly?
Many thanks in advance!
If your Cell's UI just look the same from different datasource, you can abstract a ViewModel upon your datasource, like this:
struct CellViewModel {
let title: String
let subTitle: String
...
}
Then every time you got data from an API, generate ViewModel dynamically
var students = [Student]()
var teachers = [Teachers]()
... // populate these with data via an API
var viewModel = [CellViewModel]()
... // populate it from data above by checking currently selected segmentBarItem
if(segmentControl.titleForSegment(at: segmentControl.selectedSegmentIndex)! == "Students") {
viewModel = generateViewModelFrom(students)
} else {
viewModel = generateViewModelFrom(teachers)
}
So you always keep one datasource array with your UICollectionView.
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return viewModel?.count ?? 0
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "personCell", for: indexPath) as! PersonCell
cell.title = viewModel[indexPath.row].title
cell.subtitle = viewModel[indexPath.row].subTitle
return cell
}
#IBAction func segmentChanged(_ sender: AnyObject) {
collectionView.reloadData()
}
Then try your performBatchUpdates:
self.collectionView.performBatchUpdates({
let indexSet = IndexSet(integersIn: 0...0)
self.collectionView.reloadSections(indexSet)
}, completion: nil)

Fatal error: Index out of range when downloading data from the Internet

There is a function that updates my UICollectionView:
#objc func refreshFeed(sender:AnyObject)
{
fetchWeatherData()
}
private func fetchWeatherData() {
PostArray = [PostModel]()
offset = 0
self.fetchPost()
self.refreshControl.endRefreshing()
}
This function causes the PostArray collection to be nullified and calls the fetchPost ribbon update function:
func fetchPost(URL: String){
ApiService.sharedInstance.fetchPost(url: URL, completion: { (Posts: [PostModel]) in
self.PostArray.append(contentsOf: Posts)
self.collectionView.reloadData()
})
}
After updating the collection, the function:
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cellId", for: indexPath) as! PostCell
cell.PostArray = PostArray[indexPath.item]
return cell
}
Gives an error message: Thread 1: Fatal error: Index out of range in line: cell.PostArray = PostArray[indexPath.item]
Why is this happening? The data falls into the collection.
Sorry for my English
At first, check that func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int returns valid count of items.
Than you can check if PostArray contains item at given index:
if PostArray.indices ~= indexPath.item {
cell.postArray = PostArray[indexPath.item]
}
And you need to make sure that ApiService.sharedInstance.fetchPost runs completion on main queue. If not, try this:
func fetchPost(URL: String){
ApiService.sharedInstance.fetchPost(url: URL, completion: { (Posts: [PostModel]) in
self.PostArray.append(contentsOf: Posts)
DispatchQueue.main.async {
self.collectionView.reloadData()
}
})
}

Load more activity cell CollectionViewCell

I'm trying to create a activityCell, so when the user reach the button it will show an cell with an activity indicator. This seem to work fine however if moreDataAvailable is false it should remove this cell. However i keep getting following error?
'NSInternalInconsistencyException', reason: 'attempt to delete item 0 from section 1 which only contains 0 items before the update'
numberOfItemsInSection
override func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int
{
if section == 0 {
return organizationArray.count
} else {
if self.moreDataAvailable == true {
return 1
} else {
return 0
}
}
}
Hide Collection Cell
func hideCollectionViewFooter() {
self.collectionView!.deleteItemsAtIndexPaths([NSIndexPath(forRow: 0, inSection: 1)])
}
numberOfSectionsInCollectionView
override func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int {
return 2
}
cellForItemAtIndexPath
override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
if indexPath.section == 0 {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier("OrganizationCell", forIndexPath: indexPath) as! OrganizationCollectionViewCell
cell.customerLabel?.text = organizationArray[indexPath.item].name.uppercaseString
cache.fetch(key: organizationArray[indexPath.item].coverPhoto).onSuccess { data in
cell.customerImageView?.image = UIImage(data: data)
}
return cell
} else {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier("ActivityCell", forIndexPath: indexPath) as UICollectionViewCell
return cell
}
}
Load More when reach bottom
override func collectionView(collectionView: UICollectionView, willDisplayCell cell: UICollectionViewCell, forItemAtIndexPath indexPath: NSIndexPath) {
if !loadingData && indexPath.item == organizationArray.count - 1 && self.moreDataAvailable {
self.loadingData = true
proposeAccess(false, success: {
self.loadingData = false
})
}
}
Update Organization and check if more data is available
func updateOrganizations(refresh: Bool) {
let realm = try! Realm()
GetOrganization.request(String(self.lastLoadedPage), limit: String(limit), location: self.lastLocation!, radius: String(100), refresh: refresh,
success: { numberOfResults in
//Sort by distance
self.organizationArray = GetOrganization.sortOrganizationsByDistanceFromLocation(realm.objects(Organization), location: self.lastLocation!)
self.lastLoadedPage = self.lastLoadedPage + 1
if numberOfResults < self.limit {
//Hide FooterView
self.moreDataAvailable = false
self.hideCollectionViewFooter()
}
}, error: {
self.organizationArray = GetOrganization.sortOrganizationsByDistanceFromLocation(realm.objects(Organization), location: self.lastLocation!)
print("error")
})
}
This error means that you're trying to delete cell that not existed in current table view state. Probably moreDataAvailable already was false before request in updateOrganizations was finished.
I would recommend you using table footer view for displaying activity indicator. Also, after data is loaded you can display a number of loaded items.

Resources