collectionView: reloadData works, reloadItemsAtIndexPaths does not - ios

I have a UICollectionView whose source data changes sometimes when offscreen. To update it when it returns, I wrote in the CollectionViewController:
override func viewWillAppear(animated: Bool) {
self.collectionView!.reloadData()
}
...and that works, but I never need to update more than one or two cells in my UICollectionView, so I thought it'd be better if I replaced the above version with:
override func viewWillAppear(animated: Bool) {
self.collectionView!.reloadItemsAtIndexPaths(myArrayOfIndexPaths)
}
...but when I do, Xcode gives me an error saying:
Operand of postfix '!' should have optional type, delete '!'".
When I delete the '!', it says that UICollectionView doesn't have a member named reloadItemsAtIndexPaths. What am I doing wrong?

From your code it looks like you have declared your collectionView as Optional (with ?) at the end and maybe linked it to your storyboard using an #IBOutlet. To fix your issue you should remove the ? from the :
#IBOutlet var collectionView: UICollectionView?
and substitute it with:
#IBOutlet var collectionView: UICollectionView!
this way you are telling the compiler that your collectionView definitely exists (because you have linked it from your storyboard).
Alternatively, you can bind your collectionView by doing this:
if let collectionView = collectionView {
collectionView.reloadItemsAtIndexPaths(myArrayOfIndexPaths)
}

Swift 4 reload all items in section
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
var indexPaths: [NSIndexPath] = []
for i in 0..<collectionView!.numberOfItems(inSection: 0) {
indexPaths.append(NSIndexPath(item: i, section: 0))
}
collectionView?.reloadItems(at: indexPaths as [IndexPath])
}

Swift 3 version code
var indexPaths = [IndexPath]()
indexPaths.append(indexPath) //"indexPath" ideally get when tap didSelectItemAt or through long press gesture recogniser.
collectionView.reloadItems(at: indexPaths)

as a beginner i didn't know that there's a diference when you implement a collectionView as a direct child from another viewController and implement it as an embedded view inside a viewController, maybe some dev is facing the same problem and they doesn't know, if so, look for the correct approach :)

If you want to reload for single cell, you can specify the specific of the cell like this :-
for index in indexPaths {
if index == [0, 0] {
print(index)
self.collectionView?.reloadItems(at: [index])
}
}
Hope this helps.

Related

indexPath is nil when passing cell from delegate to UITableViewController

I have a button called addSet at the end of each section of my tableView, it is used as a footerView and it is supposed to tell the UITableViewController of when it is pressed and in which section. My code for the custom table view cell is as follows
import UIKit
class FooterTableViewCell: UITableViewCell {
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
var footerDelegate:FooterTableViewCellDelegate?
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
#IBAction func addSetIsPressed(_ sender: AnyObject) {
print("Add Set is pressed")
footerDelegate?.didAddSetIsPressed(cell:self)
}
}
protocol FooterTableViewCellDelegate {
func didAddSetIsPressed(cell:FooterTableViewCell)
}
And in my TableViewController, I implement it like so
func didAddSetIsPressed(cell: FooterTableViewCell) {
let indexPath = tableView.indexPath(for: cell)
print("Index path is \(indexPath)")
}
I want to get the indexPath (the section specifically) when the user taps my button, however it always returns nil. What am I doing wrong?
To put things in context. I am using this cell as a footerView, so the cell is implemented like so
override func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? {
let cell = tableView.dequeueReusableCell(withIdentifier: "footerCell") as! FooterTableViewCell
cell.footerDelegate = self
return cell
}
so it isn't implemented in cellForRow at indexPath like it would normally be
Thanks in advance.
The thing is you put the cell FooterTableViewCell as a viewForFooterInSection,
so it's not used as a UITableViewCell in the UITableView, so the UITableView is not holding the indexPath of this UITableViewCell "Cause i said previously, the cell's view only is used as a footerView"
You need to add the button inside the cell that's being rendered on the UITableView. "The one that's being returned in the tableView(_:cellForRowAt:) method"
On a side note i noticed that you have a variable named footerDelegate in your cell, it needs to be weak to avoid memory leaks as you assign your TableViewController as this delegate,
so the UITableViewCell holds a strong reference of the TableViewController that leads to memory leak cause also in the view hierarchy the TableViewController contains the UITableView as a subView.
I found out how to do it, in order to detect the section in which the button was tapped. There must be an outlet reference in the FooterCell and in the tableViewController, in viewForFooter in Section, just add the following line
cell.addSetOutlet.tag = section

UICollectionView reloadData not working

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.

UICollectionView some cells are neither visible items nor dequeued cells

I have an issue in my UICollectionView where some cells seem to be neither part of the indexPathForVisibleItems, nor are they taken from the caching queue with dequeueReusableCell. The result is, that some cells don't receive required updates of data during a scroll and show old behavior.
For simplicity, I reduced the project to the neccessary Controllers and a minimized Storyboard. Basically, I've got a NavigationController as EntryPoint that contains the MainViewController, which itself contains a ContainerView with the CollectionViewController.
The NavigationController uses the default edit button to switch between edit and non-edit mode - this should result in an image displayed on the cells while in edit mode. Therefore I implemented setEditing and changed the images hidden property of all visible cells, and additionally i set the images hidden property while dequeuing - assuming that cells are either visible or they will be dequeued in the future.
This works fine while the CollectionView is scrolled from top to bottom. But when I switch back from Edit-Mode to Non-Edit-Mode while scrolled to the bottom and then scroll back to the top, some cells still display the image (more specific: at least the same row, which is the first non-visible row). Somehow I'd assume that the dequeued cells and the visible cells would be complementary parts of the displayed data, which should result in either the images being hidden/unhidden during the setEditing call (which works for the first 4 rows of cells) or being hidden/unhidden during the dequeuing (which works for the last few rows, except the third row in my example)
Code for the CollectionViewController:
import Foundation
import UIKit
import Photos
class CollectionViewController : UICollectionViewController {
fileprivate let CELL_ID = "PicCell"
fileprivate let IMAGE_VIEW_SIZE = 104
var selectedIndex = -1
var itemCount = 28
override func setEditing(_ editing: Bool, animated: Bool) {
super.setEditing(editing, animated: animated)
print(self.isEditing)
collectionView?.allowsMultipleSelection = editing
for indexPath in (collectionView?.indexPathsForSelectedItems)! {
collectionView?.deselectItem(at: indexPath, animated: true)
}
for indexPath in (collectionView?.indexPathsForVisibleItems)! {
let cell = collectionView?.cellForItem(at: indexPath) as? PicCell
if cell != nil {
cell?.editing = editing
}
}
}
override func viewDidLoad() {
self.navigationItem.rightBarButtonItem = self.editButtonItem
collectionView?.reloadData()
}
}
extension CollectionViewController {
override func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return itemCount;
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
print(indexPath.row)
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: CELL_ID, for: indexPath) as! PicCell
cell.editing = isEditing
cell.isSelected = false
return cell;
}
}
Code for PicCell
import Foundation
import UIKit
class PicCell : UICollectionViewCell {
#IBOutlet weak var deleteBtn: UIImageView!
var editing:Bool = false{
didSet {
self.deleteBtn.isHidden = !editing
self.deleteBtn.tintColor = UIColor.blue
}
}
override var isSelected: Bool{
didSet {
if isSelected && editing {
self.deleteBtn.tintColor = UIColor.red
} else {
self.deleteBtn.tintColor = UIColor.blue
}
}
}
override func prepareForReuse() {
super.prepareForReuse()
self.editing = false
self.isSelected = false
}
}
Code for MainViewController
import Foundation
import UIKit
class MainViewController: UIViewController {
override func viewDidLoad() {
self.navigationItem.rightBarButtonItem = self.editButtonItem
}
override func setEditing(_ editing: Bool, animated: Bool) {
super.setEditing(editing, animated: animated)
collectionController?.setEditing(editing, animated: animated)
}
var collectionController : CollectionViewController? = nil
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "ShowCollectionView" {
collectionController = segue.destination as! CollectionViewController
}
}
}
as you can see I'm using constant values for the item count in a single section, which shouldn't be a big deal.
I don't know if it matters, but my cells are 104px in width and height, the image is rendered as template and the UIContainerView is 343 in width and 473 in height, i'm testing on an iPhone 7+ simulator with iOS 10.1
If there's anything missing from the storyboard, i might add some screenshots, but i'm unsure what to post exactly
Thanks in advance for your help and kind regards
Christian
Edit: just to be clear with my question: I'm looking for some advice for either obvious mistakes in my code or a way to access UICollectionViewCells that are neither in the list of visible items nor dequeued during scroll - there might be a way to access those cells i simplay don't know about
You can change this behavior if you turn off prefetching:
collectionView?.isPrefetchingEnabled = false
Or you can keep it on, but then either:
hook into UICollectionViewDelegate methods didEndDisplaying and willDisplay to know as cells appear and disappear independent of cellForItemAt; or
have cells do some KVO on some view controller property or observe some notification that the view controller will initiate in order to know whether to change their state.

Swift configure custom cell

I have this code in uitableview controller:
var results: [Item] = []
...
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("OrderCell") as! OrderTableViewCell!
cell.item = results[indexPath.row]
return cell
}
and OrderTableViewCell.swift:
class OrderTableViewCell: UITableViewCell {
var item: Item! {
didSet {
self.setupCell()
}
}
#IBOutlet var itemNo: UILabel!
#IBOutlet var itemPrice: UILabel!
override func awakeFromNib() {
super.awakeFromNib()
}
override func setSelected(selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
}
func setupCell() {
itemNo.text = order.document_no
itemPrice.text = order.sum
}
}
Now, i need to know is this good way to setup cell(with didSet), or it is better to fill this fields itemNo and itemPrice from uitableview controller?
IMHO, this is better than adding view-dependent logic to your UITableViewController.
iOS apps frequently utilize the "model-view-controller" pattern, which you have here. While the details of this pattern can vary, it is generally considered a good thing to have "loosely coupled" components. Meaning, the controller shouldn't be concerned with which table cell (view) labels are showing what model information.
Additionally, encapsulating your logic like this inside of the view like this sets you up to cleanly observe updates to Item (your model) in the future (completely eliminating the need for additional controller interaction). You could accomplish this automatic updating via something like Key-Value Observing.
The Wikipedia page on MVC has an excellent graphic to explain how the components typically interact.
Source: https://en.wikipedia.org/wiki/Model–view–controller#Description
It seems to me that it would be better done like so:
func setupCell(_data:DataObject) {
// update layout depending on data
itemNo.text = _data.document_no
itemPrice.text = _data.sum
}
Using this:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
// this way avoids forced unwrapping. much safer
if let cell = tableView.dequeueReusableCellWithIdentifier("OrderCell") as? OrderTableViewCell{
cell.setupCell(results[indexPath.row])
return cell
}
return UITableViewCell()
}
Keep in mind that because of dequeueReusableCellWithIdentifier the tableview is reusing the instances of the cell. Every time a cell is displayed cellForRowAtIndexPath is called over again and that cell needs to be laid out again. You don't its data to be permanent.

How do I get the row of a custom UITableViewCell using a button in the custom cell, that will be sent to deleteRowsAtIndexPaths

I have made a table view in iOS that displays a list of buddy (friend) requests. For the buddy request cell, I have made it a prototype cell and have given it a custom class that extends from UITableViewCell. When I click the "Accept" button on the cell, I want to remove that row from the requests array I have and remove it from the table view as well.
The three options I have considered are
1) Giving the custom cell a property for row that corresponds to the row in the table, and hence, the row in the requests array. Then, when accept is called, pass that row to the delegate function and call
requests.removeAtIndex(row)
tableView.reloadData()
which updates all the custom cells' row property. This method works. However, is this a bad practice to reload the table data (it's only reloading from the stored array, not making a network request)
2) Giving the custom cell the row property, but then calling
self.requests.removeAtIndex(row)
self.requestsTableView.beginUpdates()
self.requestsTableView.deleteRowsAtIndexPaths([NSIndexPath(forRow:row, inSection: 0)], withRowAnimation: UITableViewRowAnimation.Fade)
self.requestsTableView.endUpdates()
However, this does not update the row value in each of the cells following the deleted cell, and I would somehow either have to update them all, or call reloadData() which isn't what I want to do.
3) Instead of passing the row value, when the "Accept" button is clicked, search for the username in the buddies list, get the index of where it is found, and then delete the row in the table using that index and deleteRowsAtIndexPaths. This seems okay to do, especially since I'll never have a huge amount of buddy requests at once and searching won't require much time at all, but I figure if I had immediate access to the row value, it would make things cleaner.
Here is the code:
View Controller
class RequestsViewController: UIViewController, UITableViewDataSource, UITableViewDelegate, RequestTableViewCellDelegate
{
// Outlet to our table view
#IBOutlet weak var requestsTableView: UITableView!
let buddyRequestCellIdentifier: String = "buddyRequestCell"
// List of buddies who have sent us friend requests
var requests = [Buddy]()
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewDidAppear(animated: Bool) {
self.getBuddyRequests()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
// MARK: -Table View
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return requests.count
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell: RequestTableViewCell = tableView.dequeueReusableCellWithIdentifier(buddyRequestCellIdentifier) as! RequestTableViewCell
let buddy = requests[indexPath.row]
let fullName = "\(buddy.firstName) \(buddy.lastName)"
cell.titleLabel?.text = fullName
cell.buddyUsername = buddy.username
cell.row = indexPath.row
cell.delegate = self
return cell
}
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
let buddy = self.requests[indexPath.row]
}
func didAccpetBuddyRequest(row: Int) {
// Remove buddy at the 'row' index
// idea 1: update all cells' 'row' value
//self.requests.removeAtIndex(row)
// reloading data will reload all the cells so they will all get a new row number
//self.requestsTableView.reloadData()
// idea 2
// Using row doesn't work here becuase these values don't get changed when other cells are added/deleted
self.requests.removeAtIndex(row)
self.requestsTableView.beginUpdates()
self.requestsTableView.deleteRowsAtIndexPaths([NSIndexPath(forRow:row, inSection: 0)], withRowAnimation: UITableViewRowAnimation.Fade)
self.requestsTableView.endUpdates()
// idea 3: don't use row, but search for the index by looking for the username
}
// MARK: -API
func getBuddyRequests() {
// self.requests = array of buddy requests from API request
self.requestsTableView.reloadData()
}
}
Custom UITableViewCell and protocol for the delegate call
protocol RequestTableViewCellDelegate {
func didAccpetBuddyRequest(row: Int)
}
class RequestTableViewCell: UITableViewCell {
#IBOutlet weak var titleLabel: UILabel!
#IBOutlet weak var acceptButton: UIButton!
var delegate: RequestTableViewCellDelegate?
var buddyUsername: String?
var row: Int?
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
}
#IBAction func touchAccept(sender: AnyObject) {
// <code goes here to make API request to accept the buddy request>
self.delegate?.didAccpetBuddyRequest(self.row!)
}
}
Thanks for taking the time to read this, I appreciate any help/best practices that you know that could help me in this situation.
There shouldn't be a problem with giving the cell the indexPath and delegate properties, and then informing the delegate when the Accept button has been tapped. You do need to call reloadData(), though, to update the references in the cells that are affected.
If you wish to minimise the number of reloaded rows, call reloadRowsAtIndexPaths() instead, but I think that creating the loop that creates the NSIndexPath objects will slow your app down just the same.
As an alternative I can suggest you another way:
First add action method to your acceptButton in viewController. Inside that method you can get indexPath of the cell that contains button. Here is implementation
#IBAction func acceptDidTap(sender: UIButton) {
let point = tableView.convertPoint(CGPoint.zeroPoint, fromView: button)
if let indexPath = tableView.indexPathForRowAtPoint(point) {
// here you got which cell's acceptButton triggered the action
}
}

Resources