Binding with the selection of an NSArrayController in another nib - objective-c

I have two nibs:
Store.nib
Product.nib
Product.nib's File owner is a subclass of NSViewController which has a property product to which various controls are bound:
#property(nonatomic, retain) SRProduct *product;
Store.nib has an NSArrayController object which has been bound to a property of SRApplicationController, which is this property:
#property(nonatomic, retain) NSArray *products;
SRApplicationController has an outlet to that NSArrayController object.
In the -[SRApplicationController init] method I init an SRProductController object with the Product.nib nib. In -[SRApplicationController awakeFromNib] I add the view of the product controller to a view in Store.nib, and I bind the productsArrayController property (the outlet) of the SRApplicationController object to the product of the product controller:
- (id)init {
if (self = [super init]) {
self.productController = [[SRProductController alloc] initWithNibName:#"Product" bundle:nil];
}
return self;
}
- (void)awakeFromNib {
[self.productView removeAllSubviews]; // this method is from a category
[self.productView addSubview:self.productController.view];
[self.productController.view setFrame:self.productView.bounds];
[self.productsArrayController bind:#"selectedObjects" toObject:self.productController withKeyPath:#"product" options:nil];
}
When I run the app, I get no errors, no warnings, the console remains empty, the table view with all products in Store.nib shows all products and I can select them. The problem is that all fields in Product.nib are empty, but they are bound to the product property of the file owner. Can anyone help me with this problem? Thanks in advance. :)

Somewhere there is some sample code that shows how to do this, I can't remember if is Apple code or from somewhere else. Basically what you need to do is have an array controller in each nib file. The array controller in the list style nib should be bound normally and it's array controller should be an accesible property. In the second nib file you need to bind the array controller's content as normal. You also need to make sure that the file's owner of this detail nib has a connection to the file's owner of the list nib. You then bind the sort descriptor for the detail array controller to listController.arrayController.sortDescriptors (it might actually be sortDescriptor can't remember off the top of my head). You also bind the selection index in the same manner. This will allow the array controller in the detail nib to keep up with what is going on in the list nib, after that you just bind each detail element as normal (i.e. the product name text field would have it's value bound arrayController.selection.productName.
If you forget to bind the sort descriptor of the detail nib's array controller to it's counterpart in the list nib the detail nib will update each time the selection changes in the list, but it might not change to the proper product (the binding just passes the selectionIndex not what object is selected).

When allocating the view controller for the Product.nib you should bind its "product" property to your array controller's selection, it can only be done in code, but that will avoid the need for multiple instances of an array controller, and avoid the need to bind them together so they look the same.
Also, I suggest not to bind the array controller's content to your own NSArray, if you do not bind that property the array controller will allocate and manage its own array. You'll be able to add/remove objects from it directly instead of having to rely on your own property to carefully notify the NSArrayController that a change occurred.
The "content" binding is there to allow to bind an array controller's arrangedObjects to the content of another controller to be able to filter and sort the content differently.

Related

Unable to get selected item of collection view

I'm having a problem trying to get the selection of my Collection View using Xcode 6 and trying to build an OSX app.
I followed the directions at http://developer.apple.com/mac/library/DOCUMENTATION/Cocoa/Conceptual/CollectionViews/Introduction/Introduction.html to create the collection view and it's working as intended but I am unable to get the selection of the item I click. I made sure that the view is selectable in IB.
I implemented the notification method mentioned in Selection Highlight in NSCollectionView and I can see when I populate the collection view that the event fires but no matter where I click in the collection view item the notification won't fire again.
As is what I think normal for collection views, I'm simply trying to get the array index of the item selected so I can show detail.
I have poured through articles on the net trying to find the solution but the vast majority of solutions are for IOS using segue's and not for OSX. The exception being the link I posted for Stack Overflow.
I've even gone as far as putting a transparent button covering my entire collection view item so I can grab a click event (which works but I still don't know which item was clicked).
My my question is: how do I get the array item of something I clicked in a collection view?
One way of hearing about changes to the array controllers selectionIndexes is to bind this property to an NSIndexSet instance in your code somewhere, and then use the KVO design pattern to request a notification when this NSIndexSet is altered. If you set this up correctly, when the user clicks on an unselected cell in your NSCollectionView the NSIndexSet that the array controller is using to store its selection indexes is updated. Since this index set is the one you're observing, you'll get a notification telling you about the change.
In the demo-app I created to answer this question, I placed the index set in question on the AppDelegate - here's the implementation file:
// Interface ////////////////////////////////////////////////////////
#interface AppDelegate ()
// This is the content array from which the NSArrayController will derive
// it's arrangedObjects array
#property (nonatomic, strong) NSArray *collectionViewContent;
// This is the NSIndexSet that I want the NSArrayController to use
// to store its selectionIndexes.
#property (nonatomic) NSIndexSet *mySelectionIndexes;
#end
// Implementation //////////////////////////////////////////////////
#implementation AppDelegate
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
// The content that will be shown in the NSCollectionView (each word
// represents a single collection view item).
self.collectionViewContent = #[#"The", #"rain", #"in", #"Spain", #"falls", #"..."];
// Tell cocoa you want to know when the array controller makes changes to
// the index set it's using to stores its selection indexes
[self addObserver:self
forKeyPath:#"mySelectionIndexes"
options:0
context:nil];
}
-(void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context {
NSLog(#"Collection view selection just changed to: %#", self.mySelectionIndexes);
}
//////////////////////////////////////////////////////////////////
In the Bindings Inspector for the NSArrayController you can now tell the array controller to use the AppDelegate property mySelectionIndexes to store its selection indexes:
If you're still experiencing problems, it's possible that you've gone wrong with the bindings somewhere else - here are all the bindings I used:
NSArrayController
The array controller will get it's content from an array managed by the AppDelegate object. It will store it's selection indexes in an NSIndexSet, also managed by the AppDelegate
Content Array: App Delegate.collectionViewContent
Selection Indexes: App Delegate.mySelectionIndexes
NSCollectionView
The collection view will get it's model data from the array controller. It will mark as selected those views that appear at the indexes stored in the array controller's selectionIndexes property:
Content: Array Controller.arrangedObjects
Selection Indexes: Array Controller.selectionIndexes
Onto the view that was automatically generated when I dragged the NSCollectionView onto the canvas, I've added one NSTextField, this text field has just one binding:
Value: Collection View Item.representedObject (In other words, type representedObject into the Model Key Path field.
A Final Word:
It's worth pointing out that you don't have to set up this binding. To get word when one of the items in your collection view is selected or unselected by the user, create a subclass of NSCollectionViewItem and override the selected setter. This property is called automatically each time an item is selected or unselected. In your implementation, you can now make adjustments to the item's view to take account of the fact that it's status has changed. In my demo-app, my custom subclass of NSCollectionViewItem was called PPCollectionViewItem:
#implementation PPCollectionViewItem
-(void)setSelected:(BOOL)selected {
// Call super...
[super setSelected:selected];
// ...now change the view associated with this item. Remember,
// the view is one of the cells in the NSCollectionView - I
// changed it from a standard NSView, to a subclass called
// CollectionViewCell which keeps a flag indicating whether
// or not it's selected (the drawRect routine is varied
// according to this flag's value).
[(CollectionViewCell *)self.view setDrawAsSelected:selected];
}
#end

Change the custom class in a storyboard using code when instantiating

I have a tab bar controller and a bunch of the same tabs. Each tab only differs in functionality, but the UI's are all the same. In the storyboard I designed the flow and UI of one tab and set it base class. Then when I create the tabs I tried typecasting them before adding them to the tab bar but it didn't work.
In the storyboard the View Controller indentified "TabView" has the custom class "TabColor"
TabRed *red = (TabRed *)[storyboard instantiateViewControllerWithIdentifier:#"TabView"];
TabBlue *blue = (TabBlue *)[storyboard instantiateViewControllerWithIdentifier:#"TabView"];
However the loadView method in TabColor gets called, not the TabRed/TabBlue.
Also if I nslog it the result is a TabColor object:
NSLog(#"%#", red)
Expected: TabRed
Actual: TabColor
tl;dr:
Storyboards and xibs contain collections of serialized objects. Specifying a class in a storyboard means you will get an instance of that class when you load the storyboard. A way to get the behavior you're looking for would be to use the delegation pattern common in cocoa/cocoa-touch.
Long Version
Storyboards, and similarly xib/nib files, are actually sets of encoded objects when you get down to it. When you specify a certain view is a UICustomColorViewController in the storyboard, that object is represented as a serialized copy of that an instance of that class. When the storyboard is then loaded and instantiateViewControllerWithIdentifier: gets called, an instance of the class specified in the storyboard will be created and returned to you. At this point you're stuck with the object you were given, but you're not out of luck.
Since it looks like you're wanting to do different things you could architect your view controller such that that functionality is handled by a different class using delegation.
Create a protocol to specify the functionality you'd like to be different between the two view controllers.
#protocol ThingDoerProtocol <NSObject>
-(void) doThing;
#end
Add a delegate property to your viewcontroller:
#interface TabColor
...
#property (strong, nonatomic) thingDoerDelegate;
And then have your new objects implement the protocol and do the thing you want them to.
#implementation RedTabDoer
-(void) doThing {
NSLog(#"RedTab");
}
#end
#implementation BlueTabDoer
-(void) doThing {
NSLog(#"BlueTab");
}
#end
Then create and hook up those objects when you load the storyboard.
TabColor *red = [storyboard instantiateViewControllerWithIdentifier:#"TabView"];
red.thingDoerDelegate = [[RedTabDoer new] autorelease];
TabColor *blue = [storyboard instantiateViewControllerWithIdentifier:#"TabView"];
blue.thingDoerDelegate = [[BlueTabDoer new] autorelease];
This should then allow you to customize the functionality of the view controller by changing the type of object that is assigned to the controllers delegate slot.
TabRed *red = (TabRed *)[storyboard instantiateViewControllerWithIdentifier:#"TabView"];
TabBlue *blue = (TabBlue *)[storyboard instantiateViewControllerWithIdentifier:#"TabView"];
Casting doesn't change values, it only changes the way the compiler interprets those values (and stops it from complaining when you use type in place of another). So casting a TabColor* to a TabRed* tells the compiler to pretend that your first pointer points to a TabRed instance, but it doesn't transmogrify the object that the pointer refers to into an instance of TabRed.
As waltflanagan explains, storyboards and .xib files contain actual objects, and the type of each object is determined when you create the file; you can't change it at run time. What you can do, though, is to have each of your several view controllers load the same view hierarchy. You don't even have to write any code to do this. Just create a .xib file containing your tab controller and the view controllers for each tab:
Be sure to set the type for each view controller appropriately in the .xib so that the right kind of view controller will be created for each tab:
Set the "NIB Name" field for each view controller to specify a .xib file that contains the view hierarchy that these controllers will use. If you specify the same .xib file for each controller, each controller will instantiate its own copies of those views:
Specify any IBOutlets in the common superclass of your view controllers so that all your view controllers have the same outlets. You can specify that superclass as the type of "File's Owner" in the common .xib file so that IB knows what outlets are available. File's owner is really a proxy for the object that's loading the .xib, so when one of your view controllers (TabRed for example) loads the common view .xib, that controller will be the one that the views in the .xib are connected to. When TabBlue loads the .xib, that object will be the one that those views are connected to.
This might seem confusing at first, but play with it. Understanding this will really help you understand .xib files (and therefore storyboards). They're a lot less magical than they seem when you're a beginner, but once you get it they'll seem even cooler.

Object mixup between programmatically created view and interface builder placeholder

I have a view controller which contains a scroll view. Inside the scroll view there is another UI core graphics view. In the view controller I create a temp object for the core graphics view and assign some data to it, then assign it to the attribute of the view controller. Eg: In the view controller:
#interface controller : UIViewController {
GraphView *graph;
}
#property ... IBOutlet GraphView *graph;
#implementation
GraphView *temp = [[GraphView alloc] init];
temp.someArray = anExistingDataArray;
self.graph = temp;
In IB, I open the view controller nib and add a scroll view, and embed a view and assign it the core graphics view class. Then hook up the IBOutlet from that view to the attribute in the view controller.
My problem is that the view controller creates the temp view object, assigns it to itself, with the correct data, however IB seems to instantiate its own object (different memory ID) and displays that, instead of the one in the view controller.
What is the correct way to build this type of setup?
If you drag an object into a nib, IB creates and archives that object. This is why you don't have to alloc/init views that you create in IB.
I'm guessing that you are creating your view in IB so that you can get the geometry correct, or...? It's rather unusual to create a view and then immediately replace it at run-time. More common, for geometric purposes, is to create a container view in IB and then add your programmatically-created views as subviews of that.
Your code left out the most important piece of this, though, which is when it's getting run. Is it in -init...? -awakeFromNib? -loadView? -viewDidLoad? The exact location matters since these occur in a well-defined sequence. If you put your code in the wrong place, it will run before the nib is unarchived and fully reconnected, so the nib will clobber whatever your code did.
So: when is your [self setGraph] (I can't bring myself to use dot syntax) code getting run?

Updating NSTableView after adding an item to array

I have a tableview whose columns are bound to an array controller. I'm programmatically adding items to the array:
Person is just a custom class;
personsPresent is in another class called Meeting, it's a MutableArray which contains Person objects.
Person *newPerson = [[Person alloc]init];
[[[self meeting] personsPresent] addObject:newPerson];
[[self tableView] reloadData];
This works, but the values don't show up into my table view until I sort the columns. I thought reloadData would do it for me.
In my xib, my nsarraycontroller's objectcotnroller settings is set to Class and Person. It's controller content it's bound to File Owner and it's model key path is meeting.personsPresent.
Any help would be appreciated.
Mark
You're mutating the array behind the array controller's back. Either ask the array controller to -rearrangeObjects (stinky) or use its -addObject: method (better).

Getting NSArrayController item for right click in NSCollectionView

I'm trying to create a file explorer using nscollectionview and am currently implementing a right click menu for each item (i.e. copy/delete/rename/etc). I currently have:
An NSCollectionView linked with an NSArrayController which holds a custom object
A subclass of NSBox as the view for each item, this also tracks mouse events and passes them to the controller
The controller has an NSMenu outlet (rcMenu) and also an NSView outlet (itemView) for the NSBox subclass that should be where the menu popup
The code for calling the menu is:
[NSMenu popUpContextMenu:rcMenu withEvent:event forView:itemView];
Once run, this works in that the menu pops up when right clicking the item in the collection view, but on inspecting the event that's passed to the controller, there's not really anything I could use to find out which item was right clicked other than the x,y coordinates (which seem to be for the NSWindow rather than the item or NSCollectionView). What I really want is the object in the NSArrayController that had it's view right clicked.
Is this down to me setting it up incorrectly, is there an easy way to figure it out, or is it just that tough to work it out?
You might try setting the menu of each collection view item's view. Most likely, you'll do this by overriding +defaultMenu in your item view class. Once you do that, comment out the popUpContextMenu:withEvent:forView: message and see whether you can get away without it.
Furthermore, it would then not be too hard to serve up different menus for different kinds of items (e.g., folders vs. packages vs. files, and different types of files at that). You'd probably have to override -menuForEvent: instead of +defaultMenu.
I found an other solution that might help.
For this solution I made a subclass of NSCollectionViewItem and NSView, respectively (and for the ease of explaining) ItemViewController and ItemView.
I'm assuming you work with IB where you have already bound your NSCollectionView to the ContentArray of your NSArrayController (also bind the selectionIndexes).
Next add an ViewController object to the NIB and make sure its custom class is set to the ItemViewController. Now connect it to the itemPrototype outlet of your NSCollectionView.
Next add a Custom View object to the NIB and set its custom class to ItemView. Connect its outlet to the view property of your ItemViewController.
In the interface file of ItemView create a representedObject-like property. With this I mean something like:
#property (nonatomic, assign) id someRepresentedObjectPropertyName
This will be the property which will represent the item in your NSArrayController.
Now go to the implementation file of ItemViewController and override the -setRepresentedObject: method. In here we will first let the ItemViewController handle setting its representedObject, afterwards we assign the same representedObject to the property we made in ItemView. The override would look like:
-(void)setRepresentedObject:(id)representedObject {
[super setRepresentedObject:representedObject];
//Do some appropiate checking on the representedObject...
if (self.view != nil) {
[(ItemView *)self.view setSomeRepresentedObjectPropertyName:self.representedObject];
}
}
Now if you go back to the implementation of ItemView you can override the method -rightMouseUp: and build/set-up a NSMenu there and use the -popUpMenuPositioning...: method. The someRepresentedObjectPropertyName property of ItemView should be set to the correct item in your NSArrayController.
EDIT:
Instead of overriding -setRepresentedObject you could also bind the ItemView's someRepresentedObjectPropertyName to representedObject.someRepresentedObjectPropertyName