Pre-creating UICollectionView cells - layout issue - performance

I have a UITableView with 6 rows. Each row contains a single UICollectionView. Each collection view contains a single section with 10-15 cells. One view controller is the datasource and delegate for both the table view and the collection view.
I would like to address some performance issues when scrolling the tableview. Whenever a new section comes into view, there is a small pause while the collection view is created and filled. Since I have a fixed number of cells (< 100) and they are almost static (they are initially loaded from a web API but the data will change only a couple of times a week), I would like to pre-build each of the collection view cells in advance. I would prefer the user waits an extra half-second on launch than encounters jerky scrolling.
To accomplish this, I have updated my collectionView: cellForItemAtIndexPath: to check a dictionary of cells I am maintaining. It looks for a key composited from the collection view index and the indexPath for the cell. If the key exists, the corresponding object is returned. If none is found, the cell is built and also added to the dictionary. This step effectively prevents cells from being un-loaded and recycled, at the expense of using more memory.
But on launch, I still need to run this once for each cell to pre-populate the dictionary. I iterate over each table view cell, find the collection view, and call
[self collectionView:collectionView cellForItemAtIndexPath:indexPath];
This is almost enough. The cells are being created and stored in the dictionary, and when I scroll to a new collection view, I see that they are being pulled from the dictionary and are displayed. But all of the cells, and all of their contents, are shoved up in the top-left corner at {0,0}.
Some logging tells me that at the time the cells are created, the frame of the collection view is {{0, 0}, {0, 0}}. I assume this is why none of my layout is being applied?
How can I resolve this?
(Would also be interested in any general comments on my pre-loading implementation).

I resolved this by calling [cell layoutIfNeeded] on the UITableViewCell (not the collection view). A more thorough explanation is welcomed.

Related

10.11 NSCollectionView - determining cell size dynamically

AppKit Release Notes for OS X v10.11 suggests that collection view items can be resized on a per-item basis:
Item size can be determined globally for all of a CollectionView’s items (by setting an NSCollectionViewFlowLayout’s “itemSize” property), or can be varied from one item to the next (by implementing -collectionView:layout:sizeForItemAtIndexPath: on your CollectionView’s delegate).
In my case, my CollectionViewItem consists of a single label that contains a string of varying length. I'm using the NSCollectionView to display an array of strings, as NSStackViews don't support array bindings, and don't flow to new lines. The array of strings is bound to the NSCollectionView's content via an array controller.
My item's nib file is properly set up, the root view and the label both have Content Hugging and Content Compression Resistance Priorities of 1000, and the edges are aligned via AutoLayout.
Now, the NSCollectionViewLayout's delegate method has this signature:
func collectionView(collectionView: NSCollectionView,
layout collectionViewLayout: NSCollectionViewLayout,
sizeForItemAtIndexPath indexPath: NSIndexPath) -> NSSize
My idea now is to grab the item itself, run a layout pass over it, and then returning the new item size.
let item = collectionView.itemAtIndexPath(indexPath)!
item.view.layout()
return item.view.bounds.size
Problem with this approach is that itemAtIndexPath returns nil. If I return a default size in the nil case, that default size is used for all cells.
How can I set a NSCollectionView to respect my item's AutoLayout constraints and for each cell, use the computed size dynamically?
There's a duplicate of this question that I answered but this is probably the one it should be directed to since it's older.
Juul's comment is correct- the items do not exist. sizeForItemAt being called is the collection asking the delegate for any specific sizing for that data entry with which it will use to help create its eventual view controller, NSCollectionViewItem. So you create a loop when you ask the collection to get you an item in the method that it uses to help get an item.
The problem we have is that we want sizing based on the appearance of that data: the length of a text label with proper formatting, not just, say, the string length. So we hit a chicken and egg problem.
The only solution I've come to, which could be prettier, is the following:
Prep
Subclass NSCollectionViewItem and ensure your collection view has a data source that returns the proper subclassed item.
Use constraints in your XIB, completely.
Your subclass should have a method that loads in the data object to be represented- both for this and of course your data source protocol methods.
At some point prior to the first sizeForItemAt call, or at the beginning of the first one if you hadn't by then, manually create an instance of your NSCollectionViewItem subclass, and use NSNib's instantiate(withOwner:topLevelObjects:) to instantiate its XIB with your subclass as an owner. Store that reference as a sort of "sizing template," so you only need to do it once. Delegate was easiest spot for me.
^Note: my first route was to attempt this through the collection's makeItemWithIdentifier, but it was more brittle as it required the collection to have items at the time of creating the sizing template. It also could not be done during an initial sizeForItemAt (accessing/making items during a reload crashes). And I was worried that because it was made with the collection it may get reused down the line and the methods below don't work or start editing visible items. YMMV.
In sizeForItemAt
Directly get the data object being represented from the datasource. Have your sizing template object represent that data object with the method I mentioned earlier.
Access the sizing template's View.FittingSize, the smallest size an item can be given its constraints/priorities, and return that.
Bam! Hasn't been stress tested or anything but no problems on my end, and its not doing a layout pass or anything, just calling FittingSize. I haven't seen this articulated anywhere online yet, so I wanted to write out the full explanation.
I did this in Xamarin.Mac, so my code won't be 1:1 and I don't want to write garbled swift and mess anything up.
TLDR: manually instantiate a NSCollectionViewItem subclass and its xib that you will store, unowned by the collection. During sizeForItem populate that item you store as a sizing reference, and return the FittingSize of the collection item's view.

NSTableView as a form - nextKeyView issue (or how to force load all rows)

Perhaps using an NSTableView as a form is a terrible idea, but I've got it working pretty well in every respect except that you can't tab from field to field.
I have a table with a label column and a field column. I also have an array that keeps track of each row in the table with its accompanying NSTextField control. The controls are set in the array as they are loaded during tableView(tableView: viewForTableColumn: row:).
After the tableview is drawn, I run through each control in the array and set its nextKeyView to the following control.
This works swell, BUT only for those controls which have been displayed on screen. Because the control isn't added until it's loaded, table rows which are off-screen aren't hooked up.
My current approach (which is awful) is to manually scroll the table several times in order to force everything to load, then set all the nextKeyViews. I haven't got it working very well yet, so I was hoping someone had a better idea. Force-loading all views, if it works, should be an OK solution, but there may be something smarter out there too.
I guess the answer is
table.scrollRowToVisible(<row>)
This will show the row you want to see.

View-based NSTableView renders blank rows after inserting new rows with animation

I have a view-based NSTableView that I'm backing with an `NSMutableArray . Periodically I go out grab some data and want to insert new rows into the table at the top.
When I do this without specifying an animation to insertRowsAtIndexes:withAnimation: it seems to work fine for hours on end. However, if I specify an animation, after about 7 or 8 inserts the table view starts to show blank rows where the inserts occurred. Scrolling down and then back up causes the new rows to render properly.
The code that calls insertRowsAtIndexes:withAnimation is in a block, and not running on the main thread, but my inserts happen inside of dispatch_async on the main queue, so I dont' think it related to multithreading.
Here is some code ... self.contents is my NSMutableArray.
dispatch_async(dispatch_get_main_queue(), ^{
[self.contentsTableView beginUpdates];
if(posts.count) {
for(NSDictionary* dictionary in [posts reverseObjectEnumerator]) {
FNPost* post = [[FNPost alloc] initWithDictionary:dictionary width:self.contentsTableView.frame.size.width];
post.delegate = self;
[self.contents insertObject:post atIndex:0];
[self.contentsTableView insertRowsAtIndexes:[NSIndexSet indexSetWithIndex:0] withAnimation: NSTableViewAnimationSlideLeft];
}
}
[self.contentsTableView endUpdates];
});
One thing I'm confused about is the part of the Apple NSTableView documentation for insertRowsAtIndexes:withAnimation that says:
The numberOfRows in the table view will automatically be decreased by
the count of indexes.
I'm confused by what that statement implies about the relationship between the number of objects in my array (and hence the result of numberOfRowsInTableView:) and the number of rows the table view thinks it has. To my thinking the number of rows in the table view should equal my array count and I want to make sure that my understanding of this is not at the root of the problem but like I said, the table works fine if no animation is specified.
Am I doing something obviously wrong here?
This issue turns out to be caused by having the NSTableView in a layer based view hierarchy. Two superviews up, I had a layer-backed view. After reading some other posts about NSTableView showing other errant behavior when in a layer-backed hierarchy I decided to turn off the layer in IB. Table animations now occur as I would expect at the expense of having to maintain some additional state to control the layer backing state when I want to use CA transitions between my views.

UITableView implementation where avoiding UITableViewCell reuse is reasonable

I know that reusing UITableViewCell objects is key to increase UITableView performance.
However I have to simulate a form with a UITableView with grouped cells, where each group is a form section and each cell is a form input (which eventually would push another UIViewController in the navigation stack to enter actual data).
The number of sections and cells are constant and small (no more than 10 cells), and I am trying to access them without having to think in indexPath values. I would like to create cells as controller's properties, and assign them in cellForRowAtIndexPath method rather than reusing them.
Is this a valid approach in terms of performance? Are there any other scenarios where it's worth it to avoid reusing UITableViewCell instances?
Thank you!
I think in your case(about 10 items) it's perfectly reasonable to NOT re-use table view cells. I've done it myself once when I had to create some registration form - I've just created cells in xib file and returns them from cellForRowAtIndexPath.
It's not gonna be performance hit as there's not any heavy calcuation performed. It might be memory consuption problem if your custom cells use some big images(or other memory consuming items). But that's usually not the case with settings/registration or simple form stuff.
I would go with cell re-use all the time except when you have small tables with custom cells. Personally I do cell re-use even when there's 3-4 rows if it's not custom cell.

how to keep the visible content after nstableview reloaddata?

I have a subclassed nstableview whose data source array may increase, by calling reloadData: I can refresh to reflect the data updating.
But after reloadData:, the tableview will always scroll to the new cell with the same old row number (for example, if the tableview was showing the 2nd cell, after reloadData:, the tableview will scroll to the new 2nd cell, therefore, the visible content of the tableview will change). How can I disable this automatic behavior and keep the visible content unchanged after the updating?
thanks in advance!
Table views don't re-scroll when their content changes. Since you're changing the table content which underlies the currently displayed rows, you need to move the scroll yourself.
I don't have code for this, but I suggest using rowAtPoint: to find the initial position of the table view, identifying or calculating the new row index for that row's content, and then calling:
[tableView reloadData];
[tableView scrollRowToVisible:newIndex];
I got this problem fixed: first, I got the rect size changed amount; then after the reloadData: , scroll the tableview to the changed point (original point + delta parts) immediately with the clipview's scrollToPoint: method. It does it so fast that you cannot realize the operation there.

Resources