I am stumbling over what I believe is probably a fundamental misunderstanding of how classes work in Objective-C. I am using Storyboards but in this app I wanted to create a simple custom date picker view for a textfield on one of my view controllers. However, I seem to be having a problem accessing any of the properties of the date picker class from my view controller:
First I modeled my CustomDatePicker.xib in IB as follows:
I have created a custom class by sublcassing UIView as follows:
CustomPicker.h
#interface CustomPickerView : UIView
#property (strong, nonatomic) IBOutlet UIDatePicker* datePicker;
#end
CustomerPicker.m
#implementation CustomPickerView
- (id)init
{
self = [super initWithFrame:CGRectMake(0, 0, 320, 258)];
if (self) {
[self addSubview:[[[NSBundle mainBundle] loadNibNamed:#"CustomPickerView" owner:self options:nil] objectAtIndex:0]];
}
return self;
}
In my ViewController:
- (void)viewDidLoad
{
[super viewDidLoad];
self.customPickerView=[[CustomPickerView alloc] init];
// THE MAIN ISSUE IS...
// Following line has no effect on how picker is presented ???
self.customPickerView.datePicker.datePickerMode=UIDatePickerModeTime;
self.dateField.inputView=self.customPickerView;
}
When the textfield is tapped, my CustomDatePicker pops up fine. However, I can't seem to set the .datePickerMode either from the viewDidLoad method of my ViewController. The only way I can change the .datePickerMode is through IB and of course that's not going to work at run-time.
I have wired up the outlet in IB and am able to access the datePicker.date from within the class but not the ViewController.
I have researched and viewed a number of ways to implement this concept. My question isn't how to implement a CustomDatePicker it is "Why can't I access properties of my CustomDatePicker from the ViewController that instantiated it?
I successfully changed the datePickerMode property when I loaded the NIB like this:
- (void)viewDidLoad
{
[super viewDidLoad];
NSArray *nibArray = [[NSBundle mainBundle] loadNibNamed:#"CustomPickerView" owner:self options:nil];
self.customPickerView = (CustomPickerView *)[nibArray objectAtIndex:0];
self.customPickerView.datePicker.datePickerMode = UIDatePickerModeTime;
self.dateField.inputView = self.customPickerView;
}
And you can probably remove your custom view's init method altogether. I've never seen a NIB loaded in the init method like you're doing. While it might be possible, I believe that's what is causing your problem. Loading the NIB in viewDidLoad of the ViewController and setting it directly seems more straightforward.
Please see solution here at:
Objective-C Custom Class Actions and Outlets
My goal is, to have a subclassed UIView (lets call it infoView) designed in his own XIB so that I can present it in many UIViewController's.
The Problem:
So far, when I was adding UIView's to a UIViewController I always had to make an UIViewController the file's owner of the UIView's .xib file to load the view with something like:
...
//this is inside the calling UIViewController's method
// InfoView *infoView is ivar and a subclass of UIView
infoView = nil;
NSArray *bundle = [[NSBundle mainBundle] loadNibNamed:#"InfoView"
owner:self options:nil];
for (id object in bundle) {
if ([object isKindOfClass:[InfoView class]])
infoView = (InfoView *)object;
}
[[self view] addSubview:infoView];
...
But I want to use the same UIView in many different UIViewController's, so I actually don't want a file's owner except maybe the class itself. In ThomasM's question he was setting the UIView itself to be the file's owner but without success.
In the answers there I found a solution to set the file's owner to nil. To do so I had to add all calling UIViewController objects from the Interface Builder object library to the InfoView.xib file and connect them with their infoView outlets.
But this doesn't feel right. So here I would like to collect solutions to
encapsulate a UIView together with his xib-file to use it in many different view controllers. How do you guys handle that?
Thx for any help.
EDIT:
The infoView is something like an overlay which appears when the user presses a button on one of the view controllers. It's NOT the View controllers "main" view. It gives detailed informations about the view of his superviews view controller and will disappear afterwards. I only fill the infoView with different contents threw out all the calling view controllers.
Like Hollance answer was pointing out I am using UINib.
To use it, leave the .xib files owner nil and place all customization of the infoView inside the initWithCoder: method of your InfoView class implementation. This will get called if you obtain the InfoView.xib like:
// here InfoView is the name of the .xib file
UINib *infoNib = [UINib nibWithNibName:#"InfoView" bundle:nil];
NSArray *topLevelObjects = [infoNib instantiateWithOwner:self options:nil];
QInfoView *infoView = [topLevelObjects objectAtIndex:0];
So you want to load a UIView from a nib that you wish to use in more than one UIViewController, and you want to connect it to an outlet on each of those view controllers. Is that correct?
Then make a UIViewController subclass (let's call it FakeViewController) with an IBOutlet property. Set that FakeViewController as the nib's File's Owner and connect your UIView to its outlet.
Done.
You just need to make sure all your other view controllers also have these outlet properties (although they don't need to be IBOutlets), but the nib loader doesn't actually check to make sure the class that you pass into the owner parameter equals the class name you specified in Interface Builder. So you can fake it.
Oh, and if you're OS 4.0 and higher, use UINib to load the nib file.
And yet another way is to create your own "controller" based on NSObject to define your own life-circle (instead of standard UIViewController life-circle).
For example:
BaseSubview.h:
#interface BaseSubview : NSObject {
UIView* _view;
}
#property (nonatomic, retain) IBOutlet UIView* view;
- (void)myMethod;
#end
BaseSubview.m:
#import "BaseSubview.h"
#implementation BaseSubview
#synthesize view = _view;
- (id)init
{
self = [super init];
if (self) {
// ...
}
return self;
}
- (void)dealloc
{
[_view removeFromSuperView];
[super dealloc];
}
- (void)myMethod
{
// view specific logic here
_view.backgroundColor = [UIColor redColor];
}
#end
InfoView.h:
#import "BaseSubview"
#interface InfoView : BaseSubview {
UILabel* _labelInfo;
}
#property (nonatomic, retain) IBOutlet UILabel* labelInfo;
#end
InfoView.m:
#import "InfoView.h"
#implementation InfoView
#synthesize labelInfo = _labelInfo;
- (id)init
{
self = [super init];
if (self) {
// ...
}
return self;
}
- (void)myMethod
{
// view specific logic here
_labelInfo.text = #"current time...";
[super myMethod];
}
#end
InfoView.xib:
file owner is InfoView
assign of outlets as usual
view is parent all other controls (such as labels, etc)
HugeAndComplicatedViewController.h:
// ...
// among other var definitions
InfoView* _infoView;
// ...
HugeAndComplicatedViewController.m, most interesting part:
// when you decide to show your view
// probably in loadView
_infoView = [[InfoView alloc] init];
[[NSBundle mainBundle] loadNibNamed:#"InfoView" owner:_infoView options:nil];
[self.view addSubview:_infoView.view];
// possibly perform specific logic
[_infoView myMethod];
// no need sub-view any more
// probably in dealloc
[_infoView release];
So now you have your own sub-view with logic and design separated from "Huge & Complicated" view-controller. It can have any life-circle you need for your current project.
does infoView need to be a subview?
in your viewController:
-(id) init {
self = [super initWithNibName:#"myNib" bundle:nil];
if (self) {
// code here
}
}
The idea basically is:
Inside of myViewController NIB I have a UIView as a subview, with connections to an IBOutlet on MyViewController class. I want to load the UIView subview from a NIB also. The subView also has a class associated with it. But the subview is not showing. Here is what I do
on MyViewController.h
#interface MyViewController : UIViewController {
IBOutlet SubView *subView; // this outlet is connected to an empty UIView on IB
}
#end
on MyViewController.m
-(id)init {
if ((self = [super init])) {
// load the main view
[[NSBundle mainBundle] loadNibNamed:#"myViewController" owner:self options:nil];
// load the sub view from another NIB
self.subView = [[[NSBundle mainBundle] loadNibNamed:#"subView" owner:self options:nil] objectAtIndex:0];
}
}
the SubView.h is defined as:
#interface SubView : UIView {
}
#end
What am I doing wrong?
What you need to do is set your file owner to be a UIViewController, hook up your view to the view property of the file owner, save your nib. In your code, load your nib like this:
UIViewController* c = [[UIViewController alloc] initWithNibName:#"Foo" bundle:nil];
SubView* subView = [c view];
Should be off to the races.
One final note, is that the type of subView should be the same as whatever is defined as the class name in interface builder. If it's a SubView there, then it's safe to declare it as a SubView* in code.
GianPac - shameless self-promotion, but I did a blog post about this about a month ago. Feel free to ignore the drop-shadow related code.
http://nathanhjones.com/2011/02/20/creating-reusable-uiviews-with-a-drop-shadow-tutorial/
Also, I (thanks to advice from several people) avoid ever having more than one view controller on my 'view'. The method I outline above allows you to accomplish this and still use IB to do the layout/outlets.
After a litte hunting through the docs I have decided on the following technique using "viewWithTag" to extract sub views from the main root view returned by loadNibNamed:owner:options: To start with I was a little confused as I assumed loadNibNamed:owner:options would return an array containing all my UI items (i.e. labels, buttons etc.) when in fact it seems to return the root view (or root views). Can anyone confirm that I am doing this right, I am just curious if there are other (maybe better) ways to extract and set items from within a xib file?
- (void)loadView {
NSLog(#"HYBRID UI: %s", __PRETTY_FUNCTION__);
NSArray *nibArchive = [[NSBundle mainBundle] loadNibNamed:#"Interface" owner:self options:nil];
NSLog(#"SIZE: %d ARRAY: %#", [nibArchive count], nibArchive);
// UIView
UIView *nibView = [nibArchive lastObject];
[nibView setFrame:[[UIScreen mainScreen] applicationFrame]];
NSLog(#"FRAME: %#", NSStringFromCGRect([nibView frame]));
[self setView: nibView];
// UILabel
UILabel *nibLabel = (UILabel *)[nibView viewWithTag:101];
[nibLabel setText:#"FRINGE"];
[nibLabel setTextColor:[UIColor whiteColor]];
// UIImageView
UIImageView *nibImageView = (UIImageView *)[nibView viewWithTag:102];
[nibImageView setBackgroundColor:[UIColor whiteColor]];
}
cheers gary
loadNibNamed:owner:options: does return an array of all root objects in the NIB. To get to other objects in the hierarchy, you can traverse the hierarchy manually (not a good idea in most cases), use tags as you did or use outlets.
If you declare outlets in your view controller for the view, the label and the image view and connect them in the NIB to the appropriate objects, they will be automatically connected in loadNibNamed:owner:options:.
PS: Why do you load the NIB manually anyway? Can't you init your view controller with initWithNibName:bundle and do the rest of the initialization in viewDidLoad?
It seems like you're making this harder than it needs to be. If you're subclassing UIViewController, just call [super initWithNibName:#"whatever" bundle:nil] in your init, then you won't have to call loadNibNamed:owner:options yourself. And since you're using a nib, you should put your code in viewDidLoad without implementing loadView.
In your interface, set up an IBOutlet for the views you are interested in:
#property (nonatomic, retain) IBOutlet UILabel *titleLabel;
In the nib, set the File's Owner class to your UIViewController subclass, and then you can connect the titleLabel to the actual UILabel.
I am new to interface builder and I would like to have a screen which contains a 3x3 grid of a UIView each of which contain a UIImageView, and 4 UILabels.
My logic is probably flawed but the way I am trying to achieve this was to:
Create a UIView Nib file MyUIView.nib and layout in IB with an imageView and 4 labels
Create a UIView subclass called MyUIView.m which contains 1 IBOutlet UIImageView and 4 IBOutlet UILabels. Link the MyUIView.nib and MyUIView.m as files owner and connect the outlets.
Then create another nib MyGridViewController.nib which has 9 MyUIView in it laid out in the 3x3 grid.
Create a UIViewController which has 9 IBOutlet MyUIView and connect them via Interface Builder.
Is it possible to load a nib into another nib graphically from within InterfaceBuilder, if so how do I do it? Do I drag a "standard" UIView onto the canvas and then change the class to be a MyUIView?
Or do I need to do this all programmatically within the MyGridViewController.m with something like:
for (int i=0; i<9; i++)
{
NSArray* nibViews = [[NSBundle mainBundle] loadNibNamed:#"MyUIView" owner:self options:nil];
[myViewArray addObject:[ nibViews objectAtIndex: 1]];
}
The only other way I have gotten this to work was to have a single Nib and put 9 UIImageViews and 36 UILabels but this obviously is a pain when I want to change something as I need to update each one of the 3x3 "cells". I thought it would be easier to change it in one file and all 9 would be updated.
You cannot do it in Interface Builder.
What I would probably do is to make a custom view, say MyGridItem, that loads MyUIView.nib as a subview when it awakes, and then use 9 of them in your MyGridView.nib.
Be careful with awakeFromNib, as it can be called twice if the view is involved in the loading of two different nibs (eg, if MyGridItem is the owner when loading MyGridView.nib, then MyGridItem awakeFromNib will be called once when it is loaded as part of loading MyUIView.nib, and once when it loads the MyGridView.nib.
Also, since you're loading the nib 9 times, you may want to cache the nib once using
NSNib* theNib = [[NSNib alloc] initWithNibNamed:#"MyGridItem" bundle:nil];
load it with:
if ( [theNib instantiateNibWithOwner:self topLevelObjects:NULL] ) {
You then may want to deallocate it after you've loaded all nine subviews.
"Yes you (almost) can."
I do it in my projects using Interface Builder.
The only flaw is that you see a white area to represent the 'nested nibs' in Interface Builder. Let's say that, in the mean time (I main waiting for Apple to add this feature in XCode), the solution I present here is acceptable.
First read this: https://blog.compeople.eu/apps/?p=142
Then, if you do ARC, follow these instructions and grab my UIVIew+Util category included here.
For ARC, you will have to allow this 'self' assignation. (https://blog.compeople.eu/apps/?p=142 state that it's not needed, but it is. If you do not, you will get some 'messages send to deallocated instance')
To achieve this in an ARC project, add the '-fno-objc-arc' flag compiler setting on your file.
Then do NO-ARC coding in this file (like dealloc setting nils, calling super dealloc, etc..)
Also, client nib's viewcontroller should use strong property to hold the instance returned by awakeFromNib. In the case of my sample code, the customView is referenced like this:
#property (strong, nonatomic) IBOutlet CustomView* customView;
I finally added some other improvements to properties handling and nib loading using copyUIPropertiesTo: and loadNibNamed defined in my UIView+Util category.
So awakeAfterUsingCoder: code is now
#import "UIView+Util.h"
...
- (id) awakeAfterUsingCoder:(NSCoder*)aDecoder
{
// are we loading an empty “placeholder” or the real thing?
BOOL theThingThatGotLoadedWasJustAPlaceholder = ([[self subviews] count] == 0);
if (theThingThatGotLoadedWasJustAPlaceholder)
{
CustomView* customView = (id) [CustomView loadInstanceFromNib];
// copy all UI properties from self to new view!
// if not, property that were set using Interface buider are lost!
[self copyUIPropertiesTo:customView];
[self release];
// need retain to avoid deallocation
self = [customView retain];
}
return self;
}
The UIView+Util category code is
#interface UIView (Util)
+(UIView*) loadInstanceFromNib;
-(void) copyUIPropertiesTo:(UIView *)view;
#end
along with its implementation
#import "UIView+Util.h"
#import "Log.h"
#implementation UIView (Util)
+(UIView*) loadInstanceFromNib
{
UIView *result = nil;
NSArray* elements = [[NSBundle mainBundle] loadNibNamed: NSStringFromClass([self class]) owner: nil options: nil];
for (id anObject in elements)
{
if ([anObject isKindOfClass:[self class]])
{
result = anObject;
break;
}
}
return result;
}
-(void) copyUIPropertiesTo:(UIView *)view
{
// reflection did not work to get those lists, so I hardcoded them
// any suggestions are welcome here
NSArray *properties =
[NSArray arrayWithObjects: #"frame",#"bounds", #"center", #"transform", #"contentScaleFactor", #"multipleTouchEnabled", #"exclusiveTouch", #"autoresizesSubviews", #"autoresizingMask", #"clipsToBounds", #"backgroundColor", #"alpha", #"opaque", #"clearsContextBeforeDrawing", #"hidden", #"contentMode", #"contentStretch", nil];
// some getters have 'is' prefix
NSArray *getters =
[NSArray arrayWithObjects: #"frame", #"bounds", #"center", #"transform", #"contentScaleFactor", #"isMultipleTouchEnabled", #"isExclusiveTouch", #"autoresizesSubviews", #"autoresizingMask", #"clipsToBounds", #"backgroundColor", #"alpha", #"isOpaque", #"clearsContextBeforeDrawing", #"isHidden", #"contentMode", #"contentStretch", nil];
for (int i=0; i<[properties count]; i++)
{
NSString * propertyName = [properties objectAtIndex:i];
NSString * getter = [getters objectAtIndex:i];
SEL getPropertySelector = NSSelectorFromString(getter);
NSString *setterSelectorName =
[propertyName stringByReplacingCharactersInRange:NSMakeRange(0,1) withString:[[propertyName substringToIndex:1] capitalizedString]];
setterSelectorName = [NSString stringWithFormat:#"set%#:", setterSelectorName];
SEL setPropertySelector = NSSelectorFromString(setterSelectorName);
if ([self respondsToSelector:getPropertySelector] && [view respondsToSelector:setPropertySelector])
{
NSObject * propertyValue = [self valueForKey:propertyName];
[view setValue:propertyValue forKey:propertyName];
}
}
}
Tadaaaa :-)
Credits goes to https://stackoverflow.com/users/45018/yang for initial solution. I just improved it.
If you create your view with the 1 image view and 4 labels in a nib (I'll call this view the "grid cell"), you can create another nib for your grid and drag in 9 instances of your grid cell. The 9 instances will only show up as placeholders (blank views) in IB, but they'll work when you run the app.
This article tells you how to do it. Check out the section called "A reusable subview". And since you're making a grid with 9 identical cells, it might make sense to use AQGridView -- see the section "A reusable AQGridViewCell".
AFAIK, there's no way to load a NIB within a NIB. What I would do, in your case, is add the UILabels programmatically in MyUIView.
Here's a slightly more dynamic way to do something like this.
I didn't really want to be pulling in the nibs and sifting through their objects wherever I wanted to use them in code, seemed messy. Also, I wanted to be able to add them in interface builder.
1) Create your custom subclass of UIView (lets call it myView)
2) Create your nib and call it myViewNIB
Add these two methods (would be smarter to have them in a superclass UIView and subclass that)
- (UINib *)nib {
NSBundle *classBundle = [NSBundle bundleForClass:[self class]];
return [UINib nibWithNibName:[self nibName] bundle:classBundle];
}
- (NSString *)nibName {
NSString *className = NSStringFromClass([self class]);
return [NSString stringWithFormat:#"%#NIB", className];
}
The so-called magic is that last line there where it returns a nibName by appending NIB to the current class.
Then for your init method, which is initWithCoder since you want to use this in interface builder (you could make it more robust so it can be used programmatically too by also setting up initWithFrame):
- (id)initWithCoder:(NSCoder *)aDecoder
{
self = [super initWithCoder:aDecoder];
if (self) {
// Initialization code
NSArray *nibObjects = [[self nib] instantiateWithOwner:nil options:nil];
UIView *view = [nibObjects objectAtIndex:0];
[self addSubview:view];
}
return self;
}
So this requires that myViewNIB's first (and probably only) member is a view. What this won't give you is a way to set things like labels programmatically inside the nib, at least not easily. Without looping through the views and looking for tags I'm not sure how else you'd do that.
Anyway. At this point you can drag out a new UIView in IB, and set the class to myView. myView will automatically search for our associated myViewNIB.xib and add its view as a subview.
Seems there should be a better way to do this.
What I've done, is:
1) For the owning view, I put in the subview as an empty view with the class name of the custom view as a placeholder. This will create the custom view, but none of its subviews or connections will be made as these are defined in the custom view's nib.
2) I then manually load a new copy of the custom view using its nib and replace the placeholder custom view in the parent.
Use:
NSView *newView = (Code to load the view using a nib.)
newView.frame = origView.frame;
NSView *superView = origView.superview;
[superView replaceSubview:origView with:newView];
Ugh!