Is it safe to set a view's value in awakeFromNib? - macos

Is it safe to do the following?
// in AppController.h
#interface AppController : NSObject
{
IBOutlet NSTextField *label;
}
#end
// in AppController.m
- (void)awakeFromNib
{
[label setIntValue:5];
}
Or is there a chance that label might not yet have been fully initialised when awakeFromNib is sent to the AppController instance?

Documentation says:
Important
Because the order in which objects are instantiated from an archive is not guaranteed, your initialization methods should not send messages to other objects in the hierarchy. Messages to other objects can be sent safely from within awakeFromNib—by which time it’s assured that all the objects are unarchived and initialized (though not necessarily awakened, of course).
In Fact, awakeFromNib is send to all objects the nib created and File's Owner , after creation of the objects and connecting outlets and actions is complete.

I'm not sure if it is safe.
But you should use viewDidLoad: for any view setup after the loading of the nib file.

Related

How do you archive a plain blue cube object in osx?

I have a plain blue cube object in IB called AppController. Here is the header file:
//AppController.h
#import <Cocoa/Cocoa.h>
#interface AppController : NSObject <NSCoding>
#property (weak) IBOutlet NSView *view;
#property (assign) int numberOfPresses;
- (IBAction)buttonPressed:(id)sender;
#end
As you can see it has an outlet to the view, a property called numberOfPresses, and and action tied to a button in IB.
Here is the implementation file
//AppController.m
#import "AppController.h"
#implementation AppController
-(void)awakeFromNib {
NSLog(#" number of presses = %d", _numberOfPresses);
}
- (IBAction)buttonPressed:(id)sender {
_numberOfPresses++;
NSLog(#" number of presses = %d", _numberOfPresses);
}
#pragma mark - Coding Protocol
-(void)encodeWithCoder:(NSCoder *)aCoder {
[aCoder encodeInt:_numberOfPresses forKey:#"numberOfPresses"];
}
-(id)initWithCoder:(NSCoder *)aDecoder {
self = [super init];
if (self) {
_numberOfPresses = [aDecoder decodeIntForKey:#"numberOfPresses"];
}
return self;
}
#end
As you can see, when the user presses on the button, the numberOfPresses is incremented. As I quit the app and fire it up again I would like
the numberOfPresses to be remembered.
The view outlet to stil be pointing to a valid view.
Now I always get 0 for numberOfPresses, and if I try to force the archiving using NSKeyedArchiver and NSKeyedUnarchiver from AppDelegate then I end up with view = null.
This problem is a general problem of archiving plain blue cube objects from IB. But I have not found an explanation of this on the Internet, although it should be a quite common problem. I must be missing or misunderstanding something.
Requirement #2 should be easy. If you do nothing, then the AppController should be loaded with its view created. In a sense it is "pre-archived" for you.
That means the real problem is how to restore the number of presses.
For something that simple, NSUserDefaults is a likely candidate. Update the defaults on a press and retrieve it during awakeFromNib.
Custom archiving and unarchiving is generally better for complete custom objects that are created dynamically, rather than ones loaded from IB.
(If it was a complicated network of objects you needed to restore, I'd be talking about CoreData instead.)
What I was after was a GENERAL solution that I could implement in ANY project and that ONLY uses NSCoding. (If anyone has a simpler solution, I would very much like to hear it). I ended up with a solution that works, by introducing two objects: a Hub, and a RootObject. The idea can be summarized in three points:
The idea is to free all objects that you want to archive from having any presence in the IB.
Introduce a Hub object which is responsible for all IB connections. The Hub is a blue cube in IB and has an outlet to AppDelegate. The AppDelegate has an outlet to the Hub.
Introduce a RootObject whose purpose is to propagate archiving to all objects that you want to archive. The RootObject is owned by the AppDelegate. The AppDelegate is responsible for initiating the archiving and unarchiving, and the RootObject is the mediator.

nsobject controller linked across two nib (xib) files. declaring nib instantiation

Apple's resource programming guide (RPG) states "it is better to distribute components across multiple nib files."...
therefore,
i have an associate window nib (Nib 2) that has an nsobjectcontroller that needs to be linked (selection self) to a nsarraycontroller in the main document window nib (Nib 1).
i need to share a common instance (either the nsarraycontroller in nib 1 or nsobjectcontroller in nib2). I can add a custom object in Nib 1. and set the File's Owner to that type of custom object. however, each nib instantiates their own instance.
is there a method of setting which nib an object was instantiated, or declaring an external reference.
i also "Make the File’s Owner the single point-of-contact for anything outside of the nib file" (RPG). Which is a NSWindowController.
Thanks in advance.
You probably want to make the owner of NIB1 responsible for instantiating NIB2. This will allow it to be the owner of both NIBs. In the common case, it might look something like this:
// In the interface...
#property (nonatomic, readwrite, retain) NSArray* nib2TopLevelObjects;
// In the implementation...
- (void)awakeFromNib
{
NSNib* nib2 = [[[NSNib alloc] initWithNibNamed: #"NIB2" bundle: [NSBundle mainBundle]] autorelease];
NSArray* tlo = nil;
[nib2 instantiateWithOwner: self topLevelObjects: &tlo];
self.nib2TopLevelObjects = [tlo retain];
// Do other stuff...
}
- (void)dealloc
{
[_nib2TopLevelObjects release];
[super dealloc];
}
At the end of this, NIB2 will have been instantiated with NIB1's owner as it's owner as well, and NIB2 will have plugged its objects into the shared owner (be sure not to plug things into the same outlet from both NIBs.)
All that said, I'm not sure this is necessarily the right pattern to use here. If these windows are both views upon the same document, you should probably make an NSWindowController subclass for each window and override -[NSDocument makeWindowControllers] to instantiate them. (The NSWindowController will be the "File's Owner" for each NIB.) Having the document NIB's owner be the NSDocument subclass is a "short cut" for simple situations. Once you need multiple windows, NSWindowControllers are the way to go.
Each NSWindowController can get back to the document via -document and the NSDocument subclass can coordinate state between the different NSWindowControllers. This is a cleaner approach, and avoids all the shenanigans with clobbered IBOutlets, etc.
For your specific case, I could see having a property like sharedArrayController on the NSDocument subclass that gets the NSArrayController from NIB1 during -makeWindowControllers, and re-vends it. Then you can then access it from NIB2 by binding to File's Owner > document.sharedArrayController.selection.

Manual binding in Cocoa

I have an ImageView which shows a lock, informing if an opened file is locked or not. I have 2 images for locked and unlocked cases. I want synchronize the displayed image with boolean value of my object representing an opened file.
To do this I want my ViewController to change the image in my ImageView depending on lock state of object. So both object and ViewController have a property "isLocked".
How can I synchronize them? It is easy in IB but I don't know how to do it programmatically. I tried in initialize method of my ViewController to use:
[ViewController bind:#"value" toObject:[ArrayController selection] withKeyPath:#"isLocked" options:nil];
But it doesn't work. In documentation it is said that I have to expose my binding before using it.
I try to put the following code in initializer method of my object:
[self exposeBinding:#"isLocked"];
But Xcode doesn't recognize this method.
Does somebody have experience with this kind of bindings establishing?
As #nick says, you want Key-Value-Observing.
[arrayController addObserver:self
forKeyPath:#"selection.isLocked"
options:NSKeyValueObservingOptionNew
context:#"this_context"]
Then when isLocked changes the -observeValueForKeyPath:ofObject:change:context: method that you have added to your viewController will be called (as long as you only manipulate isLocked in a KVC compliant way).
The options parameter lets you optionally tweak exactly what conditions will trigger the notification and what data is sent along with the notification. The context parameter is there to help you distinguish between notifications that you registered to receive and notifications your superclass registered to receive. It is optional.
Bindings seem like they might be useful to keep two values in sync. However, this is not what they do at all.
Yes, lots of things seem to give the impression that this is what they do, and there isn't much saying that this isn't what they do, also lots of people believe that this is what they do - but no, you cannot use them for this.
Only a handful of classes support bindings (they are listed here) and then, and this is the important bit, those classes only support binding their named bindings, and these bindings are not instance variables. eg NSTextField has a 'fontFamilyName' binding yet NSTextField does not have a 'fontFamilyName' property or instance variable, even a derived one. NSTextField does have a 'isBordered' property but not a binding - so you cannot bind 'isBordered'.
It does not mean anything to 'bind' an arbitrary property of an arbitrary Class.
Yes, you can bind two arbitrary values, the following code works just fine:
#import <Cocoa/Cocoa.h>
#interface SomeObject : NSObject
#property (retain,nonatomic) id someValue;
#end
#implementation SomeObject
#end
int main()
{
SomeObject *source=[SomeObject new];
SomeObject *target=[SomeObject new];
[target bind:#"someValue" toObject:source withKeyPath:#"someValue" options:0];
[source bind:#"someValue" toObject:target withKeyPath:#"someValue" options:0];
[source setSomeValue:#(42)];
NSLog(#"target: %#",[target someValue]);
[target setSomeValue:#(22)];
NSLog(#"source: %#",[source someValue]);
return 0;
}
As far as I can tell, the problem is the bit [ArrayController selection]. The first problem is that ArrayController is (or should be) a class, and getting the class's selection is probably pointless. The other problem is that even if this were an instance, you would be binding to the selection at the time of the call, which is almost certainly not what you want. You want to track the current selection as it changes.
So what you want is probably something like the following:
[myViewController bind:#"value" toObject:myArrayController withKeyPath:#"selection.isLocked" options:nil];

NIB, setValue:forKey and retain (iOS)

I know some mechanism of outlet connection when loading NIB, but I am not sure. So I'm asking some questions to ensure my knowledge. I assumed these things all true, but It's hard to find mention about these on reference documentation. Please point wrong and right things.
I have an IBOutlet defined like this: (Of course it's not recommended way)
#implementation
{
IBOutlet id var1;
}
#end
NIB loader (alloc | retain) & autorelease all top-level objects. So it will be dealloc on runloop turn ends without additional retain.
Connecting IBOutlets are done with KVC.
KVC uses accessor method primarily.
KVC uses setValue:forKey secondarily. And the IBOutlet will be handled by this method because there's no declared property or access method.
setValue:forKey retains the new value object.
setValue:forKey releases the old value object.
So top-level object connected to the IBOutlet will be retained once. So I have to release it to dealloc. This is why I must release objects connected to IBOutlet on dealloc method.
If the object connected another IBOutlet like the IBOutlet, it should be released once more to be dealloc.

initWithFrame not called, but awakeFromNib is

I am trying to subclass NSOutlineView. Here is my code:
OutlineViewSublcass.h:
#import <Cocoa/Cocoa.h>
#interface OutlineViewSubclass : NSOutlineView {
}
#end
OutlineViewSubclass.m:
#import "OutlineViewSubclass.h"
#implementation OutlineViewSubclass
- (id)initWithFrame:(NSRect)frame
{
self = [super initWithFrame:frame];
printf("debug A\n");
return self;
}
- (void)awakeFromNib
{
printf("debug B\n");
}
#end
The debug output is:
debug B
Why isn't (id)initWithFrame:(NSRect)frame being called?
Cocoa controls implement the NSCoding protocol for unarchiving from a nib. Instead of initializing the object using initWithFrame: and then setting the attributes, the initWithCoder: method takes responsibility for setting up the control when it's loaded using the serialized attributes configured by Interface Builder. This works pretty much the same way any object is serialized using NSCoding.
It's a little bit different if you stick a custom NSView subclass in a nib that doesn't implement NSCoding, in that case initWithFrame: will be called. In both cases awakeFromNib will be called after the object is loaded, and is usually a pretty good place to perform additional initialization in your subclasses.
Official Apple answer for this is Creating a Custom View.
View instances that are created in Interface Builder don't call initWithFrame: when their nib files are loaded, which often causes confusion. Remember that Interface Builder archives an object when it saves a nib file, so the view instance will already have been created and initWithFrame: will already have been called.
The awakeFromNib method provides an opportunity to provide initialization of a view when it is created as a result of a nib file being loaded. When a nib file that contains a view object is loaded, each view instance receives an awakeFromNib message when all the objects have been unarchived. This provides the object an opportunity to initialize any attributes that are not archived with the object in Interface Builder.

Resources