Objective C : class init with block ? - objective-c

Is it possible to, say, use a block as a completion handler in a View Controller's init method so that the parent view controller is able to fill in the details in a block without having to create a custom initWithNibName:andResourceBundle:andThis:andThat: for each possible properties ?
// ... in the didSelectRowAtIndexPath method of the main view controller :
SubViewController *subviewController = [[SubViewController alloc] initWithNibName:nil bundle:nil completionHandler:^(SubViewController * vc) {
vc.property1 = NO;
vc.property2 = [NSArray array];
vc.property3 = SomeEnumValue;
vc.delegate = self;
}];
[self.navigationController pushViewController:subviewController animated:YES];
[subviewController release];
in SubViewController.m :
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil completionHandler:(void (^)(id newObj))block {
self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
if (self) {
block(self);
}
return self;
}
instead of
// ... in the didSelectRowAtIndexPath method of the main view controller :
SubViewController *subviewController = [[SubViewController alloc] initWithNibName:nil bundle:nil andProperty1:NO andProperty2:[NSArray array] andProperty3:SomeEnumValue andDelegate:self];
[self.navigationController pushViewController:subviewController animated:YES];
[subviewController release];
with
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil andProperty1:(BOOL)p1 andProperty2:(NSArray *)p2 andProperty3:(enum SomeEnum)p3 andDelegate:(id<MyDelegateProtocol>)myDelegate {
self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
if (self) {
self.property1 = p1;
self.property2 = p2;
self.property3 = p3;
self.delegate = myDelegate;
}
return self;
}
So that I can do whatever I want in the main controller vs calling a predefined init method (and having to write one for each possible initialization).
Is it something bad ? will there be retain cycles ?

Which advantages do you see in using a block? The initializer is commonly used to set up private state of the instance. This private state could not be accessed from the block since the block is implemented somewhere else.
If you only use public properties why not setting them up after initialization?
SubViewController *vc = [[SubViewController alloc] initWithNibName:nil bundle:nil];
vc.property1 = NO;
vc.property2 = [NSArray array];
vc.property3 = SomeEnumValue;
vc.delegate = self;
That's exactly what the block version does (without the hassle).
Is it something bad ? will there be retain cycles ?
No, but I would dismiss your proposition for architectural reasons: You are either breaking encapsulation of the class or do not gain any advantage over just doing what the block does after initialization.

The questions are:
What is the benefit of this way?
Will you ba able to manage this code efficient?
Be aware that when program is growing new levels of invocation will be added and this will make your code hard to read, maintain, extend or develop. Think about future subclassing as well and how will you debug this code in the future to find some mismatched value.
Blocks can make you code faster, but delegate pattern will make your code clean and run in one thread, which is easy to maintain in the future, and this is real value for the professional programmer.

You could define your
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil completionHandler:(void (^)(id newObj))block;
method in a category of UIViewController; from there you would call initWithNib and then execute your completion block on the just allocated self:
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil completionHandler:(void (^)(id newObj))block
{
if ((self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil])) {
block(self);
}
return self;
}
I think this should work fine.

This is possible, no retain problems. Everything will be called synchronously on the same thread.
BUT
What is the benefit of not doing this the simple way - calling another method after init, e.g.
MyController* controller = [[[MyController alloc] init] autorelease];
[self updateController:controller];
Does the code have be called from the init method?
In general, I would advice to create separate init... methods if you want to initialize the object in different ways.

i'm really not sure why you would take this approach (unless you want to get the attention of the OOD police). it's not a good idea.
your controller can define a function or method which returns an instance, initialized the way it wants instead.

Related

Custom initialisation while using storyboard

Prior to storyboards, and working with .xib files, I used this piece of code to do screen adjustments during init.
- (id)initForNewItem:(BOOL)isNew {
self = [super initWithNibName:#"NAME" bundle:nil];
if (self) {
if (isNew) {
// Place some buttons only when isNew is true
}
}
return self;
}
Then I also implemented this to generate an exception when initWithNibName is directly called because I wanted to avoid that:
-(id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil {
#throw [NSException exceptionWithName:#"Wrong initializer" reason:#"Use initForNewItem:" userInfo:nil];
return nil;
}
Then another viewcontroller could call the custom init and the screen would be set:
MyViewController *myViewController = [[MyViewController alloc] initForNewItem:YES]; // Or NO ofcourse depending on the situation.
Now I'm using storyboard and initWithNibName is never called. Instead only initWithCoder is called but this method can only be called by the storyboard right? So how would I do something similar while using storyboard?
You wouldn't do this with storyboards.
The way to do it is to use properties to give the correct values once the view controller has been initialised.
This is done in - (void)prepareForSegue:(UIStoryBoardSegue *)segue sender:(id)sender.
You can access segue.destinationViewController and then use this to put values in to.

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];
}

Subclassing a subclass of UIViewController load issue

So, here's the issue. I'm relatively new to iOS programming, and I've taken on a giant project.
I'm working on a game with multiple levels which basically follow the same pattern, but have different sprite images and values, and I just decided to lay out all the levels in IB for speed's sake (not necessarily best practices, I know, but work with me). Each "level" has its own view controller, along the lines of "FireLevel1ViewController," "FireLevel2ViewController," etc. All of these view controllers inherit from a custom subclass of UIViewController I created called "GameController."
My problem is, when I open each level on my test device, GameController's viewDidLoad is getting called before the init or viewDidLoad methods of my subclass controllers, and so none of my level images/values are getting assigned to the superclass properties. Specifically, I have a pause menu that ought to be hidden at the outset of the level (I am doing setHidden in GameController's viewDidLoad), but since GameController's viewDidLoad runs before FireLevel1 has a chance to associate the correct IB property with PauseMenu, GameController just hides an empty view, and the actual PauseMenu never gets hidden.
I may have multiple problems going on here, but mostly I think I'm not really understanding correctly how to subclass a subclass of UIViewController and how to get the second subclass's properties/values/images to work in the first subclass's methods.
Thanks so much for any help! I hope that question made sense...
Code for GameController:
#implementation GameController
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
if (self)
{
// Custom initialization
}
return self;
}
- (void)viewDidLoad
{
[super viewDidLoad];
[self.view addSubview:pauseMenu];
[pauseMenu addSubview:helpMenu];
//Hides the pause and help menus until their respective buttons are pressed
[pauseMenu setHidden:YES];
[helpMenu setHidden:YES];
isPaused = NO;
}
Code for FireLevel1Controller:
-(id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
if(self)
{
theMainview = mainview;
theScene = scene;
theBG = bg;
theHudView = hud;
thePauseView = pauseMenu;
theHelpView = helpMenu;
}
return self;
}
-(void)viewDidLoad
{
[super viewDidLoad];
firstTurret = [[StationaryEnemy alloc]init:turretImage1 baseView:base1];
secondTurret = [[StationaryEnemy alloc]init:turretImage2 baseView:base2];
NSLog(#"I'm in view did load");
}
Did you try using viewWillAppear? That method should be called after all the visible UI elements have been initialized.
Ahhhh I figured it out - I was accidentally negating my variables. I should've been doing mainview = theMainview; instead of theMainview = mainview - I was just assigning them all to zero. I also moved them all out of init into viewDidLoad, and moved [super viewDidLoad] underneath them, and now it works perfectly!

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.