Are the cells in view based NSOutlineView getting released on collapse? - objective-c

I've never worked before with NSOutlineView and I'm curious if the cells are getting released and deallocated when the item is collapsing?
I feel like my cells are being stacked on top of each other after every time I expand and collapse the item.
Any kind of help is highly appreciated!
HSObjectViewModel* ovm = item;
HSObjectTableCellView *oCell = [outlineView makeViewWithIdentifier:#"OutlineCell"
owner:self];
oCell.textField.stringValue = ovm.hsObject.name;
NSImage* im = [[NSImage alloc] init];
if(ovm.isVisible) {
im = [[NSImage alloc] initWithContentsOfFile:[[NSBundle mainBundle]
pathForResource:#"on" ofType:#"png"]];
} else {
im = [[NSImage alloc] initWithContentsOfFile:[[NSBundle mainBundle]
pathForResource:#"off" ofType:#"png"]];
}
[oCell.eyeButton setImage:im];
oCell.eyeButton.target = self;
oCell.eyeButton.action = #selector(visibilityButtonClicked:);
[[oCell.eyeButton cell] setHighlightsBy:0];

There are two types of NSOutlineView. One is view-based (the more flexible one to use) and the other is cell-based. Assuming youre using a view-based, the NSTableCellViews in the outline view will not get deallocated when you collapse an item. The cells simply get de-queued i.e. removed from the screen to be used later.
This is done for memory effeciency reasons. The logic is "why allocate lets say 2000+ cellViews if the screen is only able to display 20 at a time?" So the cells will get de-queued (to be used later) and not deallocated generally.
HOWEVER, this behavior is unpredictable. If you set up your code the standard way, then the system will be managing the cells. You cant be 100% sure when a cell will be deallocated. If your users can delete cells from the NSOutlineView, then the chance of cells being deallocated increases.
-Edit-
Based on the comments below, you need to reset the cells after dequeuing them. Assuming your code looks something like this.
- (NSView *)outlineView:(NSOutlineView *)outlineView viewForTableColumn:(NSTableColumn *)tableColumn item:(id)item {
NSTableCellView *aCellView = [outlineView makeViewWithIdentifier:[tableColumn identifier] owner:self];
// You can reset the cell like so
if (item.status == 0) { // Assuming your item has a property called status (you can make what ever property you want)
aCellView.imageView.image = [NSImage imageNamed:#"redImage"];
} else if (item.status == 1) {
aCellView.imageView.image = [NSImage imageNamed:#"blueImage"];
} else {
aCellView.imageView.image = nil;
}
}
So basically, you observe the properties of the item (properties you should declare to distinguish what item is what) and depending on the property, you reset the cell to show the correct values. In the example above, the cell has a image status 0 = a red image, 1 = blue image, anything else = no image. Had i not reset the cells, when ever i collapse, some cells will have old values of other cells since they are being reused.

Related

Adaptive height for UITableViewCell containing an UITableView

I have an UITableView (let's call it table1) which contains static rows. The first row contains another UITableView (table2), populated dynamically from an XML file on the internet, which is completed after the cell is being created, because asynchronous. table2 is created and added to cell's contentView only once the request is done (else an error text is prompted as cell's label).
table2 is made of collapsed sections that are expanded when interacted, which thus do change the contentSize/height of the table2.
What I want to achieve is to make the cell of table1 resize dynamically at 2 key points: 1 when the web request is done, which means when the table is created and added as subview, and 2 when a section of table2 is interacted, which makes the height of table1 change; table1 cell should follow and do exactly the same height variation.
I've searched everywhere and tried tons of things, but nothing ever worked, and I've been stuck for days.
Here's what I've tried:
Overriding (estimated)heightForRowAtIndexPath:: useless as it's only called once and before table2 is added
Changing UITableViewAutomaticDimension and estimatedRowHeight: done by default
Using NSLayoutConstraints, within updateConstraints or after table2 creation
Playing with intrinsicContentSize and invalidateIntrinsicContentSize:
Using reloadRowsAtIndexPaths:withRowAnimation:
...but nothing worked, and I feel I've tried everything
About the constraints, I tried something like:
table2.top = contentView.top = cell.top (same for leading & trailing)
cell.bottom = contentView.bottom = table2.bottom (also with height)
EDIT 1:
My code:
// Cell
- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
if (self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]) {
self.openSection = -1; // reports which section of table2 is open
SPDSpinnerView *spinner = [[SPDSpinnerView alloc] initWithFrame:self.frame]; // view with UIActivityIndicator & text
spinner.label.text = #"Loading...";
[self addSubview:spinner];
self.contentView.translatesAutoresizingMaskIntoConstraints = NO;
[self loadDataWithCompletion:^(BOOL success) {
// Remove spinner
for (UIView *view in [self subviews]) {
if ([view isKindOfClass:[SPDSpinnerView class]]) [view removeFromSuperview];
}
// Handle result
if (success) {
// Add table view when done
self.table2 = [[UITableView alloc] initWithFrame:self.frame style:UITableViewStylePlain];
self.table2.tableFooterView = [[UIView alloc] initWithFrame:CGRectZero];
[self.table2 registerClass:[SPDChangelogHeaderView class] forHeaderFooterViewReuseIdentifier:#"headerView"]; // custom tappable header for collapsible sections
self.table2.scrollEnabled = NO;
self.table2.delegate = self;
self.table2.dataSource = self;
self.table2.translatesAutoresizingMaskIntoConstraints = NO;
[UIView transitionWithView:self.contentView duration:.25 options:UIViewAnimationOptionTransitionCrossDissolve animations:^{
[self.contentView addSubview:self.table2];
// FIXME: fit table2 in cell
} completion:nil];
} else {
self.textLabel.text = #"Error loading data";
}
}];
}
return self;
}
// delegates, touch handler for table2, and data loader
(nothing else related to my problem aside from usual code)
EDIT 2:
"Mockups:"
What it looks like currently
What I want it to be at step 1 (after data loading)
What I want it to be when inner sections are collapsed or expanded
For (2) and (3), red lines are the height I want for both table2 and the cell. In this MRE, table2 overlaps the rest of table1 but I want of course to keep the same spacing between cells of table1, table1 must grow as the cell does.
Thank you!
So, with the kind help of DonMag, I've reconsidered my issue. My idea was to embed a whole table in my cell: instead, they proposed to use subsections. A link is worth thousand words:
github.com/DonMag/SubSectionsDemo
If you want further explanation on how it works, let me explain. Basically, the idea is to create 2 kinds of cells you want to "simulate" your embedded table with: (sub)headers & (sub)cells. They will both be cells belonging to the section of your table1, with 2 different looks.
Then, to recreate the collapsing effect, you have to handle cells selection but only for subheader cells in tableView:didSelectRowAtIndexPath:.
Cell collapsing is done by reloading data and then adding to table1 section, during the reloading, only subcells that are not virtually collapsed, and all the subheaders.
Hope it was clear enough to help you understand the code above on GitHub.
Unfortunately I won't be using this as-is because I do use a special subclass of UITableViewController whose behavior is slightly different. I'll try to adapt as much DonMag's code as possible to my use, but the idea by itself will greatly help me.

Creating NSCollectionView with datasource programmatically

I am trying to create a NSCollectionView programmatically using a NSCollectionViewDataSource.
The code is very simple:
self.collectionView = [[NSCollectionView alloc] init];
// Add collection view to self.view etc.
self.collectionView.dataSource = self;
[self.collectionView registerClass:[NSCollectionViewItem class] forItemWithIdentifier:#"test"]
self.collectionView.collectionViewLayout = gridLayout;
[self.collectionView reloadData]
This leads to the following methods getting called (if I don't set the collectionViewLayout property explicitly these two don't get called either):
- (NSInteger)numberOfSectionsInCollectionView:(NSCollectionView*)collectionView
- (NSInteger)collectionView:(NSCollectionView*)collectionView numberOfItemsInSection:(NSInteger)section
However, collectionView:itemForRepresentedObjectAtIndexPath: is never called. Is there something else that I need to do in order to make sure that the last data source method is called? I have made sure that the two count calls return > 0, so that's not the problem.
So it seems that the problem was actually that I wasn't wrapping the NSCollectionView in a NSScrollView. This probably has to do with the layout being done incorrectly (so the items aren't requested from the data source) if it is not wrapped in a scroll view.
I've been working through different scenario's in the past days, and I dare say that using an NSScrollView, or not, makes practically no difference. With or without scrollView, I've ended up with the same errors and limitations.
What does make a huge difference is the choice between "old school" and the new-fangled collectionView. By "old school" I mean setting the itemPrototype and contents properties, something like this:
NSCollectionView *collectionView = [[NSCollectionView alloc] init];
collectionView.itemPrototype = [TBCollectionViewItem new];
collectionView.content = self.collectionItems;
NSInteger index = 0;
for (NSString *title in _collectionItems) {
NSIndexPath *path = [NSIndexPath indexPathForItem:index inSection:0];
TBCollectionViewItem *item = [collectionView makeItemWithIdentifier:#"Test" forIndexPath:path];
item.representedObject = title;
index++;
}
// Plays well with constraints
New school, something along these lines:
NSCollectionView *collectionView = [[NSCollectionView alloc] init];
collectionView.identifier = TBCollectionViewIdentifier;
[collectionView registerClass:[TBCollectionViewItem class] forItemWithIdentifier:TBCollectionViewItemIdentifier]; //register before makeItemWithIdentifier:forIndexPath: is called.
TBCollectionViewGridLayout *gridLayout = [TBCollectionViewGridLayout collectionViewGridLayout:NSMakeSize(250, 100)]; //getting the contentSize from the scrollView does not help
collectionView.collectionViewLayout = gridLayout;
collectionView.dataSource = self;
Now, you may have noticed the comment that registerClass: must be called before makeItemWithIdentifier:forIndexPath. In practice, that means calling registerClass: before setting .dataSource, whereas in your code you set .dataSource first. The docs state:
Although you can register new items at any time, you must not call the makeItemWithIdentifier:forIndexPath: method until after you register the corresponding item.
I wish I could say that by switching those two lines, all layout problems will be solved. Unfortunately, I've found that the .collectionViewLayout / .dataSource combination is a recipe for (auto)layout disaster. Whether that can be fixed by switching from NSCollectionViewGridLayout to flowLayout, I'm not yet certain.

UITableView: load all cells

Is it possible to load all cells of an UITableView when the view is loaded so that they are not loaded when I'm scrolling?
(I would show a loading screen while doing this)
Please, it's the only way at my project (sorry too complicate to explain why ^^)
EDIT:
Okay let me explain you, what I'm definite doing:
- (UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
NSString *cellIdentifier = [NSString stringWithFormat:#"Identifier %i/%i", indexPath.row, indexPath.section];
CustomTableCell *cell = (CustomTableCell *)[tableView dequeueReusableCellWithIdentifier:cellIdentifier];
NSDictionary *currentReading;
if (cell == nil)
{
cell = [[[CustomTableCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellIdentifier] autorelease];
UILabel *label;
UIView *separator;
if(indexPath.row == 0)
{
// Here I'm creating the title bar of my "table" for each section
}
else
{
int iPr = 1;
do
{
currentReading = [listData objectAtIndex:iPr-1];
iPr++;
} while (![[currentReading valueForKey:#"DeviceNo"] isEqualToString:[devicesArr objectAtIndex:indexPath.section]] ||
[readingresultsArr containsObject:[currentReading valueForKey:#"ReadingResultId"]]);
[readingresultsArr addObject:[currentReading valueForKey:#"ReadingResultId"]];
//
// ...
//
}
}
return cell;
}
My error happens in the do-while-loop:
"listData" is an array with multiple dictionaries in it.
My problem ist that when I’m scrolling my table slowly down, all is fine, but when I’m scrolling quickly to the end of the view and then I’m scrolling to the middle, I get the error that iPr is out of the array’s range. So the problem is, that the last row of the first section has already been added to the "readingresultsArr", but has not been loaded or wants to be loaded again.
That’s the reason why I want to load all cells at once.
You can cause all of the cells to be pre-allocated simply by calling:
[self tableView: self.tableView cellForRowAtIndexPath: indexPath];
for every row in your table. Put the above line in an appropriate for-loop and execute this code in viewDidAppear.
The problem however is that the tableView will not retain all of these cells. It will discard them when they are not needed.
You can get around that problem by adding an NSMutableArray to your UIViewController and then cache all the cells as they are created in cellForRowAtIndexPath. If there are dynamic updates (insertions/deletions) to your table over its lifetime, you will have to update the cache array as well.
put a uitableview on a uiscrollview
for example , you expect the height of the full list uitableview is 1000
then set the uiscrollview contentsize is 320X1000
and set the uitableview height is 1000
then all cell load their content even not visible in screen
In my case it was that I used automaticDimension for cells height and put estimatedRowHeight to small that is why tableview loaded all cells.
Some of the answers here and here suggest using automaticDimension for cells height and put mytable.estimatedRowHeight to a very low value (such as 1).
Starting with iOS 15 this approach seems not to work anymore. Hence, another way to achieve the table to "load" all cells could be by automatically scrolling to the last cell. Depending on the tables height and how many rows it can show some cells are discarded but each cell would be loaded and shown at least once.
mytable.scrollEnabled = YES;
NSIndexPath* indexPath = [NSIndexPath indexPathForRow:cellCount - 1 inSection:0];
[mytable scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionBottom animated:YES];
mytable.scrollEnabled = NO;
If you want to scroll up again just scroll to the top as outlined here.
Following the comment that was made by juancazalla, I found that if you have a tableView that is using automaticDimension, loading all the cells at once can be best achieved by setting estimatedRowHeight to a low value (such as 1).

NSTableView Cell Display Issue

I'm using a view-based NSTableView, and I've ran across a little issue.
I'm trying to switch the text color of my two labels from black to white when highlighted.
To do so, I've written the following code,
- (void)tableViewSelectionDidChange:(NSNotification *)notification
{
NSView * viewInQuestion = [table viewAtColumn:0 row:[table selectedRow] makeIfNecessary:YES];
if ([viewInQuestion isNotEqualTo:lastViewSelected])
{
[(NSTextField*)lastViewSelected.subviews.lastObject setTextColor:NSColor.blackColor];
[(NSTextField*)[lastViewSelected.subviews objectAtIndex:1] setTextColor:NSColor.grayColor];
}
[(NSTextField*)viewInQuestion.subviews.lastObject setTextColor:NSColor.whiteColor];
[(NSTextField*)[viewInQuestion.subviews objectAtIndex:1] setTextColor:NSColor.whiteColor];
lastViewSelected = viewInQuestion;
}
That works great; I get this result:
The issue is that sometimes the text doesn't appear white even though an NSLog told me that the NSTextField's color was NSCalibratedWhite (or whatever it's called).
The color also switches back to black when the textField is not visible (scrolling away from it and then back). Yet again, even when it does this, the NSTextField's color is still logged as white.
Overriding setBackgroundStyle on NSTableViewCell has worked perfectly for me, at least on OS X 10.8. (Given the number of relevant questions here on SO, one can guess that there used to be some problems before.)
The background style is updated on selection events and on window activation/deactivation, just as one would expect.
Here's my custom cell impl — as trivial as it can get:
#implementation RuntimeInstanceCellView
- (void)setBackgroundStyle:(NSBackgroundStyle)backgroundStyle {
[super setBackgroundStyle:backgroundStyle];
self.detailTextField.textColor = (backgroundStyle == NSBackgroundStyleLight ? [NSColor darkGrayColor] : [NSColor colorWithCalibratedWhite:0.85 alpha:1.0]);
// self.detailTextField.textColor = (backgroundStyle == NSBackgroundStyleLight ? [NSColor blackColor] : [NSColor whiteColor]);
}
#end
My method is very hacky, and probably not the optimal solution; But it resolves it so that's good.
Assuming you implemented tableSelectionDidChange the way I have, all you need to do is register an NSNotification and implement a custom method that should be more explicit.
In the init, awake, or didFinishLaunching part of your application...
NSView * contentView = table.enclosingScrollView.contentView;
[contentView setPostsFrameChangedNotifications:YES];
[NSNotificationCenter.defaultCenter addObserver:self selector:#selector(boundsDidChange:) name:NSViewBoundsDidChangeNotification object:contentView];
Somewhere else in the program...
(assuming hasUpdatedCell is a BOOLEAN property)
- (void)boundsDidChange:(NSNotification *)notification
{
/* Bounds can change while nothing is selected--> but we only want to execute the method if a cell is selected. */
if ([table selectedRow] == -1) {return;}
NSRect visibleRect = table.enclosingScrollView.visibleRect;
NSView * viewInQuestion = [table viewAtColumn:0 row:[table selectedRow] makeIfNecessary:YES];
NSPoint selectedViewOrigin = [viewInQuestion convertPoint:viewInQuestion.frame.origin toView:table.enclosingScrollView];
/* If the selected cell is visible, then we can go ahead and redraw the white text as a part of the workaround.
This is because scrolling away from the selected cell and back will make the cell revert back to black. */
BOOL cellVisible = NSPointInRect(selectedViewOrigin, visibleRect);
/* We already know we need to update it, and we will so we don't need to evaluate the next step in the program */
if (!cellVisible && !hasUpdatedCell) {return;}
if (cellVisible && !hasUpdatedCell)
{
/* The cell is visible but we haven't updated. Let's do it then. */
[self tableViewSelectionDidChange:nil];
hasUpdatedCell = YES;
}
else if (!cellVisible)
{
/* The cell is not visible and we need to update next time. */
hasUpdatedCell = NO;
}
}
Things then should get displayed properly.

dequeueReusableCell... for 2 rows?

why do we use dequeueReusableCellWithIdentifier if the cells and section fill exactly the size of the screen, or even less than the height of the screen : let's say we have 2 sections with only 1 row for each?
example :
switch (indexPath.section)
{
case kMonitoringSection:
{
cell = [tableView dequeueReusableCellWithIdentifier:kMonitoringCellIdentifier];
if (cell == nil)
{
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:kMonitoringCellIdentifier] autorelease];
cell.textLabel.text = NSLocalizedString(#"Monitoring", #"");
UISwitch *switchCtl = [[[UISwitch alloc] initWithFrame:CGRectMake(197, 8, 94, 27)] autorelease];
[switchCtl addTarget:self action:#selector(switchAction:) forControlEvents:UIControlEventValueChanged];
switchCtl.backgroundColor = [UIColor clearColor];
[cell.contentView addSubview:switchCtl];
}
break;
}
case kLevelSection:
{
cell = [tableView dequeueReusableCellWithIdentifier:kLevelCellIdentifier];
UILabel *levelLabel = nil;
if (cell == nil)
{
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:kLevelCellIdentifier] autorelease];
cell.selectionStyle = UITableViewCellSelectionStyleNone;
cell.textLabel.text = NSLocalizedString(#"Level", #"");
levelLabel = [[[UILabel alloc] initWithFrame:CGRectMake(171, 11, 120, 21)] autorelease];
levelLabel.tag = kLevelTag;
levelLabel.textAlignment = UITextAlignmentRight;
[cell.contentView addSubview:levelLabel];
levelLabel.backgroundColor = [UIColor clearColor];
}
in the sample code : BatteryStatus
if we had 20 rows, i would understand the point, but i'm not sure here...
Thanks
If you only have a static number of cells, say only two (different) cells in your tableView and no cell is repeated, you should instead instanciate your two cells in your XIB (and design them there with your UISwitch for the first one and your custom UILabel for the other) and point an IBOutlet to it.
Much simpler, less code, and totally makes sense when no reuse of the cells is needed.
Do read Apple's Table View Programming Guide which is a really great resource (like quite every Programming Guide in Apple's doc) and explains all this in details. In particular the part "The Technique for Static Row Content" explains this exact use case.
So of course you can use dequeueReusableCellWithIdentifier in such case, it won't hurt (and probably in the BatteryStatus sample they did it because they didn't wonder if it was really useful or not, just as an habit because they do this all the time when they have more rows), but it is not the best way to do it.
Note that sample codes provided by Apple are not always the solution to follow: they are just one way to do sthg, and especially the sample generally tend to focus on the fonctionnality it wants to test (in this case the Battery Status) and does not worry about anything else (especially performance — except if the sample is about performance of course).
You don't have to call it if you don't want to. It isn't a required method or anything like that. The templates put it in for you and many examples use it because it's just there and if you make the table bigger you don't have to add any methods in.
Don't call it if you think you don't need it.
It's needlessly wasteful to instantiate a new cell from scratch every time tableView:cellForRowAtIndexPath: is called, so I would think you'd want to persist your two cells somewhere.
You could do that in any way you want, really, but any general cell caching solution is going to look a lot like dequeueReusableCellWithIdentifier: and any less general solution may get in your way if requirements change. You're not paying a meaningful performance or readability hit for using dequeueReusableCellWithIdentifier: in your example, so why worry about it?