I have a view MyView, and it has images which I want to bind with an array in my AppDelegate.
MyView class
#interface MyView : NSView {
#private
NSArray *images;
}
#end
+ (void)initialize
{
[self exposeBinding:#"images"];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
NSLog(#"Changed!");
}
My AppDelegate
#property (retain) NSArray *images;
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
images = [[NSMutableArray alloc] init];
[view bind:#"images" toObject:self withKeyPath:#"images" options:nil];
// [self addObserver:view forKeyPath:#"images" options:0 context:nil]; // !!!
MyImage *img = [[MyImage alloc] ...];
[self willChangeValueForKey:#"images"];
[[self images] addObject:img];
[self didChangeValueForKey:#"images"];
[img release];
}
Without [self addObserver:view forKeyPath:#"images" options:0 context:nil]; the method observeValueForKeyPath: is never called.
Is it necessary to call addObserver: when using bind:? Does bind: set the KVO? And why doesn't binding work?
What you need is an implemented setter for the images property like below. The most common use-case for this is that you need to invalidate the drawing and request redraw with
-setNeedsDisplay:YES.
- (void)setImages:(NSArray *)newImages
{
if(newImages != images) {
[images release];
images = newImages;
[images retain];
}
[self setNeedsDisplay:YES]; // Addition and only difference to synthesized setter
}
You can drop the -exposeBinding: call, since that has only influence on plugins for Interface Builder, and those where lost with the introduction of Xcode 4.
The reason why the -observeValueForKeyPath:ofObject:change:context: message is not send is that for a binding the observer is not the bound-to object. There is another object in the background. (In the stack form a breakpoint you can see that its class is NSEditableBinder.) So it is correct to register as observer from within the view to the view property #"images".
Another way to get notified about a change in the view is to override -setValue:forKey: method. Then you would need to check the key string and see if it was equal to #"images". But since there are other methods from the KVC protocol like -setValue:forKeyPath:, you would need to be extra careful to not disturb the machinery, i.e. always call super.
Uh. I just realize that my answer so far assumes the easier case where you replace the whole array. Your question was for an array modification. (You do declare an immutable array property in your example, though, which only allows replacement. So keep it as declared, and my approach so far will work. Below I show the other alternative.)
Ok, lets assume you do this in the app delegate, a replacement:
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
[view bind:#"images" toObject:self withKeyPath:#"images" options:nil];
MyImage *img = [[MyImage alloc] ...];
self.images = [NSArray arrayWithObject:img];
[img release];
}
You don't need to post the change (using willChangeValueForKey: and didChangeValueForKey:, since you go through the declared property. They do that for you.
Now to the other approach where you modify an array. For that you need to use a mutable array property and modify it through an KVO-notifying proxy, like this:
[self mutableArrayValueForKey:#"images"] addObject:img];
This would pick up the change on the sending (bound-to) side. Then it would be transported to the view through the binding machinery, and eventually set using KVC.
There, on the receiving end in the view, you would need to pick up the property change to #"images". That could be done by overwriting the collection accessor method(s) and do more work there, instead of just accepting the the change. But that is a bit complicated, since there are quite a few accessor methods (See docs). Or, simpler, you could add another observation relationship from within the view.
For that, somewhere in initialization (-awakeFromNib: for example) of the view:
[self addObserver:self forKeyPath:#"images" options:0 context:nil];
and then:
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context
{
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
if([keyPath isEqualToString:#"images"]) {
[self setNeedsDisplay:YES]; // or what else you need to do then.
}
}
Note that this last observer relationship has nothing to do with the binding any longer. The value change to the bound property properly arrives at the view without, you just don't realize (get notified).
That should work.
The only way to have observeValueForKeyPath called is to call addObserver. Binding works through a different mechanism.
Related
I have an NSCollectionView specified as both my DataSource and my Delegate.
I have two issues:
Rather than doing the registerClass method, attempting to instead use the 3 lines of commented code with the (non-nil) protoNib means of registering with an NSCollectionView causes theItem to always be nil.
Using the class registry option, all works mostly fine. But if I remove the willDisplayItem and didEndDisplayingItem stubs, the system eats up gobs of memory on its first call to itemForRepresentedObjectAtIndexPath (with thousands of internal calls to these two stubs) and eventually crashes. Instruments shows thousands of 4k #autoreleasepool content items being created by AppKit.
Any idea why this might be happening?
-(void)awakeFromNib {
[self registerClass:[MECollectionViewItem class] forItemWithIdentifier:#"EntityItem"];
// NSString *nibName = NSStringFromClass([MECollectionViewItem class]);
// NSNib *protoNib = [[NSNib alloc] initWithNibNamed:nibName bundle:nil];
// [self registerNib:protoNib forItemWithIdentifier:#"EntityItem"];
__weak typeof(self) weakSelf = self;
[self setDelegate:weakSelf];
[self setDataSource:weakSelf];
...
}
- (MECollectionViewItem *)collectionView:(NSCollectionView *)collectionView
itemForRepresentedObjectAtIndexPath:(NSIndexPath *)indexPath;
{
MECollectionViewItem *theItem = [self makeItemWithIdentifier:#"EntityItem"
forIndexPath:indexPath];
return theItem;
}
-(void)collectionView:(NSCollectionView *)collectionView
willDisplayItem:(NSCollectionViewItem *)item
forRepresentedObjectAtIndexPath:(NSIndexPath *)indexPath
{
}
-(void)collectionView:(NSCollectionView *)collectionView
didEndDisplayingItem:(nonnull NSCollectionViewItem *)item
forRepresentedObjectAtIndexPath:(nonnull NSIndexPath *)indexPath
{
}
The Appkit classes are not designed to be their own delegate. NSCollectionView implements several NSCollectionViewDelegate methods and calls the delegate. I don't know why it's implemented like this and it doesn't feel right but it is what it is. If the collection view is its own delegate and a delegate method isn't implemented in the subclass then the call causes an infinite loop. Solution: don't set delegate to self.
I am trying to customize an NSImageCell for NSTableView using NSArrayController and bindings to change the background of the cell which is selected. So, I created two NSImage images and retain them as normalImage and activeImage in the cell instance, which means I should release these two images when the cell calls its dealloc method. And I override
- (void)drawInteriorWithFrame:(NSRect)cellFrame inView:(NSView *)controlView
and
- (void) setObjectValue:(id) inObject
But I find that when I click any cell in the tableview, the cell's dealloc method is called.
So I put NSLog(#"%#", self); in the dealloc method and - (void)drawInteriorWithFrame:inView: and I find that these two instance are not same.
Can anyone tell me why dealloc is called every time I click any cell? Why are these two instances not the same? What does OS X do when I customize the cell in NSTableView?
BTW: I found that the -init is called only once. Why?
EDIT:
My cell code
#implementation SETableCell {
NSImage *_bgNormal;
NSImage *_bgActive;
NSString *_currentString;
}
- (id)init {
if (self = [super init]) {
NSLog(#"setup: %#", self);
_bgNormal = [[NSImage imageNamed:#"bg_normal"] retain];
_bgActive = [[NSImage imageNamed:#"bg_active"] retain];
}
return self;
}
- (void)dealloc {
// [_bgActive release]; _bgActive = nil;
// [_bgNormal release]; _bgNormal = nil;
// [_currentString release]; _currentString = nil;
NSLog(#"dealloc: %#", self);
[super dealloc];
}
- (void)drawInteriorWithFrame:(NSRect)cellFrame inView:(NSView *)controlView {
NSLog(#"draw: %#", self);
NSPoint point = cellFrame.origin;
NSImage *bgImg = self.isHighlighted ? _bgActive : _bgNormal;
[bgImg drawAtPoint:p fromRect:NSZeroRect operation:NSCompositeSourceOver fraction:1.0];
NSPoint strPoint = cellFrame.origin;
strPoint.x += 30;
strPoint.y += 30;
[_currentString drawAtPoint:strPoint withAttributes:nil];
}
- (void) setObjectValue:(id) inObject {
if (inObject != nil && ![inObject isEqualTo:_currentString]) {
[self setCurrentInfo:inObject];
}
}
- (void)setCurrentInfo:(NSString *)info {
if (_currentString != info) {
[_currentString release];
_currentString = [info copy];
}
}
#end
As a normal recommendation, you should move to ARC as it takes cares of most of the memory management tasks that you do manually, like retain, releases. My answers will assume that you are using manual memory management:
Can anyone tell me why dealloc is called every time I click any cell ?
The only way for this to happen, is if you are releasing or auto-releasing your cell. If you are re-using cells, they shouldn't be deallocated.
Why these tow instance are not same ?
If you are re-using them, the cell that you clicked, and the cell that has been deallocated, they should be different. Pay close attention to both your questions, in one you assume that you are releasing the same cell when you click on it, on the second you are seeing that they are different.
What does Apple do when I custom the cell in NSTableView ?
Apple as a company? Or Apple as in the native frameworks you are using? I am assuming you are going for the second one: a custom cell is just a subclass of something that the NSTableView is expecting, it should behave the same as a normal one plus your custom implementation.
BTW: I found that the init is called only once, and why ?
Based on this, you are probably re-using cells, and only in the beginning they are actually being initialised.
It would be very useful to see some parts of your code:
Your Cell's code
Your NSTableView cell's creation code.
I have the following code to load a UIImagePickerController which works fine.
UIImagePickerController *mediaUI = [[UIImagePickerController alloc] init];
mediaUI.sourceType = UIImagePickerControllerSourceTypeSavedPhotosAlbum;
mediaUI.mediaTypes = [[NSArray alloc] initWithObjects: (NSString *) kUTTypeMovie, nil];
mediaUI.delegate = self;
[controller presentModalViewController: mediaUI animated: YES];
return YES;
I would like to load a modal view with some help information on how to use the UIImagePickerController:
UIStoryboard *storyboard = self.storyboard;
HelpViewController *svc = [storyboard instantiateViewControllerWithIdentifier:#"HelpViewController"];
[self presentViewController:svc animated:YES completion:nil];
How can I display the UIImagePickerController after the user dismisses the HelpViewController view?
Don't be tempted to move directly from HelpViewController to UIImagePickerController, you need to get there via your mainViewController.
Let's put your code into a method...
- (void) presentImagePicker {
UIImagePickerController *mediaUI = [[UIImagePickerController alloc] init];
mediaUI.sourceType = UIImagePickerControllerSourceTypeSavedPhotosAlbum;
mediaUI.mediaTypes = [[NSArray alloc] initWithObjects: (NSString *) kUTTypeMovie, nil];
mediaUI.delegate = self;
[controller presentModalViewController: mediaUI animated: YES];
return YES;
}
(Note that presentModalViewController:animated is depracated since ~iOS5, and you should really replace it with
[controller presentViewController:mediaUI animated:YES completion:nil];)
Let's call your viewControllers mainVC, helpVC and imageVC. There are two ways you can implement this.
method 1 - performSelector
The quick-and-slightly-dirty solution is to do this in your helpVC's dismiss button method:
- (IBAction)dismissHelpAndPresentImagePicker:(id)sender
{
UIViewController* mainVC = self.presentingViewController;
[mainVC dismissViewControllerAnimated:NO completion:
^{
if ([mainVC respondsToSelector:#selector(presentImagePicker)])
[mainVC performSelector:#selector(presentImagePicker)];
}];
}
It's slightly dirty because you need to ensure that presentImagePicker is implemented in mainVC - the compiler will give you no warnings if it is not. Also you are running a completion block after it's object has been dismissed, so there's no certainty it's going to work (in practice, it does, but still...)
Note that you have to assign the pointer self.presentingViewController's to a local variable (mainVC). That's because when helpVC is dismissed, it's presentingViewController property is reset to nil, so by the time you get to run the completion block you cannot use it. But the local variable mainVC is still valid.
method 2 - protocol/delegate
The clean way to do this is to use a protocol in helpVC to declare a delegate method, and make mainVC the delegate. This way the compiler will keep track of everything and warn you if it is not correctly implemented.
Here are the steps to do that:
In helpVC.h add this protocol above the #interface section:
#protocol helpVCDelegate
- (void) dismissHelpAndPresentImagePicker;
#end
In helpVC.h interface section declare a property for its delegate:
#property (nonatomic, weak) id <helpVCDelegate> delegate;
(the <helpVCDelegate> tells the compiler that the delegate is expected to conform to the protocol, so it will have to implement dismissHelpAndPresentImagePicker)
In helpVC.m your method can now look like this:
- (IBAction)dismissHelpAndPresentImagePicker:(id)sender
{
[self.delegate dismissHelpAndPresentImagePicker];
}
In MainVC, when you create HelpVC (=svc in your code), set MainVC as it's delegate:
HelpViewController *svc = [storyboard instantiateViewControllerWithIdentifier:#"HelpViewController"];
svc.delegate = self;
[self presentViewController:svc animated:YES completion:nil];
And be sure to implement the delegate method dismissHelpAndPresentImagePicker
- (void) dismissHelpAndPresentImagePicker
{
[self dismissViewControllerAnimated:NO completion:^{
[self presentImagePicker];
}];
}
Personally I would always use method 2. But I offered up a that solution earlier today to a similar question, and the questioner seemed to think protocol/delegate was overcomplicated. Maybe my answer just made it seem so, I have tried to simplify it here.
I have a NSDocument class, where I'd need to access the main menu window, the one that gets opened when the app start. When I operate in that window from the app all seems to work, but when trying to do the same operations from readFromFileWrapper:ofType:error: the window I access seems to be nil. Why this happens?
EDIT: Some code which deals with this:
- (BOOL)readFromFileWrapper:(NSFileWrapper *)fileWrapper ofType:(NSString *)typeName error:(NSError **)outError
{
if([[NSFileManager alloc] fileExistsAtPath:[NSString stringWithFormat:#"%#/Project.plist",[[self fileURL] path]]]) {
NSLog(#"%#", [[self fileURL] path]);
NSDictionary *project = [NSDictionary dictionaryWithContentsOfFile:[NSString stringWithFormat:#"%#/Project.plist",[[self fileURL] path]]];
if([[project objectForKey:#"type"] isEqualToString:#"vote"]) {
[self openProject:[[self fileURL] path] type:#"vote"];
return YES;
} else if([[project objectForKey:#"type"] isEqualToString:#"quiz"]) {
[self openProject:[[self fileURL] path] type:#"quiz"];
return YES;
} else {
return NO;
}
} else {
return NO;
}
}
That is my readFromFileWrapper:ofType:error: method. Here is my openProject:type: method:
-(void)openProject:(NSString *)filepath type:(NSString *)type
{
NSLog(#"Opening project # %#",filepath);
NSLog(#"%#", [MainWindow description]);
[projectDesignerView setFrame:[[[[MainWindow contentView] subviews] objectAtIndex:0] frame]];
[projectDesignerToolbar setFrame:[MainWindow frame] display:FALSE];
[[MainWindow contentView] replaceSubview:[[[MainWindow contentView] subviews]objectAtIndex:0] with:projectDesignerView];
[[projectDesignerToolbar toolbar] setShowsBaselineSeparator:YES];
[MainWindow setToolbar:[projectDesignerToolbar toolbar]];
[MainWindow setRepresentedFilename:filepath];
[MainWindow setTitle:[NSString stringWithFormat:#"%# - %#", [[filepath lastPathComponent] stringByDeletingPathExtension], [projectDesignerToolbar title]]];
NSString *path = [[NSBundle mainBundle] pathForResource:#"projectDesigner" ofType:#"html"];
NSURL *url = [NSURL fileURLWithPath:path];
[[projectDesignerWebview mainFrame] loadRequest:[NSURLRequest requestWithURL:url]];
}
NSLog(#"%#", [MainWindow description]); returns nil, when MainWindow should be the Main App Window. I think the problem is that double-clicking on a file reallocs all, and hence is failing.
It's not entirely clear what you're asking. You mention that MainWindow is an outlet in MainMenu.xib but you don't specify what class is defining the outlet.
If this window is designed to have a single main "project" window then you should assign the outlet property in your application delegate.
You can then access this from other classes using something like [(YourAppDelegate*)[NSApp delegate] mainWindow];.
If, however, you are trying to obtain a reference to the window of the current document then it's a little bit more complicated.
The reason that NSDocument does not have a window outlet by default is that it is designed to work with instances of NSWindowController that themselves manage the various windows related to the document. This is so a document can have multiple windows showing different views of the same data, additional palettes related to the document and so on. Each instance of NSWindowController would have its own window nib file and window outlet.
By default, NSDocument creates a single instance of NSWindowController for you if you do not specifically create and assign NSWindowController instances to the document. This is automatic, you don't need to even know the window controller exists.
That means that if you aren't managing your document windows with NSWindowController instances yourself, you can get the window attached to the NSWindowController that is automatically-created by NSDocument like so:
/* Only implement this in an NSDocument instance where the
automatic window controller is being used.
If the document has multiple window controllers, you must
keep track of the main window controller yourself
and return its window
*/
- (NSWindow*)documentWindow
{
if([[self windowControllers] count] == 1)
{
return [[[self windowControllers] firstObject] window];
}
return nil;
}
The normal way to handle this is to add an IBOutlet to your NSDocument subclass, then hook it up to the document window in the .xib file.
In your .h:
#interface MyDocument : NSDocument
#property (nonatomic, assign) IBOutlet NSWindow *docWindow;
#end
In your .m:
#implementation MyDocument : NSDocument
#synthesize docWindow;
#end
Then, the most important part, open up MyDocument.xib (or whatever it's called), and drag a connection from File's Owner (assuming that's your NSDocument subclass, which it is by default) to the main document window, and hook it up to the docWindow outlet.
I'm seeing some quirky behaviour with Cocoa's KVC/KVO and bindings. I have an NSArrayController object, with its 'content' bound to an NSMutableArray, and I have a controller registered as an observer of the arrangedObjects property on the NSArrayController. With this setup, I expect to receive a KVO notification every time the array is modified. However, it appears that the KVO notification is only sent once; the very first time the array is modified.
I set up a brand new "Cocoa Application" project in Xcode to illustrate the problem. Here is my code:
BindingTesterAppDelegate.h
#import <Cocoa/Cocoa.h>
#interface BindingTesterAppDelegate : NSObject <NSApplicationDelegate>
{
NSWindow * window;
NSArrayController * arrayController;
NSMutableArray * mutableArray;
}
#property (assign) IBOutlet NSWindow * window;
#property (retain) NSArrayController * arrayController;
#property (retain) NSMutableArray * mutableArray;
- (void)changeArray:(id)sender;
#end
BindingTesterAppDelegate.m
#import "BindingTesterAppDelegate.h"
#implementation BindingTesterAppDelegate
#synthesize window;
#synthesize arrayController;
#synthesize mutableArray;
- (void)applicationDidFinishLaunching:(NSNotification *)notification
{
NSLog(#"load");
// create the array controller and the mutable array:
[self setArrayController:[[[NSArrayController alloc] init] autorelease]];
[self setMutableArray:[NSMutableArray arrayWithCapacity:0]];
// bind the arrayController to the array
[arrayController bind:#"content" // see update
toObject:self
withKeyPath:#"mutableArray"
options:0];
// set up an observer for arrangedObjects
[arrayController addObserver:self
forKeyPath:#"arrangedObjects"
options:0
context:nil];
// add a button to trigger events
NSButton * button = [[NSButton alloc]
initWithFrame:NSMakeRect(10, 10, 100, 30)];
[[window contentView] addSubview:button];
[button setTitle:#"change array"];
[button setTarget:self];
[button setAction:#selector(changeArray:)];
[button release];
NSLog(#"run");
}
- (void)changeArray:(id)sender
{
// modify the array (being sure to post KVO notifications):
[self willChangeValueForKey:#"mutableArray"];
[mutableArray addObject:[NSString stringWithString:#"something"]];
NSLog(#"changed the array: count = %d", [mutableArray count]);
[self didChangeValueForKey:#"mutableArray"];
}
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context
{
NSLog(#"%# changed!", keyPath);
}
- (void)applicationWillTerminate:(NSNotification *)notification
{
NSLog(#"stop");
[self setMutableArray:nil];
[self setArrayController:nil];
NSLog(#"done");
}
#end
And here is the output:
load
run
changed the array: count = 1
arrangedObjects changed!
changed the array: count = 2
changed the array: count = 3
changed the array: count = 4
changed the array: count = 5
stop
arrangedObjects changed!
done
As you can see, the KVO notification is only sent the first time (and once more when the application exits). Why would this be the case?
update:
Thanks to orque for pointing out that I should be binding to the contentArray of my NSArrayController, not just its content. The above posted code works, as soon as this change is made:
// bind the arrayController to the array
[arrayController bind:#"contentArray" // <-- the change was made here
toObject:self
withKeyPath:#"mutableArray"
options:0];
First, you should bind to the contentArray (not content):
[arrayController bind:#"contentArray"
toObject:self
withKeyPath:#"mutableArray"
options:0];
Then, the straightforward way is to just use the arrayController to modify the array:
- (void)changeArray:(id)sender
{
// modify the array (being sure to post KVO notifications):
[arrayController addObject:#"something"];
NSLog(#"changed the array: count = %d", [mutableArray count]);
}
(in a real scenario you'll likely just want the button action to call -addObject:)
Using -[NSMutableArray addObject] will not automatically notify the controller. I see that you tried to work around this by manually using willChange/didChange on the mutableArray. This won't work because the array itself hasn't changed. That is, if the KVO system queries mutableArray before and after the change it will still have the same address.
If you want to use -[NSMutableArray addObject], you could willChange/didChange on arrangedObjects:
- (void)changeArray:(id)sender
{
// modify the array (being sure to post KVO notifications):
[arrayController willChangeValueForKey:#"arrangedObjects"];
[mutableArray addObject:#"something"];
NSLog(#"changed the array: count = %d", [mutableArray count]);
[arrayController didChangeValueForKey:#"arrangedObjects"];
}
There may be a cheaper key that would give the same effect. If you have a choice I would recommend just working through the controller and leaving the notifications up to the underlying system.
A much better way than explicitly posting whole-value KVO notifications is to implement array accessors and use them. Then KVO posts the notifications for free.
That way, instead of this:
[self willChangeValueForKey:#"things"];
[_things addObject:[NSString stringWithString:#"something"]];
[self didChangeValueForKey:#"things"];
You would do this:
[self insertObject:[NSString stringWithString:#"something"] inThingsAtIndex:[self countOfThings]];
Not only will KVO post the change notification for you, but it will be a more specific notification, being an array-insertion change rather than a whole-array change.
I usually add an addThingsObject: method that does the above, so that I can do:
[self addThingsObject:[NSString stringWithString:#"something"]];
Note that add<Key>Object: is not currently a KVC-recognized selector format for array properties (only set properties), whereas insertObject:in<Key>AtIndex: is, so your implementation of the former (if you choose to do that) must use the latter.
Oh, I was looking for a long time for this solution ! Thanks to all !
After getting the idea & playing around , I found another very fancy way:
Suppose I have an object CubeFrames like this:
#interface CubeFrames : NSObject {
NSInteger number;
NSInteger loops;
}
My Array contains Objects of Cubeframes, they are managed via (MVC) by an objectController and displayed in a tableView.
Bindings are done the common way:
"Content Array" of the objectController is bound to my array.
Important: set "Class Name" of objectController to class CubeFrames
If I add observers like this in my Appdelegate:
-(void)awakeFromNib {
//
// register ovbserver for array changes :
// the observer will observe each item of the array when it changes:
// + adding a cubFrames object
// + deleting a cubFrames object
// + changing values of loops or number in the tableview
[dataArrayCtrl addObserver:self forKeyPath:#"arrangedObjects.loops" options:0 context:nil];
[dataArrayCtrl addObserver:self forKeyPath:#"arrangedObjects.number" options:0 context:nil];
}
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context
{
NSLog(#"%# changed!", keyPath);
}
Now, indeed, I catch all the changes : adding and deleting rows, change on loops or number :-)