I was replacing the deprecated +(BOOL)loadNibNamed:owner: method in our macOS app with a more standard initWithWindowNib approach and came across the Apple guide about Document-Based App Programming Guide for Mac. One section particularly drew my attention: An NSWindowController Subclass Manages Nib Files
For records it say:
An NSWindowController object expects to be told what nib file to load
(through its initWithWindowNib... methods) because it is a generic
implementation of the default behavior for all window controllers.
However, when you write a subclass of NSWindowController, that
subclass is almost always designed to control the user interface
contained in a particular nib file, and your subclass would not work
with a different nib file. It is therefore inconvenient and
error-prone for the instantiator of the subclass to have to tell it
which nib file to load.
This problem is solved by overriding the init method to call the
superclass’s initWithWindowNibName: method with the correct nib name.
Then instantiators just use init, and the controller has the correct
nib file. You can also override the initWithWindowNib... methods to
log an error, as shown in Figure 2-4, because no instantiator should
ever try to tell your subclass which nib file to use. It is a good
idea for any NSWindowController subclass designed to work with a
specific nib file to use this technique. You should do otherwise only
if you are extending just the basic functionality of
NSWindowController in your subclass and have not tied that
functionality to any particular nib file.
Pretty reasonable, let's try it.
Note: There is a question related to the same documentation section, but it has a different problem, asks a different question and is not using Modern Objective-C syntax.
My MainWindowDelegate.h now has:
- (instancetype)init NS_DESIGNATED_INITIALIZER;
- (instancetype)initWithWindow:(nullable NSWindow *)window NS_UNAVAILABLE;
- (nullable instancetype)initWithCoder:(NSCoder *)coder NS_UNAVAILABLE;
- (instancetype)initWithWindowNibName:(NSNibName)windowNibName NS_UNAVAILABLE;
- (instancetype)initWithWindowNibName:(NSNibName)windowNibName owner:(id)owner NS_UNAVAILABLE;
- (instancetype)initWithWindowNibPath:(NSString *)windowNibPath owner:(id)owner NS_UNAVAILABLE;
And MainWindowDelegate.m now has:
- (instancetype)init {
self = [super initWithWindowNibName:#"MainWindowWin"];
if (self) {
// Initialization code here.
}
return self;
}
The only available initializer from the AppDelegate is correctly the init (if I try to use initWithWindowNibName then I get the error 'initWithWindowNibName:' is unavailable). So far so good and it works pretty nicely.
But now I have a bunch of warnings...
For - (instancetype)init { line
Designated initializer missing a 'super' call to a designated initializer of the super class
For self = [super initWithWindowNibName:#"MainWindowWin"]; line
Designated initializer invoked a non-designated initializer
If this is the suggested Apple approach to subclassing NSWindowController why all these warnings? Am I missing the point? How to fix the warnings and maintain only init initializer availability?
Related
I really haven't done much and i'm already stuck.
So far i've done:
added NSWindowController subclass (MikesWindowController.h & .m)
removed windowNibName from MikesDocument.m (since i'm implementing my own
WindowController subclass.)
I tried:
Tested if NSLog would come back at init, windowControllerDidLoadNib, applicationDidFinishLaunching. Only the NSLog at init printed.
And, tested the Main Menu -> File -> New after after compiling my Document app.
Am I implementing this right? Thanks. Any suggestions would be great! Under MikesDocument.m
-(void)makeWindowControllers{
MikesController *controller = [[MikesWindowController alloc]init];
[self addWindowController:controller];
}
After much deliberation I found that answer. Woohoo. Enjoy future.
removed initWithWindow from my NSWindowController subclass
implemented initWithWindowNibName to my NSWindowController subclass so now anytime I initialize I must specify the window nibName.
Below I implemented initWithWindowNibName in my NSWindowController subclass heres what it looks like:
MikesWindowController.m
-(id) initWithWindowNibName:(NSString *)windowNibName{
self = [super initWithWindowNibName:windowNibName];
return self;
}
(Below) Back again to the main document I corrected the makeWindowController method and instantiated my controller with "MikesDocument" (for MikesDocument.xib) and added it.
MikesDocument.m
-(void)makeWindowControllers{
MikesWindowController *controller =
// must tell controller which nib file to use.
[[MikesWindowController alloc]initWithWindowNibName:#"MikesDocument"];
[self addWindowController:controller];
}
Success! Don't even bother calling init or implementing init as it returns an error at any time.
I have two init functions in my UIViewController subclass:
- (id)init
{
self = [super init];
if (self)
{
// Custom stuff
return self;
}
return nil;
}
and
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
self = [super initWithNibName: nibNameOrNil
bundle: nibBundleOrNil];
if (self)
{
// Custom stuff
}
return self;
}
I put the init function in to avoid the call to the initWithNibName:bundle: method. I am trying to experiment with taking the xib file out. Unfortunately, calling this init [[Myclass alloc] init] calls initWithNibName:bundle: through the call to [super init].
First, where in the documentation should I be reading so that I would have expected the call to the parent init method to call my own initWithNibName:bundle: method?
Second, how is this a good design choice on Apple's part. I am not seeing why this is desirable behavior? (It may be that I am just not getting the big picture here so please feel free to clue me in.)
Third, how do I get around it best. Do I just take the initWithNibName:bundle: out of my code? Is there never a case where I would like the option of using either a xib or a manual instantiation of the class.
Usually I have to initialise my view controllers with managed object context. I implement simple -(id)initWithContext: method in which I call super's initWithNibName:bundle: method. This way I can define my own xib name.
Not sure about the first part of you question (the reading thing, that is), but Apple's class templates for VC's show that they have their own initWithNibName:bundle method which calls on super with same parameters as they are given. Hence from your situation I'd say that exactly this is designated initialiser and it's not "safe" to call simple init method on super as it will invoke initWithNibName:bundle. I believe UIViewController's init looks like this:
- (id)init
{
self = [self initWithNibName:nibNameDerivedFromClass bundle:probablyNilOrMainBundle];
if (!self) return nil;
// some extra initialization
return self;
}
Since the super class doesn't have initWithNibName:bundle it has to call method on itself making it the designated initialiser. Since you have overridden it, ObjC's runtime replaces self in that method with your class.
If you want to exclude Interface Builder from the creation of your UIViewController's GUI, you have to override loadView and create the view yourself. Don't implement initWithNibName:bundle:.
- (void)loadView {
// Init a view. The frame will be automatically set by the view controller.
UIView *view = [[UIView alloc] initWithFrame:CGRectZero];
// Add additional views (buttons, sliders etc.) to your view here.
// Set the view controller's view to the new view.
self.view = view.
}
First, where in the documentation should I be reading so that I would
have expected the call to the parent init class to call my own
initWithNibName:bundle: method?
You don't, it's a design thing. init is not the base initialization method for all classes
Second, how is this a good design choice on Apple's part. I am not
seeing why this is desirable behavior? (It may be that I am just not
getting the big picture here so please feel free to clue me in.)
When init calls that, it sends nil name and bundle, and it defaults to an empty xib file. There's always a xib file, yours or not.
Third, how do I get around it best. Do I just take the
initWithNibName:bundle: out of my code? Is there never a case where I
would like the option of using either a xib or a manual instantiation
of the class.
You don't. You don't really need to have that code there if you are just calling super, merely forwarding a method.
You can customize your view and add subviews in the viewDidLoad method. In this method you can check whether the class was created using init or using initWithNibName:bundle: by examining property nibName. When using init, nibName will be nil.
- (void)viewDidLoad
{
[super viewDidLoad];
if (!self.nibName) {
// View was not loaded from nib - setup view
}
}
I am fiddling with the new UICollectionView and the UICollectionViewLayout classes. I have created a custom layout, subclassing UICollectionViewFlowLayout.
My cell sizes are changing dynamically and I set the item sizes using the delegate method below
- (CGSize)collectionView:(UICollectionView *)collectionView
layout:(UICollectionViewLayout*)collectionViewLayout
sizeForItemAtIndexPath:(NSIndexPath *)indexPath {
NSLog(#"SETTING SIZE FOR ITEM AT INDEX %d", indexPath.row);
return CGSizeMake(80, 80);
}
Now, under the prepareLayout method of my custom UICollectionViewFlowLayout class, I need to access these size variables so that I can make calculations how to place them and cache them for layoutAttributesForItemAtIndexPath.
However, I can't seem to find any property under UICollectionView or UICollectionViewFlowLayout to reach the custom item sizes I set in the delegate method.
Found it myself.
Implement the custom class like without omitting UICollectionViewDelegateFlowLayout
#interface SECollectionViewCustomLayout : UICollectionViewFlowLayout
<UICollectionViewDelegateFlowLayout>
and then you can call
CGSize size = [self collectionView:self.collectionView
layout:self
sizeForItemAtIndexPath:indexPath];
Looking at the various UICollectionView... header files, and watching the WWDC 2012 Session 219 - Advanced Collection Views and Building Custom Layouts video (from about 6:50 onwards), it seems the extensible delegate pattern takes advantage of dynamic typing to ensure the layout can properly access its extended delegate methods.
In short...
If you define a custom layout with its own delegate, define that delegate protocol in the layout's header file.
Your delegate object (typically the UI(Collection)ViewController that manages the collection view) should declare itself to support this custom protocol.
In the case that your layout is just a UICollectionViewFlowLayout or subclass thereof, this just means declaring conformance to UICollectionViewDelegateFlowLayout.
Feel free to do this in your class extension in the .m file if you'd rather not #import the layout header into the delegate's interface.
To access the delegate methods from the layout, call through to the collection view's delegate.
Use the layout's collectionView property, and cast the delegate to an object conforming to the required protocol to convince the compiler.
Don't forget to check that the delegate respondsToSelector: as usual prior to calling optional delegate methods. In fact, if you like, there's no harm in doing this for all methods, as the typecasting means there is no runtime guarantee the delegate will even implement the required methods.
In code...
So if you implement a custom layout that requires a delegate for some of its information, your header might look something like this:
#protocol CollectionViewDelegateCustomLayout <UICollectionViewDelegate>
- (BOOL)collectionView:(UICollectionView *)collectionView
layout:(UICollectionViewLayout *)layout
shouldDoSomethingMindblowingAtIndexPath:(NSIndexPath *)indexPath;
#end
#interface CustomLayout : UICollectionViewLayout
// ...
#end
Your delegate declares conformance (I've done so in the implementation file here):
#import "CustomLayout.h"
#interface MyCollectionViewController () <CollectionViewDelegateCustomLayout>
#end
#implementation
// ...
- (BOOL)collectionView:(UICollectionView *)collectionView
layout:(UICollectionViewLayout *)layout
shouldDoSomethingMindblowingAtIndexPath:(NSIndexPath *)indexPath
{
return [self canDoSomethingMindblowing];
}
// ...
#end
And in your layout's implementation, you access the method like this:
BOOL blowMind;
if ([self.collectionView.delegate respondsToSelector:#selecor(collectionView:layout:shouldDoSomethingMindblowingAtIndexPath:)]) {
blowMind = [(id<CollectionViewDelegateCustomLayout>)self.collectionView.delegate collectionView:self.collectionView
layout:self
shouldDoSomethingMindblowingAtIndexPath:indexPath];
} else {
// Perhaps the layout also has a property for this, if the delegate
// doesn't support dynamic layout properties...?
// blowMind = self.blowMind;
}
Note that it's safe to typecast here, as we're checking the delegate responds to that method beforehand anyway.
The evidence...
It's only speculation, but I suspect it is how Apple manages the UICollectionViewDelegateFlowLayout protocol.
There is no delegate property on the flow layout, so calls must go via the collection view's delegate.
UICollectionViewController does not publicly conform to extended flow layout delegate (and I doubt it does so in another private header).
UICollectionView's delegate property only declares conformance to the 'base' UICollectionViewDelegate protocol. Again, I doubt there is a private subclass/category of UICollectionView in use by the flow layout to prevent the need for typecasting. To add further weight to this point, Apple discourages subclassing UICollectionView at all in the docs (Collection View Programming Guide for iOS: Creating Custom Layouts):
Avoid subclassing UICollectionView. The collection view has little or no appearance of its own. Instead, it pulls all of its views from your data source object and all of the layout-related information from the layout object.
So there we go. Not complicated, but worth knowing how to do it paradigm-friendly way.
There is a swift version:
self.collectionView(self.collectionView, layout: self.collectionView.collectionViewLayout, sizeForItemAtIndexPath: indexPath)
Check out UICollectionView-FlowLayout on GitHub. Same idea, this just makes accessing the extended delegate methods of flowLayout a little cleaner.
For the later readers, IOS 7 has UICollectionViewFlowLayout which has defined it.
In my case everything about layout, cell layout etc. is being defined inside nib for UIViewController and separate nib for UICollectionViewCell. MyCollectionViewCell contains UIImageView with autolayout to cell with padding/margins but square-shaped.
I need round icons instead squared but don't want to take care which nib I use for iPhone or for iPad (I have separate nibs for devices and for orientation as well).
I don't want to implement #selector(collectionView:layout:sizeForItemAtIndexPath:) into my view controller.
So, inside collectionView:cellForItemAtIndexPath:
I can just use
CGSize size = cell.imageView.bounds.size;
cell.imageView.layer.masksToBounds = YES;
cell.imageView.layer.cornerRadius = size.height/2.0;
Because collectionView:layout:sizeForItemAtIndexPath: call before collectionView:cellForItemAtIndexPath: and layout done.
You can check round avatars on the bottom
I have setup tab bar controller using interface builder, and each tab bar item is linked to a view controller (4 tabs, 4 view controllers). I want to know if Interface Builder uses an -init method to initialize the view controller because apparently this method does not get called:
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil;
... and I want to do some initializations. I can't add that to -viewDidLoad since it is recalled in case of memory warning. Any idea?
Objects loaded from a *.(nib|xib) are inited with:
- (id)initWithCoder:(NSCoder *)inCoder;
So you could override that or if doing your setup after -initWithCoder: is called is not a problem you could use:
- (void)awakeFromNib;
from the NSNibAwaking protocol.
I was also going to mention initWithCoder vs awakeFromNib.
In general, I override initWithCoder when allocating memory for the object or setting values. When you need to do some setup after the IBOutlets are connected, then override awakeFromNib. Until then, IBOutlet instance variables to other views and controls are not connected.
Sounds like you want to implement -(void) awakeFromNib.
NSNibAwaking Protocol Reference (requires ADC login)
I have declared a delegate for my cocoa application here :
MyAppDelegate.h
#interface MyAppDelegate : NSApplication {
}
- (void) applicationDidFinishLaunching:(NSNotification*) notice ;
#end
MyAppDelegate.m
#implementation MyAppDelegate
- (void) applicationDidFinishLaunching:(NSNotification*) notice {
NSLog(#"inside appdidfinishlaunching") ;
}
#end
I have linked the delegate outlet of File Owner to this object in IB.
Yet, this method is not getting called. I don't see any log messages from it.
Can you please suggest what is wrong ?
Your application delegate is not an application itself. It should inherit from NSObject, not NSApplication.
Why that matters
NSApplication is a singleton. Its init method always returns the first instance of NSApplication or any subclass, throwing away any subsequent objects you (or the nib loader) may be calling init on.
So you ended up setting your application object as its own delegate. The object you intended to make the delegate died in the second call to init, and the application object took its place.
Changing the application object to be an instance of your subclass would also have worked, but you'd still have the application as its own delegate, which is unclean and possibly dangerous (NSApplication may privately implement some of its delegate methods itself, as they're just notification handler methods). The only correct solution is to make your app delegate class not inherit from NSApplication.