Objective C programmatical binding doesn't work - objective-c

An imageView is properly bound as IBOutlet ("imageOKData"). I have programmatically bound its visibility (hidden) to a BOOL property of a custom object that already is available when creating the binding.
in ViewController.m:
- (void)viewDidLoad
{
[super viewDidLoad];
NSLog(#"Create binding: %hhd",_global.dataFile.imported);
// --> "Create binding: 0"
NSDictionary* reverseBool = [NSDictionary dictionaryWithObjectsAndKeys:NSValueTransformerNameBindingOption,NSNegateBooleanTransformerName,
nil];
[_imageOKData bind:#"hidden" toObject:_global.dataFile withKeyPath:#"imported" options:reverseBool];
}
With the above code the image should be hidden once the viewDidLoad. But its still visible. Even if i remove the reverseBool options it' still there. What am i doing wrong?

It was the options dictionary. Jeeeez... i've mistaken the order. It must be
NSDictionary* reverseBool = [NSDictionary dictionaryWithObjectsAndKeys:NSNegateBooleanTransformerName, NSValueTransformerNameBindingOption,nil];
Sometimes you don't see the hand in front of your face.

Related

Why am I getting an error about NSCollectionView not being nil?

This is my BookListObject.h:
# property (nonatomic,retain)NSImage *bookImage;
# property (nonatomic,copy) NSString *bookName;
and I connect these bindings:
bookImage ----- representedObject.bookImage
bookName ------representedObject.bookName
arrayController-------arrangedObject
-(void)awakeFromNib{
BookListObject * book=[[BookListObject alloc] init];
book.bookName=#"unknown";
book.bookImage=[NSImage imageNamed:#"dis"];
_bookList=[NSMutableArray arrayWithObjects:book, nil];
[arrayController addObject:_bookList];
}
I have seen this video.
When I run my project, it crashes and prompts that NSCollectionView item prototype must not be nil.
I found the following method, but it doesn't work:
NSCollectionViewItem *itemPrototype = [self.storyboard instantiateControllerWithIdentifier:#"collectionViewItem"];
self.bookListCV.itemPrototype=itemPrototype;
The problem remains. What should I do? How can I bind the collectionView with collectionViewItem in the storyboard? I think XCode6 supports it only when you use xib.

Subclassing NSPopUpButton to add a bindable property

I'm trying to add a bindable property to a custom NSPopUpButton subclass.
I've created a "selectedKey" property, which is meant to store a NSString associated with selected menu item.
In control init, I set self as button target and an action for the button (valueChanged:), which in turn sets "selectedKey" in accordance with user selection:
#interface MyPopUpButton : NSPopUpButton {
NSMutableDictionary *_items;
NSString *_selectedKey;
}
#property(nonatomic, readwrite, copy) NSString* selectedKey;
- (void)addItemWithTitle:(NSString *)title andKey:(NSString *)key;
#end
#implementation MyPopUpButton
- (instancetype)initWithFrame:(NSRect)frameRect {
self = [super initWithFrame:frameRect];
if (self) {
_items = [NSMutableDictionary new];
[NSObject exposeBinding:#"selectedKey"];
[super setTarget:self];
[super setAction:#selector(valueChanged:)];
}
return self;
}
- (void)addItemWithTitle:(NSString *)title andKey:(NSString *)key {
[super addItemWithTitle:title];
[_items setValue:title forKey:key];
}
- (void)valueChanged:(id)sender {
for (NSString *aKey in [_items allKeys]) {
if ([[_items valueForKey:aKey] isEqualToString:[self titleOfSelectedItem]]) {
self.selectedKey = aKey;
}
}
}
- (void)setSelectedKey:(NSString *)selectedKey {
[self willChangeValueForKey:#"selectedKey"];
_selectedKey = selectedKey;
[self didChangeValueForKey:#"selectedKey"];
[self selectItemWithTitle:[_items valueForKey:selectedKey]];
}
#end
This seems to work as expected: "selectedKey" property is changed when user changes PopUpButton selection.
Unfortunately, trying to bind this property, doesn't work.
[selectButton bind:#"selectedKey" toObject:savingDictionary withKeyPath:key options:#{NSContinuouslyUpdatesValueBindingOption : #YES }]
When selection is changed bind object is not updated accordingly.
What am I doing wrong?
I've created a "selectedKey" property, which is meant to store an NSString associated with selected menu item.
Bindings is definitely the way to go here, but your use of bind:toObject:withKeyPath:options is incorrect.
The value that you pass to the first argument must be one of the predefined values made available by Apple for that particular control. For NSPopUpButton objects, the available values are documented in the NSPopUpButton Bindings Reference. When you look through this document you'll see that there is no selectedKey option. There is however a selectedValue which has the following description:
An NSString that specifies the title of the selected item in the NSPopUpButton.
Thus the correct way to set up the binding is as follows:
[self.btn bind:#"selectedValue"
toObject:self
withKeyPath:#"mySelectedString"
options:nil];
This is all you need to do: when the action selector is fired the property stored at the keyPath you passed in as the third argument will already have been updated. This means that you can (i) get rid of the setSelectedKey method entirely, (ii) remove exposeBinding line, and (iii) remove the code within valueChanged: - Cocoa has already done this bit.
The example below implements just two methods, but, if I've understood your intentions, they should be all you need:
- (void)awakeFromNib {
self.btn.target = self;
self.btn.action = #selector(popUpActivity:);
[self.btn bind:#"selectedValue"
toObject:self
withKeyPath:#"mySelectedString"
options:nil];
// I've added a couple of additional bindings here; they're
// not required, but I thought they'd be instructive.
[self.btn bind:#"content"
toObject:self
withKeyPath:#"myItems"
options:nil];
[self.btn bind:#"selectedIndex"
toObject:self
withKeyPath:#"mySelectedIndex"
options:nil];
// Now that you've set the bindings up, use them!
self.myItems = #[#"Snow", #"Falling", #"On", #"Cedars"];
self.mySelectedIndex = #3; // "Cedars" will be selected on startup
// no need to set value of mySelectedString, because it will be
// updated automatically by the selectedIndex binding.
NSLog("%#", self.mySelectedString) // -> "Cedars"
}
- (void)popUpActivity:(id)sender {
NSLog(#"value of <selectedIndex> -> %#", self.mySelectedIndex);
NSLog(#"value of <selectedString> -> %#", self.mySelectedString);
}
A final point worth making is that none of the above should be a part of an NSPopUpButton subclass. It looks like you can - and therefore should - do everything you need to do without a custom subclass of this control. In my demo-app the code above belongs to the ViewController class, you should try doing this also.

Setting/getting global variables in objective-C

I am writing an app which is a sort of dictionary - it presents the user with a list of terms, and when clicked on, pops up a dialog box containing the definition. The definition itself may also contain terms, which in turn the user can click on to launch another definition popup.
My main app is stored in 'myViewController.m'. It calls a custom UIView class, 'CustomUIView.m' to display the definition (this is the dialog box that pops up). This all works fine.
The text links from the CustomUIView then should be able to launch more definitions. When text is tapped in my CustomUIView, it launches another CustomUIView. The problem is, that this new CustomUIView doesn't have access to the hash map which contains all my dictionary's terms and definitions; this is only available to my main app, 'myViewController.m'.
Somehow, I need to make my hash map, dictionaryHashMap, visible to every instance of the CustomUIView class. dictionaryHashMap is created in myViewController.m when the app opens and doesn't change thereafter.
I don't wish to limit the number of CustomUIViews that can be opened at the same time (I have my reasons for doing this!), so it would be a little resource intensive to send a copy of the dictionaryHashMap to every instance of the CustomUIView. Presumably, the solution is to make dictionaryHashMap a global variable.
Some of my code:
From myViewController.m:
- (void)viewDidLoad
{
self.dictionaryHashMap = [[NSMutableDictionary alloc] init]; // initialise the dictionary hash map
//... {Code to populate dictionaryHashMap}
}
// Method to pop up a definition dialog
- (void)displayDefinition:(NSString *) term
{
NSArray* definition = [self.dictionaryHashMap objectForKey:term]; // get the definition that corresponds to the term
CustomUIView* definitionPopup = [[[CustomUIView alloc] init] autorelease]; // initialise a custom popup
[definitionPopup setTitle: term];
[definitionPopup setMessage: definition];
[definitionPopup show];
}
// Delegation for sending URL presses in CustomUIView to popupDefinition
#pragma mark - CustomUIViewDelegate
+ (void)termTextClickedOn:(CustomUIView *)customView didSelectTerm:(NSString *)term
{
myViewController *t = [[myViewController alloc] init]; // TODO: This instance has no idea what the NSDictionary is
[t displayDefinition:term];
}
From CustomUIView.m:
// Intercept clicks on links in UIWebView object
- (BOOL)webView: (UIWebView*)webView shouldStartLoadWithRequest: (NSURLRequest*)request navigationType: (UIWebViewNavigationType)navigationType {
if ( navigationType == UIWebViewNavigationTypeLinkClicked ) {
[myViewController termTextClickedOn:self didSelectTerm:request];
return NO;
}
return YES;
}
Any tips on how to make the dictionaryHashMap visible to CustomUIView would be much appreciated.
I have tried making the dictionaryHashMap global by doing the following:
Changing all instances of 'self.dictionaryHashMap' to 'dictionaryHashMap'
Adding the line 'extern NSMutableDictionary *dictionaryHashMap;' to CustomUIView.h
Adding the following outside of my implementation in myViewController.m: 'NSMutableDictionary *dictionaryHashMap = nil;'
However, the dictionaryHashMap remains invisible to CustomUIView. As far as I can tell, it actually remains a variable which is local to myViewController...
It's not resource-intensive to pass around the reference (pointer) to dictionaryHashMap. A pointer to an object is only 4 bytes. You could just pass it from your view controller to your view.
But I don't know why you even need to do that. Your view is sending a message (termTextClickedOn:didSelectTerm:) to the view controller when a term is clicked. And the view controller already has a reference to the dictionary, so it can handle the lookup. Why does the view also need a reference to the dictionary?
Anyway, if you want to make the dictionary a global, it would be more appropriate to initialize it in your app delegate, in application:didFinishLaunchingWithOptions:. You could even make the dictionary be a property of your app delegate and initialize it lazily.
UPDATE
I didn't notice until your comment that termTextClickedOn:didSelectTerm: is a class method. I assumed it was an instance method because myViewController starts with a lower-case letter, and the convention in iOS programming is that classes start with capital letters. (You make it easier to get good help when you follow the conventions!)
Here's what I'd recommend. First, rename myViewController to MyViewController (or better, DefinitionViewController).
Give it a property that references the dictionary. Whatever code creates a new instance of MyViewController is responsible for setting this property.
Give CustomUIView properties for a target and an action:
#property (nonatomic, weak) id target;
#property (nonatomic) SEL action;
Set those properties when you create the view:
- (void)displayDefinition:(NSString *)term {
NSArray* definition = [self.dictionaryHashMap objectForKey:term];
CustomUIView* definitionPopup = [[[CustomUIView alloc] init] autorelease]; // initialise a custom popup
definitionPopup.target = self;
definitionPopup.action = #selector(termWasClicked:);
...
In the view's webView:shouldStartLoadWithRequest: method, extract the term from the URL request and send it to the target/action:
- (BOOL)webView: (UIWebView*)webView shouldStartLoadWithRequest: (NSURLRequest*)request navigationType: (UIWebViewNavigationType)navigationType {
if ( navigationType == UIWebViewNavigationTypeLinkClicked ) {
NSString *term = termForURLRequest(request);
[self.target performSelector:self.action withObject:term];
return NO;
}
return YES;
}
In the view controller's termWasClicked: method, create the new view controller and set its dictionary property:
- (void)termWasClicked:(NSString *)term {
MyViewController *t = [[MyViewController alloc] init];
t.dictionary = self.dictionary;
[t displayDefinition:term];
}
Create a class that will be used as singleton. Example.
You Should always keep your data in separate class as the mvc pattern suggest and that could be achieved by using a singleton class for all your dictionary terms and accesing them from every custom view when needed.

Nested NSCollectionView With Bindings

I am trying to nest NSCollection view inside of one another. I have tried to create a new project using the Apple Quick Start Guide as a base.
I start by inserting a collection view into my nib, to the view that is automatically added I drag another collection view onto it. The sub-collection view added gets some labels. Here is a picture of my nib:
I then go back and build my models:
My second level model .h is
#interface BPG_PersonModel : NSObject
#property(retain, readwrite) NSString * name;
#property(retain, readwrite) NSString * occupation;
#end
My First level model .h is:
#interface BPG_MultiPersonModel : NSObject
#property(retain, readwrite) NSString * groupName;
#property(retain,readwrite) NSMutableArray *personModelArray;
-(NSMutableArray*)setupMultiPersonArray;
#end
I then write out the implementation to make some fake people within the first level controller(building up the second level model):
(edit) remove the awakefromnibcode
/*- (void)awakeFromNib {
BPG_PersonModel * pm1 = [[BPG_PersonModel alloc] init];
pm1.name = #"John Appleseed";
pm1.occupation = #"Doctor";
//similar code here for pm2,pm3
NSMutableArray * tempArray = [NSMutableArray arrayWithObjects:pm1, pm2, pm3, nil];
[self setPersonModelArray:tempArray];
} */
-(NSMutableArray*)setupMultiPersonArray{
BPG_PersonModel * pm1 = [[BPG_PersonModel alloc] init];
pm1.name = #"John Appleseed";
pm1.occupation = #"Doctor";
//similar code here for pm2,pm3
NSMutableArray * tempArray = [NSMutableArray arrayWithObjects:pm1, pm2, pm3, nil];
return tempArray;
}
Finally I do a similar implementation in my appdelegate to build the multiperson array
- (void)awakeFromNib {
self.multiPersonArray = [[NSMutableArray alloc] initWithCapacity:1];
BPG_MultiPersonModel * mpm1 = [[BPG_MultiPersonModel alloc] init];
mpm1.groupName = #"1st list";
mpm1.personModelArray = [mpm1 setupMultiPersonArray];
(I'm not including all the code here, let me know if it would be useful.)
I then bind everything as recommended by the quick start guide. I add two nsarraycontrollers with attributes added to bind each level of array controller to the controller object
I then bind collectionview to the array controller using content bound to arrangedobjects
Finally I bind the subviews:
with the grouptitle label to representedobject.grouptitle object in my model
then my name and occupation labels to their respective representedobjects
I made all the objects kvo compliant by including the necessary accessor methods
I then try to run this app and the first error I get is: NSCollectionView item prototype must not be nil.
(edit) after removing awakefromnib from the first level model I get this
Has anyone been successful at nesting nscollection views? What am I doing wrong here? Here is the complete project zipped up for others to test:
http://db.tt/WPMFuKsk
thanks for the help
EDITED:
I finally contacted apple technical support to see if they could help me out.
Response from them is:
Cocoa bindings will only go so far, until you need some extra code to make it all work.
When using arrays within arrays to populate your collection view the
bindings will not be transferred correctly to each replicated view
without subclassing NSCollectionView and overriding
newItemForRepresentedObject and instantiating the same xib yourself,
instead of using the view replication implementation provided by
NSCollectionView.
So in using the newItemForRepresentedObject approach, you need to
factor our your NSCollectionViewItems into separate xibs so that you
can pass down the subarray of people from the group collection view to
your inner collection view.
So for your grouped collection view your override looks like this:
- (NSCollectionViewItem *)newItemForRepresentedObject:(id)object
{
BPG_MultiPersonModel *model = object;
MyItemViewController *item = [[MyItemViewController alloc] initWithNibName:#"GroupPrototype" bundle:nil];
item.representedObject = object;
item.personModelArray = [[NSArrayController alloc] initWithContent:model.personModelArray];
return item;
}
And for your inner collection subclass your override looks like this:
- (NSCollectionViewItem *)newItemForRepresentedObject:(id)object
{
PersonViewController *item = [[PersonViewController alloc] initWithNibName:#"PersonPrototype" bundle:nil];
item.representedObject = object;
return item;
}
here is a sample project that they sent back to me -
http://db.tt/WPMFuKsk
I am still unable to get this to work with my own project. Can the project they sent back be simplified further?
Please take a closer look at this answer
Short answer:
Extracting each NSView into its own .xib should solves this issue.
Extended:
The IBOutlet’s specified in your NSCollectionViewItem subclass are not connected when the prototype is copied. So how do we connect the IBOutlet’s specified in our NSCollectionViewItem subclass to the controls in the view?
Interface Builder puts the custom NSView in the same nib as the NSCollectionView and NSCollectionViewItem. This is dumb. The solution is to move the NSView to its own nib and get the controller to load the view programmatically:
Move the NSView into its own nib (thus breaking the connection between the NSCollectionViewItem and NSView).
In I.B., change the Class Identity of File Owner to the NSCollectionViewItem subclass.
Connect the controls to the File Owner outlets.
Finally get the NSCollectionViewItem subclass to load the nib:
Usefull links:
how to create nscollectionview programatically from scratch
nscollectionview tips
attempt to nest an nscollectionview fails
nscollectionview redux

Why are my Cocoa bindings broken?

I have a window with an NSTextField (in Snow Leopard), which I have binded to an NSString function in my WindowController class. This string will combine information about my table view's selection and count, provided by my array controller. It gets an initial value, "0 0", but doesn't ever update, when the selection or count changes. The binding looks like this (File's Owner is MyWindowController):
I implemented + (NSSet *)keyPathsForValuesAffecting<key> (below), but the binding never updates, even when the array controller's total count and selection change.
(Additional troubleshooting performed) I had originally been using the Display Pattern Value binding of the NSTextField, but I needed more complicated logic than that binding afforded. I then started listening to the selection changed/changing events of the TableView that displays the array controller's contents and changing the Display Pattern Value bindings dynamically, but that felt like a hack, and overly complicated.
I'm sure there's something I'm missing, but I can't tell what. Does anyone have any ideas? I've read through Apple's key-value-observing documentation, and this seems to be all that's necessary. I've checked, and my keyPathsForValuesAffectingMyString is getting called, but myString only gets called once. I've distilled my code below (updated x3).
Update 1/21
I'm still plugging away trying to figure this out. When I addObserver to self for the arrayController key paths, the notifications do fire as expected, so my key paths and the key value observing mechanism is fine. When I call [self didChangeValueForKey:#"myString"]; within my observeValueForKeyPath method for the same keys, the binding still doesn't update, leading me to believe it's a bindings problem rather than a KVO problem. I'm going to be reading up on the bindings mechanism more...
#interface MyWindowController : NSWindowController {
IBOutlet NSArrayController *arrayController;
}
- (NSArrayController *)arrayController;
- (NSString *)myString;
#end
#implementation MyWindowController
+ (NSSet *)keyPathsForValuesAffectingMyString {
return [NSSet setWithObjects:
#"arrayController.arrangedObjects",
#"arrayController.selection",
nil];
}
- (NSArrayController *)arrayController {
return arrayController;
}
- (NSString *)myString {
// Just as an example; I have more complicated logic going on in my real code
return [NSString stringWithFormat:#"%#, %#",
[arrayController valueForKeyPath:#"arrangedObjects.#count"],
[arrayController valueForKeyPath:#"selection.#count"]];
}
#end
I’ve verified this exact same bug. Someone on Cocoabuilder had a guess as to why the bug happens:
http://www.cocoabuilder.com/archive/cocoa/284396-why-doesn-nsarraycontroller-selection-et-al-fire-keypathsforvaluesaffectingkey.html#284400
I can’t speak as to whether this explanation is true, but I certainly can’t get +keyPathsForValues… to work with NSArrayControllers.
I've got a workaround, but I'm not happy about it, since it shouldn't be necessary, and I would still prefer to get the bindings working properly. I won't accept this answer, and will delete it if someone posts an actual fix. </disclaimer>
#interface MyWindowController : NSWindowController {
IBOutlet NSArrayController *arrayController;
IBOutlet NSTextField *fieldThatShouldBeBinded;
}
- (NSString *)myString;
#end
#implementation MyWindowController
- (void)awakeFromNib {
[arrayController addObserver:self
forKeyPath:#"selection"
options:0
context:NULL];
[arrayController addObserver:self
forKeyPath:#"arrangedObjects"
options:0
context:NULL];
}
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context {
if( object == arrayController )
[fieldThatShouldBeBinded setStringValue:[self myString]];
}
- (NSString *)myString {
return [NSString stringWithFormat:#"%#, %#",
[arrayController valueForKeyPath:#"arrangedObjects.#count"],
[arrayController valueForKeyPath:#"selection.#count"]];
}
#end
Make sure that the arrayController outlet is connected in Interface Builder. I'm guessing that it's nil.
Don't use the #count keyword. Bindings and KVO on array controllers will get updated when the content changes. If that doesn't work, then there is a problem somewhere else.
Another option is to use the display pattern bindings instead of a composite property. Bind Display Pattern Value1 to arrayController.arrangedObjects.#count and Display Pattern Value2 to arrayController.selection.#count, and set the pattern to "%{value1}#, %{value2}#"
I met the same problem and found another way (but it is still workaround).
You have to declare dynamic workaround property. In implementation section, just return new empty object for it. Now, you can KVO this workaround property.
#property(nonatomic,retain) NSArray *workaround;
#dynamic workaround;
- (NSArray *)workaround { return [NSArray array]; } // new *every* time
- (void)setWorkaround:(NSArray *)unused { }
+ (NSSet *)keyPathsForValuesAffectingMyString { return [NSSet setWithObject:#"workaround"]; }
To get this work, you still need to manually bind self.workaround to arrayController.selectedObjects (or whatever):
- (void)awakeFromNib // or similar place
{
[super awakeFromNib];
[self bind:#"workaround" toObject:arrayController withKeyPath:#"selectedObjects" options:nil];
}
Manual binding works as expected, workaround is updated with what you have bound it to. But KVO tests whether property value is really changed (and stops propagating if it is the same). If you return new self.workaround value every time, it works.
Warning: never call -[setWorkaround:] by yourself — this will effectively flush the other side of binding (arrayController.selectedObjects in this case).
This method has some benefits: you avoid centralized observeValueForKeyPath:... and your logic is in the right place. And it scales well, just add workaround2, 3, and so on for similar cases.