Can't get cell from previous row in cellForRowAtIndexPath - ios

I have a tableView with a bunch of custom tableViewCells. I have a button that lets the user add a new cell at the bottom of the tableview. I want this new cell to have some data displayed depending on the data displayed in the cell above it. So, in my tableView(cellForRowAtIndexPath) delegate method, I call tableView.cellForRowAtIndexPath for the previous row and cast the result to my custom cell. However, this causes a crash.
if (indexPath.row > 0) {
let previousCell = tableView.cellForRowAtIndexPath(NSIndexPath(forRow: indexPath.row - 1, inSection: indexPath.section)) as! myCustomCell
}
Interestingly, if I don't force the cast, it doesn't crash. However, this is not useful for me since I need to force the cast and access some properties that only exist in my custom class. I am sure that the cell at (indexPath.row - 1) is a cell of my custom class.

As mentioned in the comments, UITableView.cellForRowAtIndexPath(_ indexPath: NSIndexPath) returns nil when the index path points to a cell that is currently not on the screen. See also the official documentation.
Instead of calling the method on the table view, you could call it on the table view's data source. UITableViewDataSource.tableView(_ tableView: UITableView,
cellForRowAtIndexPath indexPath: NSIndexPath) is a function you have to implement and it should return a proper cell regardless of whether the index path is on- or off-screen.
However, an even better approach (again, as mentioned in the comments) would be to get the cell contents from your data model instead of the cell.

Related

How to disable one specific tableView cell in ios Swift

I have created a custom UITableViewCell.I have only two rows in my tableView. Now I want to disable 2nd cell when 1st is tapped and disable 1st cell when 2nd is tapped.
How can I do that ? please Help and guide
Implement the UITableViewDelegate method willSelectRowAtIndexPath.
In that method, return the indexPath value that's passed to you if you want the user to be able to select it, or return nil if you don't want it to be selectable.
I'll leave it to you to figure out the logic that decides when different cells should/should not be selectable.
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
tableView.cellForRowAtIndexPath(NSIndexPath(forRow: (indexPath.row + 1) % 2, inSection: 0))?.userInteractionEnabled = false
}

How do I call a specific TableViewCell

The first cell in this TableViewController is suppose to alert the user when their status is liked or favorited. I need to create a function so when a button is tapped, it will call this table view cell and notify them who liked their photo. I am just not sure how to call this specific table view cell. Can anybody help me out? I have searched SO and Apples Swift Documentation with no luck.
In your didSelectRowAtIndexPath tableview delegate method you can get the index path (row position) of the cell they tapped on and then if you've got an array of data (used to populate the tableview in the first place) you know where to look to find out which notification/activity it is:
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
let selectedItem = items.objectAtIndex(indexPath.row) as String
}
You can also get the cell by adding this inside the method:
let cell = tableView.cellForRowAtIndexPath(indexPath)
You can get the reference to the cell with cellForRowAtIndexPath method of UITableView.

What is the reason for UISegmentedControl selected index is remembering (corresponding to indexPath) even cell is reused?

I have a cell with one UISegmentedControl as shown below
i know the code below is very wrong or its not a good logic
//UIViewController
var globalCell = segmentTblCell() //CUSTOM UITableViewCell Class
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 10
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = segTblVw.dequeueReusableCellWithIdentifier("segment", forIndexPath: indexPath) as! segmentTblCell
globalCell = segTblVw.dequeueReusableCellWithIdentifier("segment", forIndexPath: indexPath) as! segmentTblCell //THIS LINE GIVES VERY STRANGE OUTPUT
return cell
}
to my knowledge, that line is only creating new object of cells when invoking cellForRowAtIndexPath, it will create more memory issues..
but what is the speciality in that line for remembering segment selected index corresponding with cell indexPath?
and i tried,
in cellForRowAtIndexPath, put a NSLOG for printing address of globalCell, address of cell & indexPath.row , and on scrolling, cell is reusing, globalCell will create new objects
by adding UISwitch to my cell, output is same
scroll UITableView many times, it makes very slow because every time THAT LINE will create many cell
by adding UITextField to my cell, then i found it affect the segment selected index
Can anyone tell me the reason for this???
Reason for asking this question is, McDonal_11's answer in UISegment value changing when tableview get scrolled
regarding to my knowledge, that line is only creating new object of cells when invoking cellForRowAtIndexPath, it will create more memory issues.. but what is the speciality in that line for rememebering segment selected index?
the cell is only created when there are none in the pool. otherwise, you'll get one that was created before. what you'll want to do is set the cell state each time in cellForRowAtIndexPath
for example
let cellIdentifier = "Filler"
let cell = tableView.dequeueReusableCellWithIdentifier(cellIdentifier, forIndexPath:indexPath)
cell.propertyForSegIndex = 1 // or whatever i want it to be
This line:
globalCell = segTblVw.dequeueReusableCellWithIdentifier("segment", forIndexPath: indexPath) as! segmentTblCell //THIS LINE GIVES VERY STRANGE OUTPUT
Makes no sense, and should not be there. Get rid of it.
Here's the deal: When you call dequeueReusableCellWithIdentifier, you get back a cell that was probably used to display other data. It's views will have old values in them. In your case, you'll get cells that have their segmented control set to values other than the first segment.
When you fetch a recycled cell, you always have to fully configure it. Assume all views have old values. (Assume image views are not empty - they contain some other image. Labels contain the wrong text. Segmented controls have the wrong segment selected, etc.)

iOS UITableView: what's the different between "cellForRowAtIndexPath" and "willDisplayCell: forRowAtIndexPath:"

Just as the question's title mentions:
What's the difference between "cellForRowAtIndexPath" and "willDisplayCell: forRowAtIndexPath:"?`
I think cell configuration can be done either in cellForRowAtIndexPath or willDisplayCell: forRowAtIndexPath:"!
You are right, cell configuration can (in theory) be done in both methods.
However, almost all UITableView have a data source which implements cellForRowAtIndexPath: (it is a required method in the protocol). On the other hand, the willDisplayCell:forRowAtIndexPath: (which is a method of the delegate, not the data source) is optional.
As configuring a cell is usually dependent on the data you want to show, cellForRowAtIndexPath: is by far the most common place to do cell configuration. (I can't even remember using willDisplayCell:forRowAtIndexPath:).
There's one notable exception: when you are using a storyboard and static cells (instead of cell prototypes), you can't do anything useful in cellForRowAtIndexPath: (because dequeueReusableCellWithIdentifier: returns nil), so you have to do configuration in willDisplayCell:forRowAtIndexPath:, viewWillAppear: or other methods.
#NSDeveloper: you're right. Thanks for the hint.
cellForRowAtIndexPath should actually return a cell instance. It should be a re-used cell, when possible. This method is required for the UITableViewDataSource Protocol. This is normally where you select the data that will be displayed in the cell. With dynamic cells, it's common to set UI properties such as selected state at the same time you set the data here.
willDisplayCell is optional and is called after. This is your last chance to customize the cell before it's displayed. At this point, the cell instance has already been created. You can change things like selected state, etc. here. You should not be changing the data/structure of the cell or instantiating anything new, but only changing the state of UI properties for the cell. This is commonly used with static cells.
I think this answers your question:
But very important thing is still there:
tableView:cellForRowAtIndexPath: method, which should be
implemented in the dataSource of UITableView, called for each cell and
should work fast. So you must return reused cell instance as quickly
as possible.
Don’t perform data binding at this point, because there’s no cell on screen yet. For this you can use
tableView:willDisplayCell:forRowAtIndexPath: method which can be
implemented in the delegate of UITableView. The method called exactly
before showing cell in UITableView’s bounds.
From Perfect smooth scrolling in UITableViews
Despite what might seem intuitive, willDisplay cell: is called immediately after cellForRowAt indexPath: is called.
I had an app where images and video would be loaded in from either a URLCache or downloaded and displayed in the cell. I noticed whenever I'd start my application all the videos and images would load before I could see them and I noticed this because I could hear the audio from the last video in the tableView while looking at the first item in the tableView. This is with 8 items in the tableView. I printed to the console to get a better idea of what the order of the delegate functions calls were.
The results:
Created cell at row 0
Drew feed cell at row 0
Created cell at row 1
Drew feed cell at row 1
Created cell at row 2
Drew feed cell at row 2
Created cell at row 3
Drew feed cell at row 3
Created cell at row 4
Drew feed cell at row 4
Created cell at row 5
Drew feed cell at row 5
Created cell at row 6
Drew feed cell at row 6
Created cell at row 7
Drew feed cell at row 7
Stopped showing cell at row 2
Stopped showing cell at row 3
Stopped showing cell at row 4
Stopped showing cell at row 5
Stopped showing cell at row 6
Stopped showing cell at row 7
The Code:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "feedCell", for: indexPath) as! FeedCell
print("Created cell at row \(indexPath.row)")
let post = posts[indexPath.row]
cell.post = post
cell.viewController = self
cell.configureCell()
return cell
}
override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
if let feedCell = cell as? FeedCell{
print("Drew feed cell at row \(indexPath.row)")
// only want download task to start when the cell will display
if feedCell.post.isVideo {
feedCell.loadVideo()
} else {
feedCell.loadImage()
}
}
}
override func tableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) {
print("Stopped showing cell at row \(indexPath.row)")
if let feedCell = cell as? FeedCell{
feedCell.player?.pause()
}
}
I faced an issue with cell configuration in willDisplayCell: when used autolayout and UITableViewAutomaticDimension. The heights of reused cells didn't calculate properly. Printing methods in NSLog shows that willDisplayCell: is called after heightForRowAtIndexPath: (tested on iOS 10.2 simulator)
cellForRowAtIndexPath: <NSIndexPath: 0x7c10daa0> {length = 2, path = 1 - 0}
heightForRowAtIndexPath: <NSIndexPath: 0x7c10daa0> {length = 2, path = 1 - 0}
heightForRowAtIndexPath: <NSIndexPath: 0x7c10daa0> {length = 2, path = 1 - 0}
willDisplayCell: <NSIndexPath: 0x7c10daa0> {length = 2, path = 1 - 0}
cellForRowAtIndexPath: <NSIndexPath: 0x7c4516f0> {length = 2, path = 1 - 1}
heightForRowAtIndexPath: <NSIndexPath: 0x7c4516f0> {length = 2, path = 1 - 1}
heightForRowAtIndexPath: <NSIndexPath: 0x7c4516f0> {length = 2, path = 1 - 1}
willDisplayCell: <NSIndexPath: 0x7c4516f0> {length = 2, path = 1 - 1}
I think this was the reason, because problem has gone after placing configuration code in cellForRowAtIndexPath:
P.S. There is an opposite article to the link and quote posted by #iTSangar: https://tech.zalando.com/blog/proper-use-of-cellforrowatindexpath-and-willdisplaycell/
The delegate method willDisplayCell will be called every time a cell is just about to get displayed on the screen. Say for example - you are downloading any image data, then this is the best place to do the download or call the method that downloads image data.
The problem with downloading image data in cellForRow or cellForItem is, a large number of images/photos comes back from the initial request. The user may never even scroll down far enough to see all of the images. This is very costly as it may eat up users cellular data.
So the big vote here for willDisplayCell is User may never scroll down, so why download all images in cellForRow?. Make use of willDisplayCell to only download those images user is about to see.
So this way you supply the cell in cellForRowAtIndexPath and update the content in willDisplayCell
Also note that cellForRow is called first and then willDisplayCell.

Handling button interaction of a dequeueReusableCell in iOS Swift

I am making an iOS app that relies on a table view. In each cell of the table view, there are 4 buttons aligned on the bottom. I have a cell class that is pretty standard and a feedController to handle the table and setting all the items of the cell.
Everything works fine on it but I can not figure out how to handle the button clicks within the cell. I can hard code it into my cell class, but then every 3 cells has the same interaction. Is there a way to pass the button click function from the cell class into the controller? I have tried checking the state from the controller and that has not worked.
Can you add a gesture recognizer as you're doing your cellForItemAtIndexPath? So I had something similar with a collection view, and what I did was as it within:
func collectionView(collectionView: UICollectionView!, cellForItemAtIndexPath indexPath: NSIndexPath!) -> UICollectionViewCell!
{
var cell = collectionView.dequeueReusableCellWithReuseIdentifier("cell", forIndexPath: indexPath) as MyCollectionView
...
I would add a gesture recognizer to each cell
i.e.
cell.addGestureRecognizer(UITapGestureRecognizer(target: self, action:Selector("tapAction:")))
And then something like:
func tapAction(recognizer: UITapGestureRecognizer) {
...
}
so recognizer ends up being the specific item tapped, and I could take action accordingly (in my case, I had my datasource of items and I would find the item in an array by casting recognizer to a cell, finding the appropriate subview, and update values on it)
I would add code block properties to your cell class which the table can assign to deal with each button. In your cell, code each button handler to call the appropriate block, or pass an index for the button used in a single block.
See my answer here which has an example, but for a switch.
How can I get index path of cell on switch change event in section based table view
If after a few cells you get the same interaction, it's possibly because you're dequeueing a reusable cell, and you're getting the same cell.
Make sure to set your .setTarget() call for your buttons in your tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) data source every time the cell is dequeued. It would help if you shared how you're handling dequeuing to see if this is your issue.

Resources