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)
Related
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?
I have a viewController with 4 buttons (HomePage) and then a TabBarController with 3 viewControllers.
One of the TabBarController's viewControllers I want to be used as a way to get back to the "HomePage" via a tabBar icon. I have associated a custom class that I created called "HomeViewController" to that viewController. See diagram below
HomeViewController .H file.
I have created a protocol with a method "returnToHomepage"
HomeViewController .M file
As soon as the view is loaded it calls the delegate.
In my HomepageViewController .H file I have made sure that the file adheres the protocol.
HomepageViewController .M file
I instantiate an instance of HomeViewController and set delegate to self but
returnToHomePage method never gets called! Not sure what I'm missing...
I think that your're calling the delegate method before the delegate is set.
When you call alloc-init on the controller, it initializes and ViewDidLoad is called,... and THEN you set the delegate... so this
[self.delegate returnToHomepage];
is called before
homeVC.delegate = self;
The HomeViewController you're creating in viewDidLoad is not the same one that's actually being presented onscreen. You'll need to access it with your UITabBarController's viewControllers method and set it's delegate that way.
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 often see this line of code
[super awakeFromNib]
in the awakeFromNib method in the implementation of a view controller
My understanding is it is telling the super class of this view controller (which would be the window) to awakeFromNib.
Am I right? If so, why do we have to tell the window to awake in the awakeFromNib method of a UIView Controller sub-class?
My understanding is it is telling the super class of this view controller ...
right so far...
(which would be the window)
oops - that's the source of your confusion.
The "super class of the view controller" is UIViewController. "super" is referring to the base class that your UIViewController sub-class inherits from; it doesn't have anything to do with the window that encloses your view.
So, what this is doing is invoking the default awakeFromNib implementation of a basic UIViewController, in addition to whatever you're doing in your sub-class implementation.
What David said above is correct,
Now for your question "why do we have to tell the window to awake in the awakeFromNib method of a UIView Controller sub-class?"
if there is any custom modification or any data that you want to load before your ViewController loads we should use the awakeFromNib.
awakeFromNib is called when the controller itself is unarchived from a nib.
This should be straight forward for a guru. I don't have any code really written out, just a couple of controllers and a custom UIView. All connected through nibs. The app loads without crashing, yet I can't see my NSLog() hit from my custom UIView.
My application delegate has default template code which calls for a class of mine called TabAnimationController. TabAnimationViewController has its view set to TabView. I made sure that in TabAnimationViewController's NIB that File's owner is set to TabAnimationViewController and that my instance of UIView has its class set to TabView.
In TabView.m I'm trying to see how NSLog is going to hit, and it's not showing up at all.
- (void)loadView {
NSLog(#"calling loadView");
}
- (id)initWithFrame:(CGRect)frame {
NSLog(#"Calling initWithFrame:");
return self;
}
Strange. I'm not sure why even after proper IB connections that my NSLog will not show up. Only anything put into drawRect: will invoke. Why isn't initWithFrame or loadView ever get hit? What if I want to customize this view programmatically?
First of all, when a view is dehydrated from nib file, instead of initWithFrame, initWithCoder is invoked. So you need to implement your initialization in initWithCoder as well. (It may be a good idea to keep the initWithFrame initialization as well, if you anticipate programmatically creating your TabView instead of hooking up in the IB. Just refactor your initialization to another method and call it from both implementations.)
Also in your initialization code above you must always call the super class's initialization. There is a boiler plate pattern all custom classes use in their init implementation for that:
if (self = [super initXXX]) { do your initialization }
return self;
Second, loadView which is actually a UIViewController method and not a UIView method is invoked only if the view outlet of the controller is nil.
Unless you are composing your view yourself programmatically using your controller, you do not need to override loadView. Instead you should override viewDidLoad, which is called after the view is loaded, to do additional initialization.
The simplest way to get this up and running is simply to use the "View based Application" template when you create a new project. It sets up everything you need to start with.
But, in short, you're looking at the wrong methods. First, you shouldn't override loadView unless you're creating your view programatically. If it's loading from a XIB file look at the initWithNibName method.
You might also want to look at the viewDidLoad, viewWillAppear and viewDidAppear methods that are triggered, well, it's fairly obvious when!