What is the designated initializer for NSTableCellView subclasses? - objective-c

I created a subclass of NSTableCellView to do some custom drawing. The table's content is obtained through a binding to an NSArrayController, thus, new instances of my NSTableCellView subclass are created 'automatically' when new data are added to the NSArrayController. I need some code to run once when a new instance is created so I thought it should go in init. I implemented both init and initWithFrame (see below) but neither of these seem to be called when new instances of the subclass are created (i.e. I don't see my NSLog messages in the console). Is there a different init method that I should use?
- (id)init {
self = [super init];
if (self) {
// Initialization code here.
NSLog(#"init");
}
return self;
}
- (id)initWithFrame:(NSRect)frame {
self = [super initWithFrame:frame];
if (self) {
// Initialization code here.
NSLog(#"init with frame");
}
return self;
}

To answer your question, the designated initializer is initWithFrame:. However, if a view is encoded in a NIB (as it is in this case), initWithCoder: is called. You have to override that method.
Don't use awakeFromNib; in general, it may get called more often than you expect, and I've seen it cause people trouble.
However, a good place to do initialization of your cell is in the table view delegate method viewForTableColumn:row: -- you can still use it and use bindings.
corbin
(I wrote the class in question).

Related

NSViewController New vs. InitWithNibName issues

I am having a weird error with NSViewController where if I allocate a view using the viewcontroller's regular init message, the view created is not my view, but when using the default NIB name, it does work.
Specifically, this code works all the time. It creates the view defined in the nib file, and displays it in the parentView.
+ (void)createTransparentViewCenteredInView:(NSView*)parentView withText:(NSString*)text duration:(NSTimeInterval)duration {
TransparentAccessoryViewController* controller = [[TransparentAccessoryViewController alloc] initWithNibName:#"TransparentAccessoryViewController" bundle:nil];
NSLog(#"%#", [controller.view class]); // Returns "TransparentAccessoryView" -- CORRECT
[parentView addSubview:controller.view];
}
However, the following code works SOME of the time (which is weird in that it doesn't always fail). With some parentViews, it works perfectly fine, and with others, it doesn't. The parent views are just random custom NSViews.
+ (void)createTransparentViewCenteredInView:(NSView*)parentView withText:(NSString*)text duration:(NSTimeInterval)duration {
TransparentAccessoryViewController* controller = [TransparentAccessoryViewController new];
NSLog(#"%#", [controller.view class]); // Returns "NSSplitView" -- INCORRECT
[parentView addSubview:controller.view];
}
The errors that comes up are as follows (I have no idea why it is bringing up an NSTableView, as I don't have an NSTableView here at all. Also, it is weird that it complains about an NSTableView when the type it prints is an NSSplitView):
2013-04-07 21:33:12.384 Could not connect the action refresh: to
target of class TransparentAccessoryViewController
2013-04-07 21:33:12.384 Could not connect the action remove: to target
of class TransparentAccessoryViewController
2013-04-07 21:33:12.385 * Illegal NSTableView data source
(). Must implement
numberOfRowsInTableView: and tableView:objectValueForTableColumn:row:
The NIB file defines a custom subclassed NSView, called TransparentAccessoryView, and hooks this up to the File Owner's view property, standard stuff (all I did was change the custom class name to TransparentAccessoryView). I added an NSLog's to see what was going on, and for some reason, in the second case, the view class type is incorrect and thinks it is an NSSplitView for some reason. The ViewController class is as follows:
#implementation TransparentAccessoryViewController
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil {
self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
if (self) {
// Initialization code here.
}
return self;
}
- (void)awakeFromNib {
self.textField.stringValue = #"";
}
+ (void)createTransparentViewCenteredInView:(NSView*)parentView withText:(NSString*)text duration:(NSTimeInterval)duration {
TransparentAccessoryViewController* controller = [[TransparentAccessoryViewController alloc] initWithNibName:#"TransparentAccessoryViewController" bundle:nil];
NSLog(#"%#", [controller.view class]);
[parentView addSubview:controller.view];
}
#end
I thought that the default init message triggers the viewcontroller to load the NIB named after the viewcontroller, which seems to be the case some of the time as the second version of my code works in certain conditions.
Does anyone know why this behavior is occurring at all?
From the docs:
If you pass in a nil for nibNameOrNil then nibName will return nil and
loadView will throw an exception; in this case you must invoke
setView: before view is invoked, or override loadView.
Therefore, if you're initializing a NSViewController with -init, you should call -setView: to set the view controller's view, or override -loadView. In the latter case, you could certainly implement the UIViewController-like behavior that you're probably expecting -- if nibNameOrNil is nil, try to load a nib that has the same name as the class.
I think that when you call init on a NSViewController, you're assuming that the implementation of init for NSViewController searches for a nib with the same name as the view controller and uses it. However, this is undocumented API or at least I can't seem to find any documentation supporting that assumption. The link you posted on your comments doesn't cite any documentation either and even reiterates that this is undocumented and that Apple could change this implementation at any point.
I think to assure that your code works in future versions of the SDK (and since it is already creating undesired behavior), you should not rely on this assumption. To achieve the same outcome simply override init and initWithNibName:bundle: in such a way as explained by this post:
#implementation MyCustomViewController
// This is now the designated initializer
- (id)init
{
NSString *nibName = #"MyCustomViewController";
NSBundle *bundle = nil;
self = [super initWithNibName:nibName bundle:bundle];
if (self) {
...
}
return self;
}
- (id)initWithNibName:(NSString *)nibName bundle:(NSBundle *)bundle
{
// Disregard parameters - nib name is an implementation detail
return [self init];
}

Objective-C how to call a method after self is initialized inside the object file?

Is there any way to know when a custom object is finished with being initialized from inside the object's file? Or let me rephrase the question, why can't I call any method inside this method?
- (id)initWithCoder:(NSCoder *)coder {
//NSLog(#"initWithCoder inside CustomObject (subclass of UIView)");
self = [super initWithCoder:coder];
if (self) {
//... initialization here
[self visibleEmptyButton]; //why does this method never get called?
}
return self;
}
EDIT:
-(void)viewDidLoad{
NSLog(#"viewDidLoad inside CustomObject(subclass of UIView) is called"); //It never gets called
[self viewDidLoad];
//initialization here...
}
(If the class you are init-ing is a subclass of UIViewController) Changing and setting things in the screen should be done after the view is loaded. Try doing it in this method:
- (void)viewDidLoad {
[super viewDidLoad];
[self visibleEmptyButton];
//Do the additional view altering here
}
If this method doesn't exist yet you can just add it to the .m file (no need to add it to the .h file).
In lieu of you're edit you could simply move the call to the UIViewController:
- (void)viewDidLoad {
[super viewDidLoad];
[TheInstanceOfYourViewClass visibleEmptyButton];
}
Also, to avoid making a whole bunch of small subview related methods public it often makes sense to create one method to handle the initial visual states.

How to write a custom initializer that prevents viewDidLoad being called?

I would like to build a custom init method for a UIViewController, but after digging around on the Internet and specifically in SO I am confused about designated initializers.
I have a subclass of an UIViewController with these two initializers:
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil {
self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
if ( self ) {
}
return self;
}
- (id) initWithFilename:(NSString *)aFilename {
self = [self initWithNibName:#"WallpaperDetailsViewController" bundle:nil];
if ( self ) {
self.filename = aFilename;
}
return self;
}
Then I have a viewDidLoad method that customizes the view according to the filename property:
- (void)viewDidLoad {
[super viewDidLoad];
// Create a UIImageView to display the wallpaper
self.wallpaper = [[UIImageView alloc] initWithImage:[UIImage imageNamed:self.filename]];
// ...
}
In another UIViewController I make the following call:
WallpaperDetailsViewController *detailsViewController = [[WallpaperDetailsViewController alloc] initWithFilename:#"foobar.png"];
[[self navigationController] pushViewController:detailsViewController animated:YES];
The result is that viewDidLoad is being called as a consequence of [self initWithNibName:], which does not initialize the UIImageView because self.filename is null.
According to other SO questions and answers, that should be the expected behavior. I am not sure about this because of my own experience in other projects prior to iOS 5. My question is:
How can I ensure that viewDidLoad: is call after initWithFilename: and not between initWithFilename: and initWithNibNameOrNil:bundle:?
If that's not possible, how can I implement an initializer method that receives custom data to create and customize the view?
Thanks!
I have found the problem.
WallpaperDetailsViewController does not inherit directly from UIViewController, but from another custom UIViewController I have implemented.
And what was the problem? That I have initialized a subview in the parent's initWithNibName method, instead of following the lazy-load technique and doing it in viewDidLoad. When WallpaperDetailsViewController was calling its parent initializer it got messy and cause viewDidLoad not to behave properly.
The solution? I moved every subview initialization in the parent class to its viewDidLoad method, and keep my original implementation of WallpaperDetailsViewController intact. Now everything is working as expected
Thanks to #Josh Caswell and #logancautrell
You don't need that empty implementation of initWithNibName:bundle:. Furthermore, it looks like your class here is establishing its designated initializer to be initWithFilename: If that's true, initWithFilename: should be calling the superclass's D.I.:
- (id) initWithFilename:(NSString *)aFilename {
// Call super's designated initializer
self = [super initWithNibName:#"WallpaperDetailsViewController"
bundle:nil];
if ( self ) {
self.filename = aFilename;
}
return self;
}
The rule is that all initializers within a class should call the class's D.I., and the D.I. should itself call the superclass's D.I.
It's not completely clear from what you've posted why loadView: is being called before your initializer has completed. Logancautrell's comment suggesting setting breakpoints in the view loading methods is good.
Why don't you just use a custom setter for the filename property that initializes the UIImage every time the filename is set?
Or, alternately, set the UIImage from the filename property in viewWillAppear: instead of viewDidLoad.
First, it is not recommended that you use dot syntax within your initializer. See the following for some good discussion:
Objective-C Dot Syntax and Init
Second, what you could do is assign the image in your initializer as well. So you could do something along the lines of
- (id) initWithFilename:(NSString *)aFilename {
self = [self initWithNibName:#"WallpaperDetailsViewController" bundle:nil];
if ( self ) {
filename = [aFilename retain];
wallpaper = [[UIImageView alloc] initWithImage:[UIImage imageNamed:aFileName]];
}
return self;
}
This will allow you to get everything setup and in good shape before viewDidLoad is called.
Good Luck!

initWithNibName: what kind of custom initialization?

edit : ok, what i've learned is you might choose between initWithNibName or initWithCoder, depending if you use a .xib or not. And "init" is just not the constructor method for UIVIewController.
This might seem to be a fairly simple question, but I'm not sure about the answer : I've read that this method "is only used for programatically creating view controllers", and in the doc : "It is loaded the first time the view controller’s view is accessed"
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil {
if (self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]) {
// Custom initialization
}
return self;
}
Ok so to understand it a bit more :
What would you write as "custom initialization" into this method?
When should you implement this method, like that in the code, if you can just write it after allocating your viewController (example : MyVC *myvc = [[MyVC alloc] initWithNibName:...bundle...];)
Thanks for your answer
Usually I do this:
// Initialize controller in the code with simple init
MyVC *myVC = [[MyVC alloc] init];
Then do this in my init method:
- (id)init {
self = [super initWithNibName:#"MyVC" bundle:nil];
if (!self) return nil;
// here I initialize instance variables like strings, arrays or dictionaries.
return self;
}
If controller needs some parameters from the initializee, then I write custom initWithFoo:(Foo *)foo method:
- (id)initWithFoo:(Foo *)foo {
self = [super initWithNibName:#"MyVC" bundle:nil];
if (!self) return nil;
_foo = [foo retain];
return self;
}
This allows simplification of initialization as well as extra initializers for your view controller if it can be initialized from different locations with different parameters. Then in initWithFoo: and initWithBar you'd simply call init which calls super and initializes instance variables with default values.
It's an init method, so you initialize everything you need to be initialized when you begin your work in your view controller. Every object ivar will be automatically initialized to nil, but you can initialize a NSMutableArray to work with or an BOOL that you want to be a certain value.
You implement this method every time you have something to initialize, as stated previously. You generally don't initialized things after allocating your view controller, that way you don't need it to do it every time you use your view controller (as you may use it at different place in your application). It's also the best practice.
I usually put in there configuration stuff that I know it wont change.
For example, the ViewController's title:
self.title = #"MyTitle";
Or if this is one of the main ViewController in a TabBar application. That is, it owns one of the tabs, then I configure its TabBarItemlike this:
UITabBarItem *item = [[UITabBarItem alloc] initWithTitle:#"something"
image:[UIImage imageNamed:#"something.png"]
tag:0];
self.tabBarItem = item;
There is all sorts of things you can do there.

alloc and init in a subclass

I have subclassed UIView and provided my own drawRect, which works fine. However I added these methods to my subclass:
- (id) alloc
{
NSLog(#"Alloc");
return [super alloc];
}
- (id) init
{
NSLog(#"Init");
if (self = [super init])
flashWhite = true;
return self;
}
I thought that whenever an object of the subclass is created (which happens through Interface Builder), the alloc and init methods would get called. However the object IS created, but my init and alloc don't get called. Shouldn't this happen to ensure proper initialization?
Also, building produces a warning that UIView may not respond to 'alloc' - doesn't it have to inherit this from NSObject, or how could a UIView be properly created?
My objective in the above was that my subclassed view would be able to do custom initialization after IB got it created.
init is not the designated initializer for UIView; initWithFrame: is. If you override initWithFrame:, it should get called, but init never gets called by UIView.
You're right about alloc, it's a class method. UIViews use initWithFrame: for initialisation, so look into overriding that.
Apple generally builds their init overrides like this:
-(id)init{
self = [super init];
if (self) {
//Custom Initialization goes here
}
}
Though the above commenters are correct. initwithFrame: is the designated initializer
However, the override as you formatted your question will get you a compiler warning for your trouble.