When does cellForItemAtIndexPath get called? - ios

Can you make an assumption as to when collectionView:cellForItemAtIndexPath: will get called? I have a UICollectionView where each cell is the same size as the collection view. Most cells get dequeued just as they become visible. Sometimes, however, collectionView:cellForItemAtIndexPath: gets called not only for the next index path but also for the one after that (at the same time). For example, if I'm currently seeing index 5, and start scrolling, but 6 and 7 will get dequeued.
Is the way collectionView:cellForItemAtIndexPath: gets called documented somewhere, or are we not supposed to assume any recurrent functionality?

or are we not supposed to assume any recurrent functionality
That is correct. I can name some calls you can make that will cause it to be called, but that's not the point. The point is that your job is to be ready to answer this question correctly and quickly at any moment. The runtime calls you as often as it thinks might be necessary in order to display the current set of cells and in order to make future scrolling as smooth as possible. As some poet says: "That's all you know and you need to know."

Related

UICollectionViewCell does load only when on view?

I'm developing an Chat application where I have a UICollectionView to control the messages and I came to a situation I would like to confirm with you.
For exemple, let's say I have 60 items in this UICollectionView, but based on the size of the items and the scrolling options I set, only the last 10 items are visible on the screen, from 50 to 59.
Based on that, it seems I'm not able to get cellForItem at IndexPath 30, for example. Is that correct?
I would like to confirm that with you before creating a solution to go over the items that are already "on screen" and I need to check. Any ideas and solutions you have already implemented is appreciated.
Also, based on the information above if, for example, I need to move on item from index path 30 to 31, will I have problems if they are not "instantiated" in the screen?
Thanks in advance!
You seem to be mixing your model, controller, and view classes, which is a bad thing™ for exactly the reason you encounter here.
I take it you're trying to access data from the index 30 (basically) and say to yourself "Hey, I already added that in the 30th cell, so I will just use the collection view's method to get that cell and take it from there". That means, you basically ask a view for data.
That won't work, because, as others pointed out (but more indirectly), there are not 60 cells at all at any given moment. There's basically as many cells as fit on the screen, (plus perhaps one or a few "buffer" cells so rendering during scrolling works, I can't remember that atm). This is why cellForItem(at:) is nil for an IndexPath that refers to a cell not actually visible at the moment. Basically it works in a similar way to a table view. The collection view simply does not keep around stuff it doesn't need to render for memory reasons.
If you need anything from a cell (which is after all also a view) at this path, why don't you get it from whatever data object represents the contents of this cell? Usually that's the UICollectionViewDataSource.
That's how the paradigm is supposed to work: The UICollectionViewDataSource is responsible for keeping around any data your app may need at a given time (this may or may not reloading it or parts of it, your choice). The UICollectionView uses its collectionView(_:cellForItemAt:) method when a certain IndexPath becomes visible, but it throws that away again (or rather queues it again so your data source may dequeue it in collectionView(_:cellForItemAt:) and reuse it for another data set that becomes visible).
And btw, please don't use use the UICollectionViewDataSource's collectionView(_:cellForItemAt:) method to get the cell and then the data from there. This method is supposed to be called by the collection view and depending on how you reuse cells or create them, this might mess up the entire process. Or at the very least create view-related overhead. Instead, get the data in the same way your UICollectionViewDataSource would get in inside of the method. Wrap that in an additional method you rely on or the like. Or, even better, rely on the model object that the controller uses as well.
Edit in response to your comment:
No, I did not mean it's bad to use a UIViewController as a UICollectionViewDataSource for a UICollectionView. What I meant was that it's bad to use the UICollectionView to get data, because that's what the data source is for. In your question you were wondering why cellForItem(at:) gives nil. That method is defined on UICollectionView. You didn't mention your intention was to move items around (I'll explain in a second), so I assumed you were trying to get whatever data was in the cell (I know, "assume makes an ass out of u and me...", sorry :) ). This is not the way to go, as the UICollectionView is not meant to hold the data for you. Rather, that's your job, and you can use a UICollectionViewDataSource for that. This latter class (or rather protocol a class can adopt) is basically meant to offer an interface for UICollectionView to get the data. It needs that, because, as said, it doesn't keep all data around. It requests stuff it needs from the data source. The data source, on the other hand, can manage that data itself, or maybe it relies on some deeper class architecture (i.e. other objects taking care of the underlying model) to get this. That part depends on your design. For smaller scenarios having the data source simply have the data in an array or dictionary is enough. Furthermore, a lot of designs actually use a UIViewControllerto adoptUICollectionViewDataSource`. That may be sufficient, but be careful not to blow up your view controller to a monstrosity that does everything. That's just a general tip, you have to decide on your own what is "too much".
Now to your actual intention: To move around cells you don't need to get them. You simply tell the UICollectionView to move whatever is at a given index path to some other index path. The according method is moveItem(at:to:). This works even if cellForItem(at:) would return nil for one of the two index paths. The collection view will ensure the cells are there before they become visible. it does so relying on the data source again, more specifically its collectionView(_:cellForItemAt:) method. Obviously that means you have to have your data source prepared for the move, i.e. it needs to return the correct cell for the given index. So alter your data source's internal storage (I assume an array?) before you move the items in the collection view.
Please see the documentation for more info on that. Also note that this is basically how to move items around programmatically. If you want the user to interactively move them around (in a chat that seems weird to me, though), it gets a little more complicated, but the documentation also helps with that.
Based on your question. If the currently visible cells on screen are from 50 to 59, the cellForItem at IndexPath 30 will not be available. It would be nil. Reason being the 30the cell would have already been reused to display one of the cells from 50 to 59.
There would not be problem to move cell from 30 to 31. Just update your array/data source and reload the collection view.
You can access the cell only if its visible for non visible cell you need to scroll programmatically using indexpath:-
collectionView.scrollToItem(at: yourIndexPath, at: UICollectionViewScrollPosition.top, animated: true)

How to add cell to tableview's reuse queue before 'cellForRow:'?

Creating a cell costs a lot of time and make the first scroll lagging, so I want to create a cell and add it to tableview's reuse queue before cellForRow: called.
I use dequeueReusableCellWithIdentifier: in viewDidLoad, but when I scroll the table, the cell is being created again.
In general all drawing methods of scrollview should be kept as simple as possible to avoid lag. This means you should prepare your data/model in viewDidLoad/viewWillAppear or even in previous ViewController. Your cellForRow should be as simple as set this image(s) and those text(s) - no checks, no expensive operations such as bluring, retrieving data from CoreData/Network, etc.
If you are not sure which thing exactly causes your lag, you should learn how to use TimeProfiler. If you feel lost in documentation, have a look this(quite outdated though) tutorial.
With thus said I cannot be able to help you anymore until you post some code which we could discuss.

Can I find out when a cell is queued?

I have a couple of things in my custom UITableViewCell's that I'd like to clean up before the cell gets queued. In a normal view, I'd put these kinds of calls in -(void)dealloc, but as they're being re-used rather than having an instance each, they won't dealloc. For now, I'd just like to print out a text in the log, NSLog(#"Cell out"); when the cell is queued, or "done".
I'm really just looking for something like -(void)didQueue in the cell-class, but my searches haven't shown anything.
I just found -(void)prepareForReuse, but I need more like prepareForQueue. I have to know when it's on its way out, not back in.
Random thought..: Searching for when the cell leaves the screen (or a bit after) might do exactly the same as what I'm looking for, but I'm thinking that costs a lot of processing..
For iOS 6+ the table view delegate will receive tableView:didEndDisplayingCell:forRowAtIndexPath: which tells you when the cell is not being used any more (irrelevant of queueing).
That said, prepareForReuse would generally be considered the correct place for the code you describe. If it isn't then you are probably assigning the cell responsibility it shouldn't have.

UITableViewCells with long labels won't indent in editing mode

I have a plain (UITableViewStylePlain) UITableView with basic (UITableViewCellStyleDefault) UITableViewCells in iOS 6.1. When it enters editing mode, its cells indent as I want them to. But only if all cell labels are short: if one is long enough to be clipped on the right side, none of the table cells will indent any more.
For instance:
table with one cell: (SHORT) => indents i.e. works
table with two cells: (LONG) (SHORT) => neither cell indents i.e. does not work
What simple steps can remedy this situation? E.g., it appears as if I cannot change the preset size properties on a basic, i.e. non-custom table view cell in Xcode.
UPDATE: Here are two images that further describe the problem (1st: correct case, 2nd: incorrect case):
UPDATE: It has turned out that the root cause is not the lengths of labels. Instead it seems to be about my async. KVO handling in relation to this table view. My tableView:cellForRowAtIndexPath: calls a getter on the cell's underlying managed (Core Data) object. It seems that managed objects' default getters in turn call their own setters, probably when faulted objects are realized. Because of the way my KVO is set up, this in leads to another call of tableView:cellForRowAtIndexPath:. As it so happens, only the 2nd case involved a KVO notification and the ensuing recursive call may cause the problem (it seems slightly odd in any case) ...
I have been able to resolve this by "prefetching" the underlying managed objects in the constructor of the table view's data source. I do this by accessing the property that is displayed in the table cell. That way the first KVO notifications are triggered in a context where they cannot lead to unwanted recursive invocations of tableView:cellForRowAtIndexPath:.
If there is a better (more elegant) way to handle the situation, I'd still be interested to learn about it.

Can I make my tableview move more fluidly by reducing calling [CALayer LayoutSubviews], and how?

According to time profile, I found that my app waste too much time on [CALayer layoutSublayers] due to calling [UITable layoutSubviews]. So the action generating tableview is not smooth, whenever tap the button at first time, I would wait almost 2 second until see next view. Each of the cell in the tableview of next view have 8 subview resulting in layoutSubviews engaged too much time. Thus I want to know how to optimize this progress, and I don't want to build a custom cell contain these 8 subviews in its drawrect: to avoid layoutSubviews. Who can help me?
My guess would be that what you see is a symptom, not the real problem.
I would wait almost 2 second until see next view
What you describe sounds like you would prerender all of the table view's cells (causing massive layout costs) when you first try display it.
Set a breakpoint in cellForRowAtIndexPath:, add a sound action + activate 'Automatically continue after evaluation' to debug this.
It should only get called like 10 times or so (depending on number of rows that are simultaneously visible).
If you are using heightForRowAtIndexPath: on long lists, that may be the source of your problem (depending on your implementation). In that case consider to avoid using it. If you target iOS 7 look into tableView:estimatedHeightForRowAtIndexPath: it might help.
(link: https://developer.apple.com/library/ios/documentation/UIKit/Reference/UITableViewDelegate_Protocol/Reference/Reference.html).

Resources