Getting the displayed image out of an NSImageCell - objective-c

I have an NSImageCell table column whose valuePath is bound to a path supplied by my object through an NSArrayController.
My NSTableViewDelegate implements the -tableView:heightOfRow: method, in order to have variable row height. I need to calculate row height based on the dimensions of the image displayed in the aforementioned column.
Right now, I'm getting horrible performance, though, since I'm calling [[NSImage alloc] initWithContentsOfFile:<path>] for each iteration. Is there any way to load the image representation that each NSImageCell has already retrieved for display?
I'm happy with the performance exhibited by using the valuePath binding, and would rather not cache each image on my own, as there will be many of them, each somewhat large.
I tried the NSTableColumn method -dataCellForRow:, which sounded perfect, except the cell returned returns an objectValue that seems to be the last row for which data was loaded.
Update (solution, unsatisfactory)
I figured out an approximate solution (posted simultaneously below), but it seems clumsy (and I've already seen it fail at random, irreproducible times), and I am still looking for a better solution.
I'm using the -tableView:willDisplayCell:forTableColumn:row: delegate method to populate a mutable dictionary with [[cell objectValue] size] (the image's size) keyed to the represented object's unique ID. Then, in the -tableView:heightOfRow: call I'm looking up the cover from this dictionary. I need to call [tableView noteNumberOfRowsChanged] after data is loaded, otherwise the dictionary isn't filled properly for the initial screen of data.

I tried the NSTableColumn method -dataCellForRow:, which sounded perfect, except the cell returned returns an objectValue that seems to be the last row for which data was loaded.
That's because the NSTableColumn only uses a single data cell for displaying the entire column, swapping out its value as it draws.
If I were you, I would probably try implementing the – tableView:willDisplayCell:forTableColumn:row: method in your NSTableViewDelegate. Then you can intercept the cell before it's about to be drawn, get its value at that moment, and cache just its height (as an NSNumber* or CGFloat). Then you can return that value in -tableView:heightOfRow:.
Then again, it's possible that -tableView:heightOfRow: gets called before – tableView:willDisplayCell:forTableColumn:row:, so that might not work. I'm not sure. But that's where I would start.

Related

How can I reload items without removing and inserting with UITableViewDiffableDataSource?

I'm implementing a search screen in my app using UITableViewDiffableDataSource. Each cell represents a search hit and highlights the search match in the cell title, kind of like Xcode's Open Quickly window highlights portions of its result items. As text is typed into the search field, I update the results list. Results move up and down in the list as their relevance changes.
The trick is that I need to force every cell to re-render every time the search text changes, because a new search string means an update to the highlighted portions of the cell title. But I don't want to animate a deletion and insert, because it's still the same item. How can I tell the data source using the snapshot that it needs to reload cells?
I declare the data source like this:
#property (retain) UITableViewDiffableDataSource<NSString *, SearchHit *> *dataSource;
SearchHit represents one search result; it has properties for a display title and an array of ranges to highlight in the title. And it overrides hash and isEqual: so that every result row is uniquely identified.
My code looks something like this:
-(void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText
{
NSArray<SearchHit *> *hits = [self fetchHits:searchText];
NSDiffableDataSourceSnapshot<NSString *, SearchHit *> *snap = [[[NSDiffableDataSourceSnapshot alloc] init] autorelease];
[snap appendSectionsWithIdentifiers:#[#""]];
[snap appendItemsWithIdentifiers:hits];
[snap reloadItemsWithIdentifiers:hits];
[self.dataSource applySnapshot:snap animatingDifferences:YES];
}
At first I didn't have the reloadItemsWithIdentifiers call there, and then no cell would change at all once it was in the result list. Adding the reload call helped, but now most of the cells are constantly one update behind. This smells like a logic error somewhere in my code, but I've verified that the hits passed to the snapshot are correct and the hits passed to the data source's cell creation callback are not.
This article by Donny Wals and this related Twitter thread involving Steve Breen suggests that the way to fix this is to make the item identifier type only represent the properties needed to display the cell. So I updated SearchHit's hash and equality comparison to include the highlighted portions of the title, which they didn't before. Then I got delete and insert animations for all the cells on every update, which I don't want.
This seems like what reloadItemsWithIdentifiers should do...right?
Sample project here on GitHub.
The diffable datasource API may not be the right tool to effect animations on cells themselves. It’s geared towards the animation of the appearance, disappearance and ordering of cells. If your data source has a change that is expressed via Hashable conformance the api will see it as a change and delete/insert etc.
My advice would be to remove the search text from the item identifier and have each cell observe the search text and effect an animation or redraw independently from the datasource.
The proper solution to this is actually in the names of the APIs - the objects you give to the data source should be identifiers, like rowid values from a database. In my case, when the item identifiers don't represent rows in a database that I can look up, I just need to keep the state of the objects in some sort of lookup structure, so that when I call reloadItemsWithIdentifiers, I get the state for each cell from that structure, not from the object that the data source hands to me.

Creating an array item with a property that's dependent on an aggregate function on the array itself

Suppose I have an NSArrayController containing a number of 'blob' objects. These objects are displayed in a view-based NSTableView via bindings.
Now suppose that each 'blob' object contains a property called amount. For one of the NSViews in each row of the table I'd like to display amount / max_amount_in_array.
In other words, I somehow need to bind my cell to the NSArrayController's arrangedObjects.#max.amount and to the NSTableViewCell's objectValue.amount at the same time and perform my calculation.
Is there a way to handle this nicely using bindings?
Currently the only idea I have to is to have a ratio property in 'blob' and recalculate it myself every time that an object is added to the array. That's quite possible, but it just seems like there should be a more bindings-like way to solve the problem.
I've now done this like so:
[libraryCell.myView bind:#"amount"
toObject:libraryCell
withKeyPath:#"objectValue.amount"
options:nil];
[libraryCell.myView bind:#"max"
toObject:_librariesController
withKeyPath:#"arrangedObjects.#max.amount"
options:nil];
There are thus two properties in myView (amount and max) and when either of them change I do the calculation and update the display accordingly.

Using NSTableView for logging?

I've been trying to work out how to use table views and I'm a little stuck if I'm honest. I wanted to use a tableview with a limited number of rows (say 50 max). It starts of empty, with 0 rows. Then I wanted to do something along the lines of:
[self logMessage:#"Waiting for response"];
Which inserts a new row at the bottom with the above text. If I do another call to this pseudo function:
[self logMessage:#"Server response received"];
It should insert yet another new row below the previous row, and ensure it is visible. Once the above limit of 50 is reached, and a new message is inserted, I wanted the oldest message to be removed. All of this would be scrollable, with the latest being visible by default.
Am I looking at the right thing to do this? Eventually, I was hoping to have this in a nice little drawer below the main window, which I can then toggle from the main menu if needed. But as I said, I can't work out how to use a table view properly, it doesn't seem to be as straight forward as other objects are.
Any example code would be greatly appreciated!
Since log viewer is a read-only application of a UITableView, the way you do it is rather straightforward once you understand the basics. Recall that table views rely on their data models to provide them with the correct information that needs to be displayed.
A data model for "the last fifty lines of log" could be as simple as an NSMutableArray: use insertObject:atIndex: to add lines, and removeLastObject to remove the "overflow" lines, like this:
NSMutableArray *logLines = [NSMutableArray array]; // <<== this goes into the init method
-(void) addLogLine:(NSString*)line {
[logLines insertObject:line atIndex:0];
while (logLines.count > 50) {
[logLines removeLastObject];
}
}
Now you can use logLines as your table's "model": the data provider can tell how many lines there are by looking at logLines.count; the content of each row in the table will be the object at the corresponding index in logLines, and so on. Take a look at the UITableView section of your favorite iOs tutorial for the "boilerplate code" that needs to be written in order to display array elements in a UITableView.

Inserting image into NSTableView using bindings

I've an NSTableView bound to an NSArrayController with two columns. One column is bound to the arranged objects of the array controller and displays a string.
I'd like to display an image in the other column, but I just can't make it work. I've dragged an NSImageCell to the column and set the image by hand but it won't show up at runtime. I've double checked and the image is in my resources directory.
Am I missing something? What else should I do to make that image appear?
So you want to have the same image appear for each row? Is that why you're setting it "by hand"? For that, you can mix NSTableViewDataSource methods with bindings. The idea is your string column will be bound as usual, but your image column isn't bound. It has its identifier set (like "imageColumn"). You then use numberOfRowsInTableView: and tableView:objectValueForTableColumn:row: to provide the array controller's object count (so it has the right number of rows) and simply always return your static image when it asks for the value for the right column (checking the id for your "imageColumn"), returning nil otherwise.
If the image is not static (that is, you want to use it as an indicator of some kind) you can use the above method (return some image based some value) OR bindings. To use Bindings, you might add a property to whatever class your array controller is holding, like "status" (a number). You'll then use a custom NSValueTransformer that transforms the status number into a corresponding image. Your column will be bound to the array controller's arrangedObjects.status, using the value transformer (see NSValueTransformer for instructions for use - you have to register it, then use its name). The result is an image in your column that corresponds to a certain status.

How do I set Cocoa NSLevelIndicatorCell values?

I have a tableview. One of the columns in the tableview uses an NSLevelIndicatorCell.
I want to be able to allow the user to edit the warn and critical values for the level indicator such that when they enter a value into a a "warning level" textbox, it changes the warn value of the level indicators being displayed in ALL of the tableview's rows.
I am very much a newbie with Objective-C so all I can figure out so far is that I must need a delegate method to watch the textbox BUT if I succeed in doing that, how on earth do I send the new value to the particular tableview column so that the update happens to ALL of the rows (i.e. how do I send what message to the tableview and target a cell within a column within a tableview)?
Here is the code to the solution I came up with should anyone need it.
- (IBAction)setWarningLevel:(id)sender {
double v;
NSScanner *ns = [NSScanner scannerWithString:[warnLevel stringValue]];
[ns scanDouble:&v];
[levelIndicator setWarningValue:v];
}
This is a textbook case for using Cocoa bindings. Just bind the value of the text field to the NSLevelIndicatorCell in your table view (do that in Interface Builder). The updates should happen automagically.
I think it should apply for all the cells in the table view if you apply the binding to the cell in IB. However if it doesn't, you will need to write a couple lines of code that set up the binding every time a new row in the table is created. That link above will explain everything in detail, but basically you will be setting up a Key-Value Observer relationship in code between the text field and the instance of the level indicator in the row being created.
I think you may have overdone it.
NSTextField subclasses NSControl, so you need to look in the docs for NSControl for a useful function.
Try re-writing it like this; assuming you're taking the value from a warnLevel textfield.
- (IBAction)setWarningLevel:(id)sender {
double v = [warnLevel doubleValue];
[levelIndicator setWarningValue:v];
}
Although this is usually shortened to this;
- (IBAction)setWarningLevel:(id)sender {
[levelIndicator setWarningValue:[warnLevel doubleValue]];
}
You should probably have some validation that the textfield has a valid number. If you're only choosing a couple of numbers have a look at using a stepper control.
Usually, with Cocoa, if you feel like you're jumping through too many hoops, there is sometimes an easier way.
Usually ;-)