Using NSTableView for logging? - objective-c

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.

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.

Using for loop to add to an array, objects aren't being added until for loop is completely done looping

So, I am using the code below to cycle through json arrays if the json array returns positive, I add the json array to another array containing all of total arrays. This can take some time to complete however, because it is going through several different json urls plugging in different dynamic information.
I edited the code below to show more basic information. My problem is that data isnt actually entered into the array until the for loop is finished. I need it to be added as soon as it finds it, not after it runs through all the onlineChannels.count - Any help?
for (int i = 0; i<onlineChannels.count; i++) {
[jsonResults addObject:[parsingJson objectAtIndex:0]];
NSLog(#"%i",jsonResults.count);
[MBProgressHUD hideAllHUDsForView:self.view animated:YES];
[self.tableView reloadData];
} else {
[MBProgressHUD hideAllHUDsForView:self.view animated:YES];
[self.tableView reloadData];
}
}
My problem is that data isnt actually entered into the array until the for loop is finished.
How do you know?
I see that you're calling -reloadData, so perhaps you're expecting the change to be reflected in your table immediately? I believe that your objects are being added to the array immediately, but the change isn't displayed on the table right away. -reloadData doesn't actually redraw the table -- drawing happens as part of the main run loop. You're probably running this code on the main thread, and by doing so you're preventing the run loop from getting any time to redraw the table.
The best way to solve this sort of problem is to move time consuming processes like your loop to background threads. That will leave the main thread free to redraw the table as necessary, and it'll make your app more responsive as well.
Also, making the table reload all its data every time you add a single item even when that item's row isn't being displayed seems quite wasteful. Consider instead calling a method like reloadRowsAtIndexPaths:withRowAnimation: to update only the part of the table that has changed.

How to bind a buttons title to a particular value in an array IOS?

I have six on-screen buttons whose titles need to correspond to six elements in an NSMutableArray, when the value in the array changes, I also need the title to change with it. I am having trouble figuring out how to create that constantly updating line to the button, I'm still quiet new to objective-c development as well as Xcode.
I also need to make sure that when there is no value at that particular index of the array, the button cannot be clicked on
here is an example of one of the buttons
- (IBAction)card1Pressed:(id)sender {
if (self.userHasEnteredFirstNumber) {
if (!self.userHasEnteredSecondNumber) {
self.secondNumber = [sender currentTitle];
}
}
else{
self.firstNumber = [sender currentTitle];
}
}
The end goal is to have the user press two buttons, then chose weather to add, subtract, multiply, or divide them. After they pick one of those four operations, the values that the buttons were assigned to in the array will be removed and replaced with whatever the new number is. So after they do this once there will only be 5 numbers left in the array, then 4, then 3..... and so on.
The numbers will be drawn and added to an NSMutable array titled currentHand
UPDATE: Using UIOutletCollection I linked the buttons to the method like this
the link to the picture is here "sorry about not being able to directly post it but new users must have a reputation of 10 before they can"
link to photo of declaration and implementation with interface-builder of IBOutletCollection
was this correct?
the code for the header file regarding the IBOutletCollection is as follows "please note that this has been connected to the six buttons I want to use it with in interface builder, a picture of it is shown above"
#property (nonatomic,retain) IBOutletCollection (UIButton)NSArray *buttonArray;
the code in the implementation file regarding the IBOutletController is as follows
# synthesize buttonArray = _buttonArray;
You want to use Key-Value observing, check out this from the Apple documentation:
https://developer.apple.com/library/ios/#documentation/Cocoa/Conceptual/KeyValueObserving/KeyValueObserving.html#//apple_ref/doc/uid/10000177i
Okay okay, so essentially you have a 'Button'-Array, whose 'Caption' is served by a NSMutableArray, which can easily be changed around, correct?
Now, this is only how I'd do it, there may be better solutions out there, but here goes:
You need a 'change' interface, which is called EACH time, something happens to your NSMutableArray. For example (since I don't know WHAT exactly is in that array of yours):
- (void) ChangeArray:(int)IDofElement (NSString *)newValue {
[arr replaceObjectAtIndex:IDofElement withObject:newValue]; //Updates/Empties the indexed Element.
if(newValue != nil) {
[buttons[IDOfElement] setText:newValue];
}
buttons[IDOfElement].enabled = (newValue != nil); //Makes Button 'clickable'
}
Like this, each time the value inside that array of yours gets changed, the corresponding caption also gets updated. Of course you would need an array of the buttons for this to work, but I don't think that's the big problem.
Another possible solution: Save a reference to the button inside of the object that's stored in the array and whenever the value is changed, change the button's caption too.
Does this help? Mind you, this is not the 'perfect solution', but something I'd come up with.
Please also note, that this is just from the top of my head and probably faulty at some points. But it should be able to point you in the general direction.

Getting the displayed image out of an NSImageCell

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.

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 ;-)