Please help, this is driving me mad :(
I have a form as part of an OSX Cocoa app i'm writing which uses an NSPopUpButton to select the type of record you are creating/editing.
I am using bindings to bind the popup button to an array controller and my selected value is bound to an NSString.
I can get the popup populated and can select an item and save the record ok but there are two things I can't get right:
1) The selected value object contains the textual name of the item rather than its key - I have set Content Objects binding
2) I cannot restore the popup's selected item on reloading the form.
Here's some code samples to assist:
#implementation snippet:
IBOutlet NSPopUpButton *popMediaType;
IBOutlet NSArrayController *acMediaTypes;
#property (nonatomic, copy) NSString *selMediaType;
#interface snippet:
- (void)setupForm {
NSArray *types = [[NSArray alloc] initWithObjects:[NSDictionary dictionaryWithObjectsAndKeys:#"Video", #"name", #"video", #"value", nil],
[NSDictionary dictionaryWithObjectsAndKeys:#"Advert", #"name", #"advert", #"value", nil],
[NSDictionary dictionaryWithObjectsAndKeys:#"Graphic", #"name", #"graphic", #"value", nil],
nil];
[acMediaTypes setContent:types];
}
In interface builder:
acMediaTypes is the referencing outlet for an array controller object called mediaTypes
popMediaType is the referencing outlet for the NSPopUpButton on the form
My NSPopUpButton's bindings is configured thus:
Content (mediaTypes.arrangedObjects)
Bind to: mediaTypes
Controller Key: arrangedObjects
Model Key Path: <null>
Content Objects (mediaTypes.arrangedObjects)
Bind to: mediaTypes
Controller Key: arrangedObjects
Model Key Path: value
Content Values (mediaTypes.arrangedObjects)
Bind to: mediaTypes
Controller Key: arrangedObjects
Model Key Path: name
Selected Value (File's Owner.selMediaType)
Bind to: Files Owner
Controller key: <null>
Model Key Path: selMediaType
The popup displays correctly, shows the text from the "name" portion of the array properly.
If I read selMediaType in my code using something like NSLog() then I see "Video" for example if that item is selected - it should be "video" (lower case from the "value" part of the dictionary)
I can store the values in my sqlite database with no problems and inspect them using sqlite3 from the terminal and everything is saved.
If I load the values back from the table, I would have thought that just setting selMediatype would have reset the item position in the NSPopUpButton but it doesn't.
Solution was easy in the end but not obvious to the new players to the game.
First, your recipient variable to bind the selection to must be a property not just an iVar
Second, in the bindings, bind selected object, NOT selected value.
Binding selected value will put the displayed valued in your target variable eg "Video" but binding to selected object will put the key in your target variable eg "video".
Related
I have an NSTableView with a checkbox column and an NSArrayController.
The NSTableView is bound to the NSArrayController to show its content.
The checkbox column is bound to imgArrayController.arrangedObjects.check1. When the program starts the array is empty. I add values using the arrayController in a loop this way:
[imgArrayController addObject:[NSDictionary dictionaryWithObjectsAndKeys:
[NSImage imageNamed: [NSString stringWithFormat:#"%#" ,fileName]], #"image",
[NSString stringWithFormat:#"%#" ,fileName], #"filename",
#"YES", #"check1",
nil]];
The correct number of rows are added, however I am getting an error when it tries to set checkbox to YES. This is the error I get:
2013-02-03 19:11:46.357 Stockuploader[561:303] Error setting value for key path check1 of object {
} (from bound object <NSTableColumn: 0x100152280> identifier: check1): [<__NSDictionaryI 0x10010c190> setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key check1.
Could somebody please help me to understand what I am doing wrong? The rows appear inside the table but they are all empty.
Try using NSMutableDictionary instead of NSDictionary. Also try using [NSNumber numberWithBool: YES] instead of #"YES" (which is a string, not a boolean).
EDIT:
In a cell-based NSTableView you would need to bind the column (not the cell itself) to the arrayController with a Controller Key of arrangedObjects and a Model Key Path of check1.
In a view-based NSTableView you would need to bind the checkbox control to the Table Cell View with a Model Key Path of objectValue.check1.
I'm writing my first master detail view in Cocoa. My data model is really simple: just an NSMutableArray that contains instances of a single class having a few NSStrings properties.
Here's what I've created so far:
A class representing the instances.
An NSMutableArray specified as a property of my app delegate to hold the class instances.
A master detail view, with an NSTable and some text fields to hold the properties of a selection instance.
An NSArrayController with a binding specifying the app delegate, and the name of the NSMutableArray property as the model key path.
Bindings between the columns of the NSTable and the NSArrayController (controller key = "arrangedObjects", and model key path of each column = a property of the class).
Bindings between the text fields of the view and the selection (controller key = "selection", and model key path of each text field = a property of the class).
A "+" button and a "-" button in the view to enable adding and removing objects.
However, I'm having two problems with this design:
(1) I can't find a good way to implement the "+" and "-" buttons. First, I bound them to the add: and remove: properties of the array controller. While this seems to work, it has a problem: my class declares an init member that initializes the NSStrings to stub values, but the array controller doesn't seem to [init] the new instances, because the new list entry has empty strings for each column.
Next, I attached them to IBActions in my app delegate that added or removed an object from the NSMutableArray. However, this feels wrong - it feels like I'm violating the model-view-controller architecture by not talking to the array controller. For example, the "-" function has to talk to the array controller to get the selected item. Also, I notice that I have to send a [didChangeValueForKey] message after altering the array - which feels like a signal that I'm doing this wrong.
(2) One of the detail subviews in my view is an NSTextView that's bound to an NSString in the selected object. It really isn't working as expected: when new text is entered, the text remains the same even if other members of the class (which should have different values) are selected. Also, the text data isn't being saved to any instance of the class.
Any thoughts? Thanks in advance.
Here is an example that should be close to what you want:
#import "AppDelegate.h"
#import "Members.h"
#implementation AppDelegate
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
self.theData = [NSMutableArray arrayWithObject:[[Members alloc] init]];
}
-(IBAction)addMember:(id)sender {
[self.controller addObject:[[Members alloc] init]];
}
-(IBAction)removeMember:(id)sender {
[self.controller removeObjectAtArrangedObjectIndex:self.controller.selectionIndex];
}
The array controller (whose IBOutlet is controller) has its content array bound to theData. Members is the name of my class that has 3 string properties, name, author and blurb. In the detail view, the 2 text field's are bound to ArrayController.selection.name or author, just like you said you did. The other property, blurb, is bound the same way (ArrayController.selection.blurb), but to the Attributed String value of a text view. This worked fine, I'm not sure what your trouble with the text view was. If I add text to the text view, it does show up there if I click another row in the mater table and then click back (also if I log the array, it shows up there too).
I have an API provided NSArray with a bunch of content objects – we'll call this acquiredFruit – and an empty NSMutableArray called likedFruit.
I've created NSArrayControllers for both arrays and bound my TableView to acquiredFruit.arrangedObjects. The first column of the tableView is bound to arrangedObjects.name and correctly shows all the delicious fruit.
I've created a second column with a checkbox – when the user fills the box I'd like to add the fruit to my likedFruit array. Unchecking the box should remove the fruit object from the likedFruit array.
Essentially I'd like my NSTableView to join between two array controllers. I have a feeling I should be making a single separate controller for this, but I'm unsure how to approach the problem.
I should also mention that I'm aware I could iterate through my array and construct another object with the fields I need, but my goal is to do this by using bindings, if possible.
Thoughts?
I think you should use one array controller.
You can have an attribute on Fruit called liked. Now your "liked" checkbox column is connected to arrangedObjects.liked. Later, when you want to determine the set of all liked fruits, you can query your fruits array:
NSArray * likedFruits = [ allFruitsArray filteredArrayUsingPredicate:[ NSPredicate predicateWithFormat:#"liked = YES"] ] ;
If in another part of your UI you are displaying only liked fruit, you can set your array controller's filterPredicate to the predicate above to get just those fruits.
EDIT: Let's say NSFruit is provided via someone else's API. Let's use the "General Technique for Adding Properties to Someone Else's Class":
#interface NSFruit (Liking)
#property ( nonatomic ) BOOL liked ;
#end
#implementation NSFruit (Liking)
-(BOOL)liked
{
return [ objc_getAssociatedObject( self, "_abliked" ) boolValue ] ;
}
-(void)setLiked:(BOOL)b
{
objc_setAssociatedObject( self, "_abliked", [ NSNumber numberWithBool:b ], OBJC_ASSOCIATION_RETAIN_NONATOMIC ) ;
}
#end
(I've written this same code for like 100 posts recently!)
I'm not at my Xcode computer right now, so i can't test this, but it seems like you don't really need another array controller, but just another array to hold the likedFruits. I think you need to create an array of dictionaries from your acquiredFruits array that would have one key for the fruit name and another key with a bool value for whether the check box is checked --this bool would be bound to your second column. I'm not sure about the next step on how to tell the likedFruit array that it need to add a new fruit -- I think the check box could have an action method that you could use to have the likedFruit array add the object in the row where the check box was clicked.
After Edit:
Here is an example of how to do what I suggested. I take an array of fruits and turn it into an array of dictionaries (called theData) that include a key for the value of your check box (In IB the content array of the array controller is bound to theData, and the columns are bound to Array Controller.arrangedObjects.fruitName and Array Controller.arrangedObjects.isLiked). checkChanged is an IBAction connected to the check box (but note the sender is actually the table view), and I use the value of the check box to determine whether to add a fruit to likedFruits or delete one. I put one more method, connected to a button just to check the values in likedFruits.
#implementation AppDelegate
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
self.theData = [NSMutableArray array];
self.likedFruit =[NSMutableArray array];
NSArray *acquiredFruits = #[#"Apple",#"Orange",#"Pear",#"Peach"];
for (NSString *aFruit in acquiredFruits) {
NSDictionary *dict = [NSDictionary dictionaryWithObjectsAndKeys:aFruit,#"fruitName",[NSNumber numberWithBool:NO],#"isLiked", nil];
[self.theData addObject:[dict mutableCopy]];
}
self.theData = _theData;
// NSLog(#"%#",self.theData);
}
-(IBAction)checkChanged:(NSTableView *)sender { //connected to the button cell in the table view (but sender is the table view)
NSString *theFruit = [[self.controller.arrangedObjects objectAtIndex:sender.clickedRow ] valueForKey:#"fruitName"];
BOOL doWeLikeIt = [[[self.controller.arrangedObjects objectAtIndex:sender.clickedRow] valueForKey:#"isLiked"] boolValue];
if (doWeLikeIt) {
[self.likedFruit addObject:theFruit];
}else{
[self.likedFruit removeObject:theFruit];
}
}
-(IBAction)logLikedFruits:(id)sender {
NSLog(#"%#",self.likedFruit);
}
I want to show in a label the current number of elements in an NSArray called pages. Following other guides I did the following:
Created an NSArrayController in IB (called pagesController) and bounded it to the NSArray pages
Bounded the value property of the label to the NSArrayController with Controller Key = arrangedObjects and Model Key Path = #count
The problem is that when the program is running the label always shows "0". To check if the things are working correctly I tried to log a message when the user clicks on another button (the button basically inserts a new element in the pages array): NSLog(#"count = %d", [self.pagesController valueForKeyPath:#"arrangedObjects.#count"]); in this case the output is correct, i.e. the current number of elements in the array printed is correct.
Where am I mistaking?
You many not be using KVC correctly. Your array controller will only be notified with the pages property is redefined, not when objects are added to the array. Try wrapping your code with the appropriate change notifications:
[self willChangeValueForKey:#"pages"];
[pages addObject:someObject];
[self didChangeValueFOrKey:#"pages"];
i created a nsarraycontroller to display its data on my nstableview. this is working. but how do i modify this data now?
[arrayController addObject:[...]]
adds an object, how do i get it back?
i tried:
NSMutableArray *data = [arrayController mutableArrayValueForKey:#"column1"];]
but then i get this error
2011-05-29 19:25:50.125 TestApp[1665:903] [<NSArrayController 0x113808500> valueForUndefinedKey:]: this class is not key value coding-compliant for the key column1.
(the objects in my arraycontroller representing the rows in my view are of a class (named FileEntry) consisting of 4 properties and i added these as keys to my nsarraycontroller)
i thought kvc-compilant means every attribute has a getter and setter? (and isnt that what properties do?).
i tested my class if i could use this function:
FileEntry *entry = [[FileEntry alloc] initWithUrl:#"test"]; //(this adds the string "test" to the property "fileurl")
NSLog(#"%#\n", [entry valueForKey:#"fileurl"]);
and it returns:
> 2011-05-29 19:31:54.760 TestApp[1718:903] test
and it works. so how can my class not be kvc-compilant?
anyway, i also tried to use the tableviews datasource instead but cant get those 2 functions to work. would that be of more use than the nsarraycontroller? is it even possible to modify data of the nsarraycontroller?
Usually you need a contend array for the NSArrayController to handle, so you could simply take this array and use the NSMutableArray methods. For example you have in your MyController.h a declaration of an array with its setter and getter methods, and you only have to bind the contend array of the array controller to it. Then you can take two buttons for the add and remove methods of the array controller to add something. Now you must bind the value of the column to e.g. FileEntry.fileurl of your array controller managed objects, Now you should be able to add stuff to the table view. And you can also Edit the names in the table view. If you want to get some stuff out of the array in your program somewhere, you can use an outlet of your table view like this:
NSInteger row = [myTableViewOutlet selectedRow];
FileEntry* myEntry = [myArray objectAtIndex:row];