Which method would be better to bind data with UITableView? - ios

For a long time, I have been binding data with table view cells in the delegate method - tableView:cellForRowAtIndexPath:.
But recently, I read a blog, which is about how to get a better performance. And it tells us that we should bind data with table view cells in another delegate method - tableView:willDisplayCell:forRowAtIndexPath:, since cells are not going to be displayed in - tableView:cellForRowAtIndexPath:.
And I try it in my personal project, but it seems weird. Some cells are overlapped.
Before that in my project, I use Masonry to layout the subviews of the cells. After I bind data with table view cells in - tableView:cellForRowAtIndexPath:, I call [cell setNeedsLayout] and [cell layoutIfNeeded].
And I start to think, which is the better method to bind data? Or, how should I modify my code to get such performance with the least cost(time of modifying).

It appears there is an ongoing debate on this topic.
I think the article you are referring to is found here. It argues that you should do data binding in willDisplayCell instead of cellForRowAtIndexPath.
However, this article points out that the cellForRowAtIndexPath and willDisplayCell are performed in the same layout cycle which means that data binding in either place would have the same effect.
The thing that they both agree on is the fact that calculating the height of the row should be very fast. Having a complex calculation may lead performance issues.
In any case, it's important to write good model code so that your view controllers can focus on hooking up the model to the view. Downloading images to display within UITableView code and other network/device intensive processes is a generally bad idea and can cause issues.
Moral of the story: calculate row height as quickly as possible and write good model code so that your view controller can do less computation.

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.

UITableview cell reinitializing every time in iOS 7

All,
I hope most of you know that with ios7 there is not need to do a null check for tableview reuse
if (cell == nil) {
But unfortunately, because of that the cells are always reinitialized, as we put the code in the same method for initializing values. The problem is only with text fields inside the tableview though.
Let me explain the scenario. I have a table view with multiple rows, and some rows contain multiple text boxes. I populate the textboxes with data from server when the page is loaded. Since the cells are always re-initialized as i explained above, whatever I enter in the field goes away and the server data is re populated once i scroll down and come back to the initial stage. This is because the populating the data code is also in the same place. After fetching a reusable cell it populates the data.
Previously till ios6, we used if(cell==nil) and hence we loaded server data inside the cell and when reusing the cell, this piece of code will never be called.
I have other dirty solutions, but would like to know if someone else has a graceful way of dealing this. Please help.
You just don't store any data in the table view cell but in the model that fills the table cell. This is always the way it should be done.
Looking from the MVC standpoint than the UITableViewCell is a view. Since it is reused by iOS you should use a model to the view.
Yes, this is the expected behavior of UITableView. For performance reasons, cells are reused. Thus, it is your responsibility to populate the views in a Table View Cell every time tableView:cellForRowAtIndexPath: is called.
The thing I don't understand from your question - are you making a network call every single time a cell comes into view? If so, cache the results somewhere. Or, if it's a small amount of data, consider just doing it all in one shot at the beginning (still need to be asynchronous though).
One thing I see a lot of developers do is move a lot of code into UITableViewCell subclasses, which sounds like a good idea because it's modular, but makes solutions for problems like this more difficult. Have the Table View Data Source manage the network calls.
If you need some inspiration, look at Apple's LazyTableImages sample.

Load tableViewCell from Storyboard without using dequeueReusableCellWithIdentifier

I want to load a tableViewCell from the storyboard without using dequeueReusableCellWithIdentifier to create a prototype cell to reference before cellForRowAtIndex is called. Calling that outside of CellForRowAtIndex does funny stuff.
dequeueReusableCellWithIdentifier is still being used in cellForRowAtIndex as it normally has been.
I dynamically set the cell heights based on it's contents. This requires me to know where the size, positions, text attributes like font sizes, alignments, etc of views in the tableViewCell. Otherwise I have to hard code these values to match what's in the storyboard.
What I'm currently doing is create a new xib file with just the cell, load it from viewDidLoad, and keep a pointer to it.
-(void)viewDidLoad {
// typical coding stuff goes here
// load nib
UINib *nib = [UINib nibWithNibName:#"ContentTextCell" bundle:nil];
// assign nib to identifier
[self.tableView registerNib:nib forCellReuseIdentifier:#"ContentTextCell"];
// reference cell
NSArray *topLevelObjects = [nib instantiateWithOwner:nil options:nil];
_referenceContentTextCell = [topLevelObjects objectAtIndex:0];
}
Is there any way for me to load the tableview cell without make it's own nib? Using dequeueReusableCellWithIdentifier:forIndexPath: causes the tableView to behave in an incorrect way.
Additional notes
I think a lot of people are under the impression I call the code on cellForRowAtIndex. It's called in viewDidLoad. It was always displayed as such but perhaps it was easy to understand when skimming the question. I also still use dequeueResuableCell as normal in cellForRowAtIndex. Just wanted to make that clear.
The row heights are dynamic. If you made the suggestion that I should make the row height 44 then you may want to read the question more carefully before attempting to answer the question.
The text I'm using is from a json file, it requires the paragraph, font and positioning of a textView to calculate the text height, which impacts the row height. I'd like to pull these pieces of data from the prototype cell rather then hard coding the values, and making sure they match what's in the storyboard.
The code already works as is. It runs fine. I just think it'd be more convenient to be able to pull the prototype cell from the storyboard rather than making a new xib for it.
If I understand correctly, the row height is a function of your model and some attributes of subviews in the cell, for example, a string from your model and the font size in the cell's text view.
I agree that keeping the view attributes of the cell's subviews in code seems redundant with keeping them in the storyboard, but you're also right that keeping a reference cell around is weird. It's weird whether you get the cell from a nib or whether you find a way to get it from the storyboard (which I don't think you can, which is the answer to your stated question).
Dequeueing a cell in heightForRowAtIndexPath makes little more sense and probably causes an infinite recursive loop as I'm sure you've found.
So you probably won't love this answer, but I think best idea is "redundancy with an attitude change". "Redundancy" means that you keep the view attributes in code, like a method that returns the textView's desired font pointSize. "Attitude change", means you don't think of this as redundancy. Instead, your code should be the authority on all of the view attributes pertinent to row height. Think of the prototype cells in the storyboard as just a way of visualizing what the correct coded values should be. I'd even recommend setting the attributes in code using the coded values when you configure the cell, especially if there aren't too many of them.
Finally, if your rowHeight calculation is elaborate, and it sounds like it is, be sure to also implement
- (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath
and do a much quicker calculation therein (like return a constant).
I've just been working through an almost identical problem. You want the height, but the height depends on a bunch of the cell UI setup. It might even be a fairly simple task of checking the padding height above and below an image. Something which could get tweaked in IB several times before you are done so you don't want to duplicate the padding value in code, you want to check them against the current values in IB.
You can call dequeueReusableCellWithIdentifier: from anywhere (assuming you have a reference to the UITableView). So you don't need another way to get the cell. This will not cause recursion or infinite loop problems.
There is a catch though. The cell won't go back to the auto-reuse pool. A UITableView figures out which cells scroll off the screen and puts them in the pool, but this cell never goes onto the screen and so the table view never remove it from the screen and put it back in the pool. To make matters worse, the table still has a strong reference to it; so if you lose your reference, it's not going to get cleaned up, it's essentially leaked memory. You can't use it, the table view can't use it and it will stay in memory until the backing UITableView gets deallocated.
I found a few ways around problem:
Dequeue the cell once, store it someplace safe and use the same one every time you need to test out some cell formatting. You can even call prepareForReuse: to clean it up every time you want to test something new. Holding a single extra cell in memory is unlikely to be a killer for most apps.
Sneak it back into the re-use pool. Wait, what? Yeah, I said you can't do it, but you can, but the thought makes me cringe a little. Next time you get to cellForRowAtIndexPath: don't dequeue a new cell, use the one you've got laying around and return that one instead. This will slip your wayward cell back into the fold and it will eventually make it's way back into the reuse pool.
So you can do this.... should you? Well, that's a different question. Apple has provided some alternatives to try and get around this issue, but in some cases they just don't work effectively.
That's a bad pattern and you will start to get undesired behaviours which you have already seen some of. It might be better to store the height against the cell as a class method.
+ (CGFloat)requiredHeight;
{
return 44.f;
}
and call it as such
self.tableView.rowHeight = [OKACell requiredHeight];
This does mean you need to manage the height in two places but because of the descriptive name and class method it shouldn't be difficult to change when that time comes.

How can I access a specific instance of a header or footer in a UICollectionView?

I can call collectionView:cellForItemAtIndexPath: to get a specific cell in order to modify that cell at any time--for instance, to update its label. I am speaking of the class instance method on UICollectionView, not the UICollectionViewDataSource method.
However, there does not appear to be a similar method for headers/footers. collectionView:viewForSupplementaryElementOfKind will return unique instances of headers/footers, instead of giving an already allocated instance. Because of this, I cannot get a pointer to a header in order to update (say) a label. I am forced to reload the section and modify it in the body of the collectionView:viewForSupplementaryElementOfKind method.
Is there a proper way to get a pointer to a specific header/footer, without resorting to a custom object cache or tags?
As far as I know there isn't (it took until iOS 6 for there to be an equivalent call for a UITableView, footer/headerViewForSection).
If I had to guess why this isn't possible I'd say it's because in some layouts the UICollectionView will automatically re-layout and adjust the supplementary views, so adjusting view contents 'on the fly' could be problematic. However, assuming UICollectionView doesn't do anything funny like copying views you should probably be able to hold your supplementary views in a separate array and modify them that way - that said, no guarantees this won't introduce some odd or strange behaviour (same for your suggestion of using tags and going into the collection view hierarchy).
So the short answer is no - there isn't a method that supports this in the API. If you had a compelling use case it's probably worth filing a feature request through Apple's bug reporter though.
The correct way to do this is to update your data model object in the data source, and then call [myCollectionView reloadData]. This will trigger your collection view to ask for the data again and update its subviews as needed.
Another possible solution is to have the header views listen for an NSNotification that you broadcast, and then the ones that care can update themselves based on whatever criteria you have.
Or give them a tag and use viewWithTag:.

Resources