UICollectionView reloadData not working - ios

Its getting called in viewDidLoad, after fetching the data used.
After some print debugging it looks like it calls all the appropriate delegeate methods, if no data is changed. If there has been some data changed, cellForItemAt does not get called.
Reloading the whole section works fine. But gives me an unwanted animation. Tried disabling UIView animation before, and enabling after reloading section, but still gives me a little animation.
collectionView.reloadSections(IndexSet(integer: 0))
Here is my current situation, when using reloadData()
The UICollectionViewController is a part of a TabBarController.
I'm using custom UICollectionViewCells. The data is loaded from CoreData.
First time opening the tab, its works fine.
After updating the favorites item in another tab, and returning to this collectionView, its not updated. But if i select another tab, and go back to this one, its updated.
var favorites = [Item]()
override func viewWillAppear(_ animated: Bool) {
collectionView!.register(UINib.init(nibName: reuseIdentifier, bundle: nil), forCellWithReuseIdentifier: reuseIdentifier)
if let flowLayout = collectionView!.collectionViewLayout as? UICollectionViewFlowLayout {
flowLayout.estimatedItemSize = CGSize(width: 1,height: 1)
}
loadFavorites()
}
func loadFavorites() {
do {
print("Load Favorites")
let fetch: NSFetchRequest = Item.fetchRequest()
let predicate = NSPredicate(format: "favorite == %#", NSNumber(value: true))
fetch.predicate = predicate
favorites = try managedObjectContext.fetch(fetch)
if favorites.count > 0 {
print("Favorites count: \(favorites.count)")
notification?.removeFromSuperview()
} else {
showEmptyFavoritesNotification()
}
print("Reload CollectionView")
collectionView!.reloadData(
} catch {
print("Fetching Sections from Core Data failed")
}
}
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
print("Get number of section, \(favorites.count)")
if favorites.count > 0 {
return favorites.count
} else {
return 0
}
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
print("Get cell")
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath) as! SubMenuCell
let menuItem = favorites[indexPath.row]
cell.title = menuItem.title
cell.subtitle = menuItem.subtitle
return cell
}
Console Print/Log
Starting the app, going to the CollectionView tab where there are no favorites:
Load Favorites
Get number of section, 0
Get number of section, 0
Reload CollectionView
Get number of section, 0
Switching tab, and adding and setting an Item object's favorite to true, then heading back to the CollectionView tab:
Load Favorites
Favorites count: 1
Reload CollectionView
Get number of section, 1
The datamodel has 1 item, reloading CollectonView, cellForRowAtIndex not called?
Selecting another tab, random which, then heading back to the CollectionView tab, without changing any data.
Load Favorites
Favorites count: 1
Reload CollectionView
Get number of section, 1
Get cell
Now my Item shows up in the list. You can see from the Get Cell that cellForItemAt is called aswell. Nothing has changed between these last two loggings, just clicked back/fourth on the tabs one time.
Forgot to mention, but this code IS working fine in the simulator.
On a read device its just not giving me any error, just not showing the cell (or calling the cellForItemAt)

After some more debugging i got an error when the items got reduced (instead of increasing like i've tried above).
UICollectionView received layout attributes for a cell with an index path that does not exist
This led med to iOS 10 bug: UICollectionView received layout attributes for a cell with an index path that does not exist
collectionView!.reloadData()
collectionView!.collectionViewLayout.invalidateLayout()
collectionView!.layoutSubviews()
This solved my problem.
I am using autoresizing cells, but i guess this does not explain why cellForItemAt did not get called.

in my case, my collectionview was in a stackview, i didnt make any constraints, when i added the necessary constraints it worked.
Just check collectionview's constraints.

Ok, maybe I can help someone =)
I do this:
Load data from API
Then after load data call DispatchQueue.main.async like this
DispatchQueue.main.async {
self.pointNews = pointNews
}
In self.pointNews didSet I try to do collectionView.reloadData(), but it work only, if I call DispatchQueue.main.async in didSet
DispatchQueue.main.async {
self.collectionView.reloadData()
}

var paths: [IndexPath] = []
if !data.isEmpty {
for i in 0...salesArray.count - 1 {
paths.append(IndexPath(item: i, section: 0))
}
collectionView.reloadData()
collectionView.reloadItems(at: paths)
}
This is the code helped me, for solving this kind of issue.

Post your code.
One possibility is that you are using `URLSession and trying to tell your collection view to update from the it's delegate method/completion closure, not realizing that that code is run on a background thread and you can't do UI calls from background threads.

I faced the same problem with self resizing cells and above solution works as well but collection view scroll position resets and goes to top. Below solution helps to retain your scroll position.
collectionView.reloadData()
let context = collectionView.collectionViewLayout.invalidationContext(forBoundsChange: collectionView.bounds)
context.contentOffsetAdjustment = CGPoint.zero
collectionView.collectionViewLayout.invalidateLayout(with: context)
collectionView.layoutSubviews()

i was facing also this kind of issue so finally i am able to solved using this line of code
[self.collectionView reloadData];
[self.collectionView reloadItemsAtIndexPaths:#[[NSIndexPath indexPathWithIndex:0]]];

I had the same issue. I was updating the height of the UICollectionView based on the content and reloadData() stops working when you set the height of the collection view to zero. After setting a minimum height. reloadData() was working as expected.

Related

Show hints for new users in collectionView in iOS

I'm trying to create hints for new users in iOS application, like to open profile click here, etc. I have a collection view and inside it I have elements that I fetch from server.
I need to show hint near the first element. My code:
var tipView = EasyTipView(
text: "Выбирай предмет и начинай обучение",
preferences: EasyTipView.globalPreferences
)
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let section = sections[indexPath.section]
switch section {
case .fav_subjects:
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "FavSubjectsCollectionViewCell", for: indexPath) as! FavSubjectsCollectionViewCell
let favSubjects = model[.fav_subjects] as! [Subjects.FavSubject]
let cellModel = favSubjects[indexPath.item]
cell.configure(with: cellModel)
//here I'm trying to render hint near first element of collectionView
if indexPath.section == 0 && indexPath.item == 0 {
let view = cell.contentView
tipView.show(forView: view)
}
return cell
case .all_subjects:
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "AllSubjectsCollectionViewCell", for: indexPath) as! AllSubjectsCollectionViewCell
let allSubjects = model[.all_subjects] as! [Subjects.Subject]
let cellModel = allSubjects[indexPath.item]
cell.configure(with: cellModel)
return cell
}
}
}
My problem is when I open screen for the first time my hint displays on the top of the screen and not near the first element, but when I switch to another screen and then go back, hint displays exactly where expected.
I suggest that my problem is screen rendered for the first time before all items fetched from server so tried to use layoutIfNeeded() to rerender when the size is changed but it doesn't help:
tipView.layoutIfNeeded()
if indexPath.section == 0 && indexPath.item == 0 {
let view = cell.contentView
self.tipView.layoutIfNeeded()
self.tipView.show(forView: view)
}
Also I have tried to use viewDidAppear, because I thought that if screen is already rendered than I will have element rendered and I will show hint in the right place for the first render but it turns out that in viewDidAppear cell is nil:
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
let cell = collectionView.cellForItem(at: IndexPath(item: 0, section: 0))
}
Incorrect(when screen opened first time):
Correct:
I'm new to iOS so I think I missed something important, please help.
The method cellForItem(at:) gets called before the cells gets displayed on screen. In fact, at this point the final frame of the cell is not yet determined, it will happen later when the cell gets added to the view hierarchy and the layout pass happens.
In some cases even if cellForItem(at:) gets called, the cell may never appear on screen.
If you always display the hint near the first element, perhaps it will be easier to use the collection view's frame as reference instead of the first cell unless you need the hint to scroll along with the cell.
Alternatively, add the hint as a subview to the cell's contentView and use Auto Layout to define its position inside the cell. Don't forget to remove the hint from the view hierarchy when appropriate because the same cell may end up being dequeued for another index path during scroll.

UITableView with Full Screen cells - pagination breaks after fetch more rows

My app uses a UITableView to implement a TikTok-style UX. Each cell is the height of the entire screen. Pagination is enabled, and this works fine for the first batch of 10 records I load. Each UITableViewCell is one "page". The user can "flip" through the pages by swiping, and initially each "page" fully flips as expected. However when I add additional rows by checking to see if the currently visible cell is the last one and then loading 10 more rows, the pagination goes haywire. Swiping results in a partially "flipped" cell -- parts of two cells are visible at the same time. I've tried various things but I'm not even sure what the problem is. The tableView seems to lose track of geometry.
Note: After the pagination goes haywire I can flip all the way back to the first cell. At that point the UITableView seems to regain composure and once again I'm able to flip correctly through all of the loaded rows, including the new ones.
func tableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) {
// Pause the video if the cell is ended displaying
if let cell = cell as? HomeTableViewCell {
cell.pause()
}
if let indices = tableView.indexPathsForVisibleRows {
for index in indices {
if index.row >= self.data.count - 1 {
self.viewModel!.getPosts()
break
}
}
}
}
In order to create a "Tik Tok" style UX, I ended up using the Texture framework together with a cloud video provider (mux.com). Works fine now.
I was facing the same issue and as I couldn't find a solution anywhere else here's how I solved it without using Texture:
I used the UITableViewDataSourcePrefetching protocol to fetch the new data to be inserted
extension TikTokTableView: UITableViewDataSourcePrefetching {
func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
viewModel.prefetchRows(at: indexPaths)
}
}
prefetchRows will execute the request if the visible cell is the last one, as in my case
func prefetchRows(at indexPaths: [IndexPath]) {
if indexPaths.contains(where: isLastCell) {
getPosts(type: typeOfPosts, offset: posts.count, lastPostId: lastPost)
}
}
private func isLastCell(for indexPath: IndexPath) -> Bool {
return indexPath.row == posts.count - 1
}
I have a weak var view delegate type TikTokTableViewDelegate in my view model to have access to a function insertItems implemented by my TikTokTableView. This function is used to inform the UITableView where to insert the incoming posts at
self.posts.append(contentsOf: response.posts)
let indexPathsToReload = self.calculateIndexPathToReload(from: response.posts)
self.view?.insertItems(at: indexPathsToReload)
private func calculateIndexPathToReload(from newPosts: [Post]) -> [IndexPath] {
let startIndex = posts.count - newPosts.count
let endIndex = startIndex + newPosts.count
print(startIndex, endIndex)
return (startIndex..<endIndex).map { IndexPath(row: $0, section: 0) }
}
and this is the insertItems function implemented in TikTokTableView and here is the key: If we try to insert those rows, the pagination of the table will fail and leave that weird offset, we have to store the indexPaths in a local property and insert them once the scroll animation has finished.
extension TikTokTableView: TikTokTableViewDelegate {
func insertItems(at indexPathsToReload: [IndexPath]) {
DispatchQueue.main.async {
// if we try to insert rows in the table, the scroll animation will be stopped and the cell will have a weird offset
// that's why we keep the indexPaths and insert them on scrollViewDidEndDecelerating(:)
self.indexPathsToReload = indexPathsToReload
}
}
}
Since UITableView is a subclass of UIScrollView, we have access to scrollViewDidEndDecelerating, this func is triggered at the end of a user's scroll and this is the time when we insert the new rows
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
if !indexPathsToReload.isEmpty {
tableView.insertRows(at: indexPathsToReload, with: .none)
indexPathsToReload = []
}
}

In cellForItemAt collection view is not updating indexPath by collectionView.reloadData() in swift 3

I actually want to create table with rows and columns and doing this using UICollectionView. Problem is with reloading or updating collectionView to add or delete rows in my table. Columns and hardcoded i-e 4.
This is how i am doing;
Creating CollectionView from storyboard
After loading page. Hitting route to get data
After fetching data save the fetched data in an array object.
Update collection view with the size of the array by collectionView.reloadData().
Starting here.
Referring collection view from storyboard to my viewController
#IBOutlet weak var collectionView: UICollectionView!
then in "viewDidLoad".
self.collectionView.delegate = self
self.collectionView.dataSource = self
Now after getting response from server. I am updating my Array from which i get number of rows to create. After that This is how i am reloading collectionView.
DispatchQueue.main.async
{
self.collectionView.collectionViewLayout.invalidateLayout()
self.collectionView.reloadData()
print("size of the array is(self.portfolioHandler.data.count)")
}
In my numberOfSections function. i am getting correct number of cells to create. returning as well.
func numberOfSections(in collectionView: UICollectionView) -> Int {
var toReturn : Int = 1
if (self.portfolioHandler != nil) {
let data : [PortfolioData] = self.portfolioHandler.data!
print("dataCount : \(data.count)")
toReturn = data.count + 1
print("toReturn : \(toReturn)")
}
return toReturn
}
With logs i am getting the returned number. it is correct and returning data correctly.
Now in my cellForItem At index function. IndexPath is not updating. it is still 1. the top header i am creating. No Cell beneath it.
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "MyCell", for: indexPath) as! MyCollectionViewCell
print("section: \(indexPath.section) && row: \(indexPath.row)")
After everything i tried. Nothing helps me to get the correct cells to create in my collectionView.
A trick can work. Only if i give hardcoded value in numberOfSections in start. Then after server response and reloading collectionView it will create those number of cells like 3 or 4 what i hardcoded above. No more then that.
But i have to create number of cells which i get from server. Dynamic.
I have spent much time on it. Please help me where i am wrong. or how i can resolve this.
After all attempts. I am unable to solve this reload issue of collection view.
But I handle this in my app by this trick
collection view first time make cells correctly. problem is with reloading. So i made my server request in previous View Controller before coming to this View Controller. And sending the response to this View Controller. This way. i have number of cells to made. So it worked.
But as a learner i am looking forward for an answer and real solution to this reload issue problem.
I will appreciate that. Although thanks #ɯɐɹʞ for your comments.

UICollectionView set default position

I have a CollectionView with 3 Cells which extend across the whole device's screen and basically represent my 3 main views.
I have paging enabled, so it basically works exactly like the iOS home screen right now.
My problem is that I want the "default" position of this CollectionView to be equal to view.frame.width so that the second Cell is the "default" view and I can swipe left and right to get to my secondary views.
I have already tried via
collectionView.scrollToItem()
and
collectionView.scrollRectToVisible()
as well as
collectionView.setContentOffset()
but they all seem to work only after the view has loaded (I tried them via a button in my navigation bar).
Thank you in advance!
EDIT:
Now this works, but I also have another collection view in that one middle cell which holds a list of little 2-paged UICollectionViews which are actually objects from a subclass of UICollectionViewCell called PersonCell each holding a UICollectionView. I want these UICollectionViews to be scrolled to index 1 as well, this is my code:
for tabcell in (collectionView?.visibleCells)! {
if let maincell: MainCell = tabcell as? MainCell {
for cell in maincell.collectionView.visibleCells {
if let c = cell as? PersonCell {
c.collectionView.scrollToItem(at: (NSIndexPath(item: 1, section: 0) as IndexPath), at: [], animated: false)
}
}
}
}
This is executed in the viewDidLayoutSubviews of my 'root' CollectionViewController.
EDIT 2:
Now I tried using following code in the MainCell class (it's a UICollectionViewCell subclass):
func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
let personCell = cell as! PersonCell
personCell.collectionView.scrollToItem(at: (NSIndexPath(item: 1, section: 0) as IndexPath), at: [], animated: false)
}
EDIT 3:
Long story short, I basically need a delegate method that is called after a cell has been added to the UICollectionView.
Did you try [self.collectionView scrollToItemAtIndexPath:indexPath atScrollPosition:UICollectionViewScrollPositionCenteredHorizontally animated:YES];
Try calling the code in viewDidLayoutSubviews
Okay, I got it!
This SO answer gave me the final hint:
https://stackoverflow.com/a/16071589/3338129
Basically, now I just do this after initialization of the Cell:
self.collectionView.alpha = 1
self.data = data // array is filled with data
self.collectionView.reloadData()
DispatchQueue.main.async {
for cell in self.collectionView.visibleCells {
(cell as! PersonCell).collectionView.scrollToItem(at: (NSIndexPath(item: 1, section: 0) as IndexPath), at: [], animated: false)
}
self.collectionView.alpha = 1
}
Basically, the code in the DispatchQueue.main.async{...} is called on the next UI refresh cycle which means at a point where the cells are already there. To prevent the user from seeing the wrong page for a fraction of a second, I set the whole collectionView's alpha to 0 and toggle it on as soon as the data is there, the cell is there and the page has scrolled.

Updating Data and Reloading the CollectionView

I am attempting to update a data object that holds the data for my collectionView (simply just filtering the data model), and the wishing to reload the collectionView with the filtered data. The issue is upon reloading the collectionView I am getting and error indexPath out of range when the reload happens. Seems when debugging that the collectionView is attempting to reload the original dataObject, not the filtered one. What am I doing wrong:
func handleFilterAction(sender: FilterButton){
guard let type = sender.buttonFilterType else {return}
switch type {
case .FilterAll:
return
case .FilterDay:
return
case .FillterWeek:
return
case .FilterMonth:
collectionView?.performBatchUpdates({
let objects = FilterDataManager.filterDataWith(currentLocalData: &self.eventObjects, filterOption: .ThisMonth)
self.eventObjects.removeAll()
self.eventObjects = objects
}, completion: { (completion) in
self.navigationItem.title = "This Month"
self.collectionView?.reloadData()
})
}
self.handleShowFilterView()
}
The eventObjects is what the collectionView should be reading for the data:
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return eventObjects[section].sectionObjects.count
}
You call performBatchUpdates when you want to animate the changes to the collectionView. iOS expects you to make calls to methods such as collectionView?.insertItems(at: [IndexPath]) and
collectionView?.deleteItems(at: [IndexPath]) to let it know what you are inserting and deleting so that it can animate it. For both of these calls, you pass in an array of all of the IndexPaths being inserted or deleted.
When you do this, iOS has expectations for the number of items that are now in the collectionView. The formula is:
newItemCount = previousItemCount - deletedItemCount + insertedItemCount
In your case, you modified your model, but you never called the insertItems and deleteItems methods, so iOS still thinks all of the items are there. It is this disconnect between your model and the collectionView that is causing your problem.
To fix this, you need to do one of the following:
Call deleteItems and insertItems with the array of IndexPaths that you are deleting and inserting (call delete first).
Or, forget about performBatchUpdates and just update your model data and call reloadData.
Also, make sure numberOfSections is returning eventObjects.count.

Resources