Adding properties to UIControls without subclassing - objective-c

I have embedded UIButtons in my TableViewCells. In order to track which cell the button belongs to, I would like to add an NSIndexPath property to UIButton. I do not want to subclass a UIButton. Is there a way I can do this with categories?
EDIT: I believe the idea of setting tags will not work if I have multiple sections in the table view. Another approach of accessing the button's superview's superview to determine the cell seems more like a hack. Looking for a cleaner way of doing this.

There are several approaches. If you just really need a single number rather than a full index path, you can use setTag:. It's inflexible, but it's readily available.
The next best solution is associative references. These can hang data onto any object, with proper memory management. I usually create a category that adds a property implemented using objc_setAssociatedObject and objc_getAssociatedObject.

You can find out which cell a clicked button was in without having to associate tags or index paths with the buttons, which can get messy when re-using cells. Tags do work with sectioned tables but you have to mess about with numbers, e.g. 10001 is section 1, row 1, so you've got to convert at one end and convert back at the other, too fragile for my liking.
A nicer method is to use UITableView's indexPathForRowAtPoint: method. When your button's action is activated (assuming the target is in your table view controller:
CGPoint point = [sender convertPoint:sender.center toView:self.tableView];
NSIndexPath *path = [self.tableView indexPathForRowAtPoint:point];
sender here is cast to UIButton.

In order to track which cell the button belongs to
You already know which cell the button belongs to: it's the cell that the button is a subview of.
Start with a reference to the button. (If you are the button, this is self. If you're the button's target for its action, then when the button is tapped and emits an action message, it passes itself along as parameter to the action message, usually called sender.)
Look up the view hierarchy (superview) to find the UIViewTableCell containing the button.
UIView* v = theButton;
while (![v isKindOfClass: [UITableViewCell class]]) v = v.superview;
Now you can do whatever you like with that info. For example, you seem to want an NSIndexPath; then call -[UITableView indexPathForCell:].

Related

Cocoa- Using representedObject for NSButton in NSCollectionView

Setup
I have a NSCollectionView. I have a checkbox in the View Prototype. I've successfully set up bindings so the Card Title and action get populated. (image 1, below)
Goal
I'd like, when I click the checkbox, to run a function that accesses the specific CardModel that the View Prototype is already able to access. I'll then manipulate its data accordingly.
Research
I found this article on SO: Get the representedObject values of NSCollectionViewItem NSButton click, which describes my situation pretty well. The answer, unfortunately, is without specific code. Here's what's suggested:
So, first, set the represented object of your button's cell to the
collection view item that owns the button. (You can do this in the nib
editor.) Then, in your action method, get the button's cell, then the
cell's represented object (which is the item), then the item's
represented object.
Seems simple enough, right?
Attempted Solution(s)
I create a method cardCheckBoxClicked: and connect it to the checkbox.
As per the advice above, I connect the button cell's outlet representedObject to Card Collection View Item. (image 2)
I then attempt to get the Card Collection View Item's representedObject in code.
From MainWindowController.h:
-(IBAction)cardCheckBoxClicked:(id)sender
{
CardModel* cModel = [[sender representedObject] representedObject];
NSLog(#"card title: %#",cModel.title);
}
Error
When I click on the checkbox, I get the following error:
-[NSButton representedObject]: unrecognized selector sent to instance 0x6080001581b0
Question!
So - how do I access the button cell's represented object? Did I misunderstand the advice given above? How can I successfully access the data I need?
Images (reference)
bindings example
represented object connection
This here:
-[NSButton representedObject]:
Is you asking the Class NSButton to run the method representedObject. Make sure you distinguish between a Class an an object or instance of that class.
You need to take the actual button, get its button cell, (at least I think that's what you want), and then call representedObject on the cell. If I am understanding you correctly. I never touch interface builder, so here's completely made up code that lines up with what you are asking for.
someObject = [[theButton cell] representedObject];
In addition to CH Buckingham's answer, you should also consider using bindings. You can bind the checkbox's value binding to the collection view item with a model key path which goes through representedObject to some property on your CardModel. (If desired, the key path can keep going through your model object graph.) That will set that property whenever the button is toggled.

Multi-selection behavior at user selection differs from setSelected:animated:

I have a UITableView that displays contacts from AddressBook. I have the multi-selection editing style enabled. When I present the tableView, it works as intended. The user taps a row and the red checkmark to the left side of the cell fills in. Great.
I save these selections in an array property on the presenting view controller. When I show my table again, I would like for these cells to remain selected (i.e., not blue highlighted, just with the red check mark still filled in). In cellForRowAtIndexPath: I get the object for the cell at that index path, test whether the presenting view controller's array contains that object, and if so [cell setSelected:YES];
It seems to me that this should result in the same selected appearance as when the user touches a cell (just the red check mark). Instead, the red check does fill in but also the cell highlights blue momentarily and the text turns white permanently, creating the impression that the contact's name has disappeared. I'm happy to post code if it's helpful but this seems like maybe just a quirk of UITableView that someone might be familiar with?
EDIT: This image shows the kind of checkmarks that I have (and want). I can't piece together though why when I call [cell setSelected:YES]; it behaves differently than it does in response to a touch. What you're looking at here is after I call [cell setSelected:YES];. And it looks almost how I want but the names have disappeeared. And I don't want the cell to highlight when the list appears.
If I understand the visual effect you're looking for, you should be getting it by using cell.accessoryType = UITableViewCellAccessoryCheckmark; rather than [cell setSelected:YES];.
This is resolved. In think I was making problems for myself by trying to setSelected in cellForRowAtIndexPath. Maybe the table view's reusing of the cells was making it act crazy?
The solution for getting the cells to select correctly between presentations of the multi-selection style table was to store the selected indexes at viewWillDisappear. I gave the presenting view controller a property NSArray *selectedIndexes, and in viewWillDisappear called
[self.presentingViewController setSelectedIndexes:[myTableView indexPathsForSelectedRows]];
Then in viewWillAppear, I put that array into an ivar, iterated through it and called [myTableView selectRowForIndexPath:[selectedIndexes objectAtIndex:i] animated:YES];
That did the trick. Thanks to user523234 for the comment. Helped get me thinking on the right track.

How can I be notified when a NSComboBox selection changes IF the combo box is dynamically added with a new table row?

I have a NSTableView and each row has a NSComboBox.
The table column is bounded to NSArrayControllerA and each NSComboBox is bounded to NSArrayControllerB.
I would like to be notified when the selected value in any NSComboBox changes.
So far, I've tried to add a listener to the NSArray managed by the NSArrayControllerB, but I'm not notified about any change.
I've also tried to add an observer to the NSTableView, with a NSComboBoxSelectionDidChangeNotification but it seems the notifications are not propagated to the parent views. And the combo boxes are dynamically created when a new row is inserted.
thanks
UPDATE: How is the NSComboBox bound ?
OK. I assume you've bound the combo box's selection to the array controller's selection? If so, try observing the array controller's selectionIndex property. (It's KVO-compliant.)
On my iPhone so I can't easily test right now, but that should do the trick.
OK, scratch that, now that I better understand the question.
How about instead you set the selector for the cell to some method in your controller (with an outlet to the enclosing table view), say, -comboBoxClicked: and then implement something like:
- (void)comboBoxClicked:(id)sender
{
NSUInteger changedRow = [[self tableView] selectedRow];
// Do something with changedRow
}
I did a cursory test (just NSLogging changedRow) and it seemed to work for me, at least in a very basic application.

mouseover detection in NSTableView's NSCell?

I am wanting to change the text background color on a tableview's cell when it is hovered upon, similar to how AddressBook "highlights" the label of a contact's element when you mouseover the label names. However I cannot figure out how to accomplish...
detecting a mouseover on a particular NSCell and...
After detecting the cell his hovered upon, highlighting the text in that cell (not highlighting the entire row as if the user selected that row)
As NSCell is not a subclass of NSView this seems to be a very difficult task.
Any example of this or explanation on how this might be done would be greatly appreciated.
Thanks!
I actually got it working using another method. I got it from the example posted here... http://www.cocoadev.com/index.pl?NSTableViewRollover
https://web.archive.org/web/20111013060111/http://cocoadev.com/index.pl?NSTableViewRollover
Instead of using NSCell's tracking mechanism, I am tracking mouseEntered/mouseExited and mouseMoved within my subclassed NSTableView.
When the tableview awakeFromNib method is called, I create a trackingRect from the visible portion of the tableview
I have a BOOL ivar that is set to YES when the mouse is within the tracking area(mouseEntered) and NO when it is not (mouseExited)
Within the mouseMoved method, I determine the current row the mouse cursor is on and set it to an NSInteger ivar and then call the tableview's setNeedsDisplayInRect: passing the rect of the row that the mouse is on.
I also override resetCursorRects to remove the old tracking rect and add a new one...this method is called when the tableview is scrolled upon so that it's tracking the latest visible rect.
Finally in my tableview's delegate, I determine the selected row (by retrieving the row index from the NSInteger ivar of the table view and change the cell's text color (or anything you want) if the currently drawn cell matches the row the mouse cursor is on. All this is done in the delegate method: tableView:(NSTableView *)aTableView willDisplayCell:(id)aCell forTableColumn:(NSTableColumn *)aTableColumn row:(NSInteger)rowIndex
I hope this helps others, as this was a bit tricky. It is also probably important to make sure that tableview is the firstResponder when the view loads, just makes things a bit more streamlined and cleaner.
Btw, is there a way to make a specific control in a view always be the firstResponder with nothing else possible as being the firstResponder? Even a method such as the iPhones... viewWillAppear method will help as I could set the first responder each time the view is visible...but i'm not aware of such a method on the Mac.
Overall, it's not a simple task as you noticed.
To track the mouse in an NSCell, subclass NSCell and override
-[NSCell startTrackingAt:inView:]
and
-[NSCell stopTracking:at:inView:mouseIsUp:]
Once you've detected the mouse is tracking inside a cell, you can find out which cell you are in the table with [tableView rowAtPoint:point] and [tableView columnAtPoint:point], and then find your frame with [tableView frameOfCellAtColumn:column row:row]
Then, you can change the way your cell is drawn by changing some property of the cell or changing the way it's drawn directly by overriding drawInteriorWithFrame:inView:.
Here's documentation on subclassing NSCell:
http://developer.apple.com/mac/library/documentation/cocoa/conceptual/ControlCell/Tasks/SubclassingNSCell.html
I achieved something similar by making use of addGlobalMonitorForEventsMatchingMask: handler: of NSEvent within my NSTableView subclass for the NSMouseMovedMask. using this along with columnAtPoint and rowAtPoint of NSTableView I was able to figure out if the position of the mouse was within a given cell.
Using this information I was able to bring up a PopOver when the mouse was over a particular cell.

NSTextFieldCell Coordinates

I'm trying to get the NSPoint cooridnates of an NSTextFieldCell, but NSTextFieldCell doesn't inherit from NSView, and therefore doesn't have the frame method. Any ideas on how to get around this?
I'm trying to use Matt Gemmel's MAAttachedWindow to attach little helper popups to certain elements. I want to attach it to an area on the window, and other than hacking it together by manually messing with the x and y coordinates, I'm not sure how to get the coordinates.
You can call [theCell controlView] to get a reference to the control that owns a particular cell. If the NSTextFieldCell object is part of a simple control, such as an NSTextField, the following should be sufficient:
NSRect cellFrame = [[theCell controlView] frame];
NSPoint origin = cellFrame.origin;
//..
If, however, the NSTextFieldCell is part of a more complex control, such as an NSTableView, where a single cell is used in multiple places, you will need more information in order to determine the proper rectangle. NSCell offers the method representedObject, which can help you to determine which object in the NSTableView is represented by the cell at that particular moment. Without knowing more about your specific case, I don't know how much more detail to provide in that regard.
Here is one possible solution, assuming you are able to discern the row and column information from the object stored in representedObject:
NSTableView * tableView = [theCell controlView];
id cellObject = [theCell representedObject];
NSInteger row = //... determine from representedObject
NSInteger col = //... determine from representedObject
NSRect cellFrame = [tableView frameOfCellAtColumn:col row:row];
A cell is a reusable object that's owned by an NSControl (an NSView subclass). It doesn't have an origin point because it doesn't actually represent a place on the screen, it's up to the control (which has a specific frame rectangle) to draw it where and when it's needed. Think about a table view for example, it might use a single cell that's re-drawn in several places for each row.
The cell is passed a frame rectangle by the control when it's drawn onto the screen, in most cases that should be all you need. You can also take advantage of NSCell's sizing methods, which can tell you how much room it needs to display its content.
For using MAAttachedWindow, could you instead use the control's frame (if it's a single-cell control), or in the case of a table view with a specific row one of the NSTableView layout methods? rectOfRow: or rectOfColumn:, for example. You could override NSTextFieldCell and position the window in one of the drawing methods, but I'd save that for my absolute last choice if possible.
It doesn't have co-ordinates. There are none to get. The way cells work is that the view tells the cell “draw here”.
So, if you're not in one of the cell's drawing methods and you really, really need co-ordinates, you need to ask the view that owns the cell. Usually, this is [cell controlView].
(Perhaps you're thinking of UIKit? UITableViewCells are very different from NSCells.)