Trouble setting properties and performing actions on a tableviewcell outside of cellForRowAtIndexPath: - objective-c

I have a custom tableviewcell that I create a pointer to in the header of my view controller and init inside cellForRowAtIndexPath
if ([[cellOrder objectAtIndex:indexPath.section] isEqualToString:#"balanceCell"]) {
balanceCell = (BalanceCell *) [tableView dequeueReusableCellWithIdentifier:#"balanceCell"];
if (balanceCell == nil) {
NSArray *nib = [[NSBundle mainBundle] loadNibNamed:#"BalanceCell" owner:self options:nil];
balanceCell = (BalanceCell*) [nib objectAtIndex:0];
}
return balanceCell;
}
I only have one instance of balanceCell in the table view, so I assumed that if I wanted to set any properties of the cell, I could just refer to balanceCell
However, this is not working. When the user presses a button, the method below is called (I verified that it is actually being called). However, the methods called on balance cell don't work.
- (void)addBalanceCell {
[cellOrder addObject:#"balanceCell"];
[table reloadData];
balanceCell.leftEquation = equationCell.leftView.equationOrder;
balanceCell.rightEquation = equationCell.rightView.equationOrder;
[balanceCell setUpText]; // not called
}
What is the proper way to reference balanceCell?

Well this isn't the interface builder way but you should have an array that functions as your datasource.
That datasource should contain the objects that define a BalanceCell.
If you can't manage those objects and reload the table then you might want to consider changing it so.
A UITableViewCell should mainly be used as a View, a means to show the data, Model, that is stored somewhere else aka your datasource.
At the moment you do "addBalanceCell" you should make a new instance of the dataObject and manage your data here. After you manage it with the data you want you can just add if to your datasource and update the table, which means you then made a new cell since there is a new entry in the datasource.
The same way you update these cells by accessing those dataObjects in your datasource. You can ask for the object in your datasource and manage it the way you want it to. If you then update the table the cell should update itself with the new data.
In short, your cell should mainly hold the means to show data, not the data itself.

Related

Updating NSTableView datasource asynchronously

I have been googling on this subject, but didn't seem able to find a consensus on the solution to this type of problem. When I use a data source with an NSTableView, if I need to populate the data source in background, there're a couple questions that pop in my mind regarding threading. I'm hoping to get some guidance here.
What would happen if I modified the data source between the main threading calling [NSTableView numberOfRowsInTableView:] and [NStableView tableView:objectValueForTableColumn:row:]? If the object the table view is asking for isn't valid anymore, what should I do?
Is making change to data source only on main thread the solution to this situation?
If 2 is the answer, does it apply to the case when binding is used?
If your data source takes some time to populate, and you're currently showing a table with older data, I think you have a couple of options:
Show a spinner over the UI while the re-population occurs, then call [tableView reloadData]
Keep the older data around so the tableView remains responsive, then once the new data has been fetched/computed, tell the datasource about the new NSArray (or whatever object holds the new data), and call [tableView reloadData].
You can't be changing the data backing your datasource on the fly, unless you inform the tableView of each item/row changing as you go.
To address the threading part, you can use a background thread to populate an NSArray of new data, once complete switch to the main thread, and on that call [dataSource setBackingArray:newStuff]; [tableView reloadData];
Many thanks to Graham Perks in comments to one of answers. This actually an answer worth to be written out explicitly. I want just to add small snippet from my project as illustration:
- (void) populateTable
{
DAL *dal = [[DAL alloc] init]; // MySQL engine
NSMutableArray *tmp = [NSMutableArray new];
NSMutableArray *records = [dal RetrieveRecordswithSql:#"select id, serial, scannerid, scans, offloaded, uploaded from scan_set_v3" withColumnsCount:#(6) andColumnsDelimiter:ScanSetRecordColumnDelimiter];
for (NSString *rec in records) {
ScanSetRecord *newRec = [[ScanSetRecord alloc] initWithScanSet:rec];
if (newRec) {
[tmp addObject:newRec];
}
}
self.dataArray = tmp;
[self.tableView reloadData];
}

ARC weird deallocation behavior in UITableView

I've had this behavior happen to me twice recently and I was wondering what the root cause of the problem is (aka how do I make sure this never happens so I don't have to waste a ton of time fixing it).
When I allocate something inside of a tableview cell that is slated for reuse, once another cell is loaded and the table reloads, sometimes that object is deallocated.
Example:
SubHolder *dataStorage;
- (void) initializeLicenseTable
{
LicenseCell *sampleLicense = [LicenseCell new];
self.licenseData = [[NSMutableArray alloc] initWithObjects:sampleLicense, nil];
nib = [UINib nibWithNibName:#"LicenseCell" bundle:nil];
if (dataStorage == nil)
{
dataStorage = [SubHolder new];
dataStorage.owner = self;
[dataStorage addStorageLocation];
}
} //cellForRowAtIndexPath and stuff
This code doesn't work without the if statement (it causes a dataStorage to become a zombie)
What causes this behavior? It seems like testing if dataStorage is nil and only then allocating it is the opposite of what should fix a zombie problem.
-EDIT-
If this behavior is caused by sharing of the variables, how can I make it so that each time an instance of this object is created it makes its own data storage object? Each table has its own information, which is not shared with other tables.
Since dataStorage is a global variable (visible in the file scope of your class), it will be shared by all the instances of your class.
Now, if a second instance of your class is initialized and you don't check for
if (dataStorage == nil)
then your global object will be overwritten and thus at some point deallocated through ARC. If some other object had stored its value somewhere, it will try to access the old object and you get the zombie access.
EDIT:
if each object needs its own dataStorage, you will simply need to declare
SubHolder *dataStorage;
in your interface declaration, or a property like:
#property (nonatomic, strong) SubHolder *dataStorage;
It looks like you're just creating new cells all the time instead of reusing them.
You should re-use cells like this:
UITableViewCell* cell = [tableView dequeueReusableCellWithIdentifier:#"myCell"];
if(cell == nil)
{
cell = [[UITableViewCell alloc] initWithStyle:aStyle reuseIdentifier:#"myCell"];
}

Displaying array in uitableview

I have an array created like this:
[currentQuizArrayQuestions insertObject:[quizArrayQuestions objectAtIndex:randomIndex] atIndex:0];
How do I access its data to display in the table?
I'm trying this:
NSString *cellValue = [currentQuizArrayQuestions objectAtIndex:indexPath.row];
cell.textLabel.text = cellValue;
I know it's wrong, but how do I get to the data array object stored in the currentQuizArrayQuestions array.
You need to implement the cellForRowAtIndexPath method. See this tutorial on UITableViews: http://adeem.me/blog/2009/05/19/iphone-programming-tutorial-part-1-uitableview-using-nsarray/
That one uses a UITableViewController, but the idea is the same.
Edit:
To use a different array, do the same thing with the cell for row at index path method, changing the array that the cellValues come from according to what you need. Then call [tableView reload]; which will run through cellForRowAtIndexPath with the contents of the new array.

reloadData in NSTableView but keep current selection

I have anNSTableView showing the contents of a directory. I watch for FSEvents, and each time I get an event I reload my table view.
Unfortunately, the current selection then disappears. Is there a way to avoid that?
Well, you can save selection before calling reloadData and restore it after that.
NSInteger row = [self.tableView selectedRow];
[self.tableView reloadData];
[self.tableView selectRowIndexes:[NSIndexSet indexSetWithIndex:row] byExtendingSelection:NO];
This worked for me in some cases. But if you insert some items BEFORE the selected row, you should andjust your row variable by adding count of added items to it.
Swift 4.2
Create an extension and add a method which preserves selection.
extension NSTableView {
func reloadDataKeepingSelection() {
let selectedRowIndexes = self.selectedRowIndexes
self.reloadData()
self.selectRowIndexes(selectedRowIndexes, byExtendingSelection: false)
}
}
Do this in case you use the traditional way of populating table views (not NSArrayController).
It depends on how you populate your NSTableView.
If you have the table view bound to an NSArrayController, which in turn contain the items that your table view is displaying, then the NSArrayController has an option to preserve the selection. You can select it (or not) from within Interface Builder as a property on the NSArrayController. Or you can use the setPreservesSelection method in code.
However, if you completely replace the array of items that the NSArrayController manages each time you get your FSEvents, then maybe the preservation of selection cannot work. Unfortunately the Apple docs on this property of NSArrayController are a bit vague as to when it can and cannot preserve the selection.
If you are not using an NSArrayController, but maybe using a dataSource to populate the table view, then I think you'll have to manage the selection yourself.
In the case of using Data Source, Apple Documentation in the header file on reloadData() is that
The selected rows are not maintained.
To get around, you can use reloadDataForRowIndexes(rowIndexes: NSIndexSet, columnIndexes: NSIndexSet). As mentioned in the same header file
For cells that are visible, appropriate dataSource and delegate methods will be called and the cells will be redrawn.
Thus the data will be reloaded, and the selection is kept as well.
A variant on #silvansky's answer.
This one has no need to keep track of count of items inserted/deleted. And it maintains multiple selection.
The idea is to...
1. create an array of selected objects/nodes from the current selection.
2. refresh the table using reloadData
3. for each object obtained in step 1, find/record it's new index
4. tell the table view/outline view to select the updated index set
- (void)refresh {
// initialize some variables
NSIndexSet *selectedIndexes = [self.outlineView selectedRowIndexes];
NSMutableArray *selectedNodes = [NSMutableArray array];
NSMutableIndexSet *updatedSelectedIndex = [NSMutableIndexSet indexSet];
// 1. enumerate all selected indexes and record the nodes/objects
[selectedIndexes enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *stop) {
[selectedNodes addObject:[self.outlineView itemAtRow:idx]];
}];
// 2. refresh the table which may add new objects/nodes
[self.outlineView reloadData];
// 3. for each node in step 1, find the new indexes
for (id selectedNode in selectedNodes) {
[updatedSelectedIndex addIndex:[self.outlineView rowForItem:selectedNode]];
}
// 4. tell the outline view to select the updated index set
[self.outlineView selectRowIndexes:updatedSelectedIndex byExtendingSelection:NO];
}

Passing array of custom objects to NSCell in NSMatrix programmatically

I have an NSArray of custom NSObjects. Each object has some properties and an image that I would like to display in a grid view. NSMatrix appears to be a good solution to my problem, but I am having issues getting the content of the objects to display.
Couple of things to note.
I am not using core data
I am trying to do this programmatically
I have considered using NSCollectionView but NSMatrix appears to be a better solution in this case
All the cells follow the same display format as each other - i.e. I'm not wanting to pass different cells different types of objects, just a different instance of the object
Assume I have an NSView (matrixContainerView) in a window. The controller file has an IBOutlet to matrixContainerView. In my controller I have the following in my awakeFromNib:
NSMatrix* matrix = [[NSMatrix alloc]
initWithFrame:[matrixContainerView bounds]
mode:NSRadioModeMatrix
cellClass:[MyCustomCell class]
numberOfRows:5
numberOfColumns:5];
[matrix setCellSize:NSMakeSize(116, 96)];
[matrix setNeedsDisplay:YES];
[matrixContainerView addSubview:[matrix autorelease]];
[matrixContainerView setNeedsDisplay:YES];
The class MyCustomCell header looks like the following:
#interface MyCustomCell : NSCell {
MyModel * theObject;
}
-(MyModel *)theObject;
-(void)setTheObject:(MyModel *)newValue;
And the implementation file as follows (drawing simplified):
#implementation MyCustomCell
-(void)drawInteriorWithFrame:(NSRect)theFrame inView:(NSView *)theView {
...drawing code using MyModel e.g. [MyModel isValid] etc...
}
-(MyModel *)theObject {
return theObject;
}
-(void)setTheObject:(MyModel *)newValue {
[theObject autorelease];
theObject = [newValue retain];
}
#end
After some initialization and population of the array containing MyModel objects in the controller, I want to populate the NSMatrix with instances of the objects.
How do I do this?
I have tried adding just two objects from the array as follows (just as a test):
MyCustomCell * cellOne = (MyCustomCell *)[matrix cellAtRow:0 column:0];
[cell setTheObject:[myArrayOfObjects objectAtIndex:0]];
MyCustomCell * cellTwo = (MyCustomCell *)[matrix cellAtRow:0 column:1];
[cellTwo setTheObject:[myArrayOfObjects objectAtIndex:1]];
But this just creates the first object image. If the above had worked, it would been a straightforward task of enumerating through the array and adding the objects.
How do I go about adding the cells and passing the appropriate objects to those cells in order that they can be displayed correctly?
The Apple docs are sparse to say the least on NSMatrix as far as the programming guide goes. The information in there is very useful to me, but only after I have added the objects and got them displaying!
Update
If I do not add the two objects (as per my example above) the output is no different, i.e. a single representation of my custom cell is drawn to screen. This tells me that the single representation I see is being done at the initialization of the matrix and in fact I wasn't drawing anything to column 0 row 0 when in fact I thought I was. Which leaves me now more confused.
Might be that the matrix actually has the two cells but its frame is too small to display them?
After adding the cells try calling [matrix sizeToCells]