Swift: UICollectionView invalidateLayout not getting triggered - ios

I have a collection view with a datasource and a layout class. The class is linked to the collection view in the Attributes Inspector.
By tapping buttons I need to retrieve data for the collection and this can mean different numbers of section/items. Therefore I need to restructure the layout each time. However this never seems to occur.
At the moment I have the following in the Success function for the data retrieval request.
listingsView.reloadData()
listingsView.collectionViewLayout.invalidateLayout()
listingsView.collectionViewLayout.prepareLayout()
I'm not sure that the prepareLayout is needed but tried it anyway. The datasource updates fine but the prepareLayout is never triggered by any of the lines.
Do I need some special settings or a different location for the invalidateLayout request?
Thanks.

Sorry - ignore this. Had a Boolean variable on layout that wasn't getting altered due to a forced return.
Just listingsView.collectionViewLayout.invalidateLayout() works fine.

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)

Changing my tableview data source every time the view appears. Where should I do it?

I declare an array at the beginning of my UITableViewController:
class ArchiveTableViewController: UITableViewController {
var dataSource: [[Book]]!
(…)
And then I have a function getDataSource() that updates the array from a database.
My first thought was to call it at viewWillAppear, but it seems that the table view loads before that, so it ends up not matching the array.
I could call the function from every single table view method, but that seems a little stupid. So where is the best place to do it? It must get called every time the view appears, so viewDidLoad won't work, but it must get called before the tableview methods, so viewWillAppear won't work either. It's like I need something in between. Or is there a better way to do what I want?
Thanks in advance,
Daniel
Edit: I should have added, that dataSource array is made up of other arrays, each representing a section in the table view. I get the number of sections and the number of rows in a section from the array too, so it must stay the same throughout the tableview methods or the app will crash, it might try to populate a row that shouldn't exist anymore, for instance.
Edit 4: Ah, ok, I got it! I'm sorry for wasting everyone's time looking for complicated answers. I just had to call getDataSource() from both viewDidLoad and viewWillAppear, and call tableView.reloadData() only in viewWillAppear.
I was trying all combinations of where to put things and with reloadData inside getDataSource() it would sometimes get called repeatedly forever until it crashed… or on other attempts I would call getDataSource() from viewWillAppear and not from viewDidLoad, and then it would crash the first time… anyway, I missed the most obvious combination and now I can't understand how I didn't see it before. Thank you all so much for your time.
Set it in override func loadView().
EDIT: As a general practice this is how you should go it:
Show loading overlay on screen while table data is being fetched.
Once data is available:
2.1. remove the loading overlay.
2.2. update data source model.
2.3. reload your table view so latest model could be picked.
Are you are showing other view controller and then going back to the one with tableView and that's why you want to update the tableView every time the view appears?
If so - it should just work the way you initially tried, if you put the getDataSource in viewWillAppear() and it calls tableView.reloadData() in the end (as you have it here in the source sample) then it will effectively show just what you have prepared for it (all the tableview methods will be called again after reloadData() even if they were already called before).
So I would advise debugging why it is not working when you call it from viewWillAppear(). Please add some screenshots or other data of how it is not correct with this method.
As a sidenote, if getDataSource() takes significant time, it may delay showing your view controller if you put it in viewWillAppear. In such case you'll need to do the way #Abhinav laid out for you (but you can start it from viewWillAppear).
Update: You're right, depending on how you've written numberOfSectionsInTableView() and tableView(_, numberOfRowsInSection) you may need to call getDataSource() also from viewDidLoad() or else you may crash because you're not handling empty datasets.
In that case you don't need tableView.reloadData() because table view methods will be called anyway, so as you noted, it's good to separate it from your getDataSource().
Regarding getDataSource() called repeatedly until crash - it probably happened when you added getDataSource() in one of UITableViewDataSource methods (like numberOfSectionsInTableView()) because reloadData() inside getDataSource() would trigger this method again.
Every function needs to access your var dataSource. So that's where the call to getDataSource must go: You create an optional variable that contains either dataSource or is nil, and then you give dataSource a getter which returns that optional variable unwrapped if not nil, and calls getDataSource otherwise.

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.

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:.

Random UI updating with custom UITableViewController and Prototype cells defined from Storyboard

I have created a subclass of UITableViewController that is used as the custom class for a View in my storyboard. The view has a number of sections/rows defined as prototype cells which are instantiated and shown as expected. Most of the UITableView delegate methods simply call the super's methods that handle section amount, rows, etc.
I have cached content that is used to update the cells on -viewDidLoad and then an asynchronous operation that reloads the content from my server and rebinds the data afterwards (on the main thread, -performSelectorOnMainThread::). It is at this point that, seemingly arbitrarily, some of the cells content (labels primarily) will be cleared of data and not updated to the new data for anywhere between 10-15 seconds. At which point either the content which just show up or sometimes scrolling in the table view will cause the content to appear.
I know the selector is being called and on the main thread, the UI element's text properties are getting set but randomly they just...don't update. I've tried adding in -setNeedsDisplay on the tableView, controller's view and -reloadData on the tableView (though the last one seemed unnecessary as they're prototype cells) all to no avail.
Anyone come across this?
I am not sure if this is caused by the thread aspects, or how the tableView is handled. If it is tableView, check out this post since there are some similarity to disappearing data that my app was experiencing until I handled the dynamic, prototype tvCells correctly.
My comment did indeed fix my problem. It seems UILabel's are not updated when modified but rather it's decided internally when to repaint them - regardless if you try to force it with display or setNeedsDisplay.
Solution: Use a UITextView instead of a UILabel

Resources