Subclassing and overriding a UIView created with a nib file - uiview

Let's say I've created a subclass of UIView and I'm loading it with a nib file.
I do this:
MySubView.m
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
NSArray *nib = [[NSBundle mainBundle] loadNibNamed:#"MySubView" owner:self options:nil];
[self release];
self = [[nib objectAtIndex:0] retain];
self.tag = 1;
[self fire];
}
return self;
}
- (void)fire {
NSLog(#"Fired MySubView");
}
Now I want to create some variations, but I don't want to copy the nib file, so I try to subclass MySubView like this, changing the background color:
RedMySubView.m
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
self.backgroundColor = [UIColor redColor];
[self fire];
}
return self;
}
- (void)fire {
NSLog(#"Fired RedMySubView");
}
The view is created, the background color is changed, but the fire action is not overridden by the subclass. If I call the fire method, the result is Fired MySubView in the console.
How can I resolve this?
I want to keep the nib layout, but give it a new class.

I would say that with [self release] in the MySubview initializer initWithFrame you are throwing away the class you want to create with the initializer. The class is loaded by the loadNibName method and therefore has the same class as defined in the nib.
So it is useless to call the initializer in the subclass.
Try to implement your own nib constructor in MySubview (e.g. initWithNibFile):
- (id) initWithNibFile:(NSString *) nibName withFrame:(CGRect) frame
etc. and call this constructor in RedMySubview
- (id) initWithNibFile:(NSString *) nibName withFrame:(CGRect) frame {
self = [super initWithNibFile:mynib withFrame:MyCGRect];
if (self)
....
If you now look up that your nib file really has RedMySubview as class, fire should be
overridable. If you use both MySubview and RedMySubview, you must duplicate the xib.
Or you create an abstract class (a stub) which implements only the initWithNibFile initializer and the UIViews you want to create are subclasses of it:
MyAbstractNibUIView initWithNibFile:withFrame:
MyRedSubview : MyAbstractNibUIView red.xib
MyGreenSubview :MyAbstractNibUIView green.xib
MyBlueSubview : MyAbstractNibUIView blue.xib

When you call self = [[nib objectAtIndex:0] retain] you basically override your "self" object to become a MySubView, since a MySubView is the base object in the nib file. This is undesired because if the calling class is a RedMySubView, then it will be overridden into a MySubView.
Instead you want to change your - (id)initWithFrame:(CGRect)frame in MySubView into this:
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
NSArray *nib = [[NSBundle mainBundle] loadNibNamed:#"MySubview" owner:self options:nil];
// The base of the nib file. Don't set self to this, instead copy all of its
// subviews, and "self" will always be the class you intend it to be.
UIView *baseView = [nib objectAtIndex:0];
// Add all the subviews of the base file to your "MySubview". This
// will make it so that any subclass of MySubview will keep its properties.
for (UIView *v in [baseView subviews])
[self addSubview:v];
self.tag = 1;
[self fire];
}
return self;
}
Now, everything should work in the initializer of "MyRedSubView", except that fire will fire twice, since you call it both in MySubView and RedMySubView.

Related

Delegate is being released in case of custom View created with XIB

I have created MyCustomView.xib/h/m: which extends UIView class. Then in my Main.storyboard, put UIView object, changed the class to MyCustomView and linked to MainController.h. So, MainController contains reference to MyCustomView instance.
For loading from xib, in MyCustomView I do the following:
- (instancetype)initWithCoder:(NSCoder *)aDecoder
{
self = [super initWithCoder:aDecoder];
if (self) {
if (self.subviews.count == 0) {
[self commonInit];
}
}
return self;
}
- (instancetype)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
[self commonInit];
}
return self;
}
- (void) stretchToSuperView:(UIView*)view
{
view.translatesAutoresizingMaskIntoConstraints = NO;
NSDictionary *bindings = NSDictionaryOfVariableBindings(view);
NSString *formatTemplate = #"%#:|[view]|";
for (NSString * axis in #[#"H",#"V"]) {
NSString * format = [NSString stringWithFormat:formatTemplate,axis];
NSArray * constraints = [NSLayoutConstraint constraintsWithVisualFormat:format options:0 metrics:nil views:bindings];
[view.superview addConstraints:constraints];
}
}
- (void)commonInit
{
MyCustomView* view = nil;
NSArray *views = [[NSBundle mainBundle] loadNibNamed:#"MyCustomView" owner:self options:nil];
view = views.firstObject;
[self addSubview:view];
[self stretchToSuperView:views.firstObject];
}
This works quite well, until I want to declare delegate in MyCustomView in order to notify MainController to any change(button click, etc). So, my ManController conforms MyCustomViewDelegate and implements methods.
EDIT 1 setting delegate
//MainViewController.m file
#interface MainViewController ()
#property (strong, nonatomic) IBOutlet MyCustomView *customView;
#end
#implementation MainViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.customView.delegate = self;
}
The problem here is that delegate becomes nil and I don't understand the reason, so don't know what's the mistake.
Edit 2 I think somehow I have 2 different instances of MyCustomView.
I have added new property in MyCustomView:
#interface MyCustomView : UIView
#property (weak, nonatomic) IBOutlet UIButton *firstItem;
#property(nonatomic, weak)id <MyCustomViewDelegate> delegate;
// this is test property
#property(nonatomic, assign)int testProperty;
#end
And when I set this property in viewDidLoad and then click to first button, I see that testProperty has value 0. So, this could mean something wrong with IBOutlet MyCustomView *customView.
You are correct, that you have two view objects. The one that you added to the storyboard and the one you created via loadNibNamed.
Bottom line, loadNibNamed will create the view for you (assuming that you've specified MyCustomView as the base class for the view specified in the NIB; you can leave NIB's "File owner" blank). You can then write a convenience method in MyCustomView to instantiate the NIB-based view:
+ (instancetype)myCustomViewWithDelegate:(id<MyCustomViewDelegate>)delegate {
NSArray *array = [[NSBundle mainBundle] loadNibNamed:#"MyCustomView" owner:delegate options:nil];
MyCustomView *view = array.firstObject;
NSAssert([view isKindOfClass:[MyCustomView class]], #"Base class of NIB's top level view is not MyCustomView");
view.delegate = delegate;
return view;
}
Then, rather than specifying the view on the storyboard, you must instantiate and add it to the view hierarchy programmatically, e.g.:
- (void)viewDidLoad {
[super viewDidLoad];
MyCustomView *view = [MyCustomView myCustomViewWithDelegate:self]; // since `self.myCustomView` should be `weak`, let's hold the view in local variable
[self.view addSubview:view];
self.myCustomView = view; // then set the property after the view is safely added to view hierarchy
NSDictionary *views = #{#"myCustomView" : self.myCustomView};
self.myCustomView.translatesAutoresizingMaskIntoConstraints = false;
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|[myCustomView]|" options:0 metrics:nil views:views]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|[myCustomView]|" options:0 metrics:nil views:views]];
// ...
}
Obviously, when adding a view programmatically, you have to specify its frame and autoresizingMask, or use constraints like shown above.
Unfortunately, I can't make a simple comment, so I have to ask, and give advices as 'answer'.
First of all, I don't see where do you set the delegate to your MainController, but I think in the viewDidLoad() of the controller.
Second thing. It's really important how do you setup your MyCustomView.xib, because you create a brand new object MyCustomView, and its properties will be unavailable from the controller.
MyCustomView* view = nil;
NSArray *views = [[NSBundle mainBundle] loadNibNamed:#"MyCustomView" owner:self options:nil];
view = views.firstObject;
[self addSubview:view];
Without knowing the setup of the xib, and setting method of the delegate, here is my guess what could help you:
In MyCustomView.xib set the class of the rootView to UIView.
In MyCustomView.xib set the class of the File's Owner to MyCustomView
Create your subview as a UIView.
UIView* view = nil;
NSArray *views = [[NSBundle mainBundle] loadNibNamed:#"MyCustomView" owner:self options:nil];
view = views.firstObject;
[self addSubview:view];
Set the delegate of the myCustomView object in your controller.
self.myCustomView.delegate = self;
I hope it will help, if not, then please extend your question with the delegate setter code block, and your xib setup method. (class of file's owner, class of root view etc)

Issue with a custom view in iOS

How is this possible?
PlayingCardView *view = [[PlayingCardView alloc] initWithFrame:currentFrame];
if ([view isKindOfClass:[PlayingCardView class]])
NSLog(#"checked!");
The if below doesn't work. However, it says that my view is of UIView class, not the PlayingCardView, which is inherited from the UIView. The problem is that because of it I can't send PlayingCardView's messages to my view.
update:
- (instancetype) initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
[self setup];
return self;
}
It is possible if you override initWithFrame for PlayingCardView the wrong way. You might be doing something like self = [[UIView alloc] initWithFrame:] instead of self = [super initWithFrame:]. Check any init methods that you have overridden for PlayingCardView.

Why do both init functions get called

If I have a class that is setup like this to customize a UIView.
#interface myView : UIView
- (id)init {
self = [super init];
if(self){
[self foo];
}
return self;
}
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
// Initialization code
[self foo];
}
return self;
}
-(void) foo{
//Build UIView here
}
How come foo is called twice whether I use
myView *aView = [[myView alloc]init]];
or
myView *aView = [[myView alloc]initWithFram:aFrame];
UIView init calls UIView initWithFrame:. Since you override both, calling your init method results in your initWithFrame: method being called:
In other words: your init calls UIView init. UIView init calls initWithFrame:. Since you override initWithFrame:, your initWithFrame: is called which in turn calls UIView initWithFrame:.
The solution, since your initWithFrame: will always be called, is to remove the call to foo from your init method.

Button on iOS is not sending events on touchUpInside

I created a .xib file form my custom view.
I created .h/.m files for that view.
I ctrl dragged from button to header file to create an IBAction and set the value to touchUpInside. Here is what is happening:
http://screencast.com/t/R1WTpK7xp
WTF?
It triggers event when up is outside the button?
EDIT:
Here is the screenshot:
And what is the thing with down vote? I don't see a point in that.
View.h
#import <UIKit/UIKit.h>
#import "DrawingViewDelegate.h"
#interface DrawingBottomToolbarView : UIView
#property (weak) id <DrawingViewDelegate> delegate;
- (IBAction)lineSegmentButtonPush:(id)sender;
#end
View.m
#import "DrawingBottomToolbarView.h"
#implementation DrawingBottomToolbarView
#synthesize delegate;
- (id)initWithFrame:(CGRect)frame
{
NSLog(#"frame");
self = [super initWithFrame:frame];
if (self) {
// Initialization code
[self addSubview:[[[NSBundle mainBundle] loadNibNamed:#"DrawingBottomToolbarView" owner:self options:nil] objectAtIndex:0]];
//[[[NSBundle mainBundle] loadNibNamed:#"DrawingBottomToolbarView" owner:self options:nil] objectAtIndex:0];
//[self addSubview:self.];
}
return self;
}
//-(id)initWithCoder:(NSCoder *)aDecoder{
//
// NSLog(#"coder");
// if ((self = [super initWithCoder:aDecoder])){
// [self addSubview:[[[NSBundle mainBundle] loadNibNamed:#"DrawingBottomToolbarView" owner:self options:nil] objectAtIndex:0]];
// }
// return self;
//}
- (IBAction)lineSegmentButtonPush:(id)sender
{
NSLog(#"line push");
}
#end
I don't get it where is the problem.
EDIT 2:
I tried setting buttons as outlets and add target/action in code and same thing happens:
.h
#property (weak, nonatomic) IBOutlet UIButton *lineSegmentButton;
.m
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
// Initialization code
[self addSubview:[[[NSBundle mainBundle] loadNibNamed:#"DrawingBottomToolbarView" owner:self options:nil] objectAtIndex:0]];
self.currentSelectedPathSegment = NoneSegment;
[self.lineSegmentButton addTarget:self action:#selector(lineSegmentButtonPush:) forControlEvents:UIControlEventTouchUpInside];
}
return self;
}
EDIT 3: Here is where I add two views. drawing view is created in code, bottomToolbar is created from .xib file.
kBottomToolbarHeight is constant with same value as height defined in .xib file.
- (void)viewWillAppear:(BOOL)animated
{
[self.view addSubview:self.drawingView];
[self.view addSubview:self.bottomToolbar];
CGRect selfRect = self.view.bounds;
CGRect drawingViewRect = selfRect;
CGRect bottomToobarRect = selfRect;
drawingViewRect.size.height = selfRect.size.height - kBottomToolbarHeight;
bottomToobarRect.size.height = kBottomToolbarHeight;
bottomToobarRect.origin.y = drawingViewRect.size.height;
self.drawingView.frame = drawingViewRect;
self.bottomToolbar.frame = bottomToobarRect;
}
The behavior you mention and show in the screencast is exactly the same as I experienced when I somewhere in the view hierarchy had a parent view with a UITapGestureRecognizer.
I'm unsure whether or not to flag your question as a possible duplicate of this one which helped me solve my problem.
For reference this is not a problem in iOS 6.0, only earlier versions.

Why is it when Custom UIView loads itself from a NIB but is also embedded in another NIB I get infinite loop

I have a problem with a custom uiview (MyCustomView) loading its view from a NIB (MyCustomView.xib).
The problem is the MyCustomView is reused in other XIBs so awakeFromNib is called.
The CustomView.xib also contains an instance of MyCustomView so loadNibNamed in MyCustomView.awakeFromNib goes into infinite loop
im getting infinite loop as:
SomeVC.xib
+ calls MyCustomView awakeFromNib
++ which calls loadNibNamed: MyCustomView.nib
+++++ but top level View is also instance of MyCustomView
....so awakeFromNib: goes into infinite loop
Im just wondering if when a custom view loads its self from a nib then top level UIView shouldnt be instance of MyCustomView?
If so how do I wire up outlet/actions (do I make MyCustomView the files owner instead?)
anyway cheers
LONG EXPLANATION:
I have a row of buttons which I want to reuse on a number of screens.
So I created a custom view
#interface ScreenMenuButtonsView : UIView
#property (retain, nonatomic) IBOutlet UIButton *buttonHome;
#property (retain, nonatomic) IBOutlet UIButton *buttonMyMusic;
#property (retain, nonatomic) IBOutlet UIButton *buttonStore;
#property (retain, nonatomic) IBOutlet UIButton *buttonSocial;
#property (retain, nonatomic) IBOutlet UIButton *buttonSettings;
#end
I wanted to layout the view in IB so I created a NIB
ScreenMenuButtonsView_iPad.xib
Files Owner: no set NSObject
VIEW: ScreenMenuButtonsView
+ UIButton
+ UIButton
+ UIButton
I want the custom view to load its own NIB internally and I used the example from
http://nathanhjones.com/2011/02/20/creating-reusable-uiviews-with-a-drop-shadow-tutorial/
http://nathanhjones.com/wp-content/uploads/2011/02/ReusableTableHeaders.zip
- (id)initWithFrame:(CGRect)frame {
//self = [super initWithFrame:frame]; // not needed - thanks ddickison
if (self) {
NSArray *nib = [[NSBundle mainBundle]
loadNibNamed:#"HeaderView"
owner:self
options:nil];
[self release]; // release object before reassignment to avoid leak - thanks ddickison
self = [nib objectAtIndex:0];
self.layer.shadowColor = [[UIColor blackColor] CGColor];
self.layer.shadowOffset = CGSizeMake(1.0, 1.0);
self.layer.shadowOpacity = 0.40;
}
return self;
}
Which is ok except this presumes you call initWithFrame somewhere to instantiate the CustomView.
The HeaderView example calls it like this:
// load the header - this could be done in viewWillAppear as well
HeaderView *header = [[HeaderView alloc]
initWithFrame:CGRectMake(0, 0, 320, 60)];
header.lblTitle.text = #"That. Just. Happened.";
tblData.tableHeaderView = header;
[header release];
I wanted to be able to use my custom view class in any other XIB
HomeScreenViewController.xib
Files Owner: HomeScreenViewController
View:
+ ....other controls
+ ScreenMenuButtonsView
The problem is when HomeScreenViewController is loaded it calls awakeFromNib on my customView:
so I moved the nib loading out initWithFrame and into a method called setupView and called it from initWithFrame AND awakeFromNib
- (id) initWithFrame:(CGRect)frame
{
if ( ( self = [super initWithFrame: frame] ) )
{
[self setUpView];
}
return self;
}
- (void)awakeFromNib
{
[self setUpView];
}
- (void) setUpView{
if (self) {
NSArray* nibViewsArray = nil;
if([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPad){
//IPAD
nibViewsArray = [[NSBundle mainBundle] loadNibNamed:#"ScreenMenuButtonsView_iPad"
owner:self
options:nil];
...
}
My problem is that when HomeScreenViewController.xib is loaded:
HomeScreenViewController.xib
Files Owner: HomeScreenViewController
View:
+ ....other controls
+ ScreenMenuButtonsView
It tries to create ScreenMenuButtonsView and because its in a NIB
it calls
ScreenMenuButtonsView awakeFromNib
BUT in here I call loadNibNamed:
- (void) setUpView{
_viewSetupAlready = TRUE;
//http://nathanhjones.com/2011/02/20/creating-reusable-uiviews-with-a-drop-shadow-tutorial/
//self = [super initWithFrame:frame]; // not needed - thanks ddickison
if (self) {
NSArray* nibViewsArray = nil;
if([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPad){
//IPAD
nibViewsArray = [[NSBundle mainBundle] loadNibNamed:#"ScreenMenuButtonsView_iPad"
owner:self
options:nil];
}else{
//IPHONE
nibViewsArray = [[NSBundle mainBundle] loadNibNamed:#"ScreenMenuButtonsView_iPhone"
owner:self
options:nil];
}
if(nibViewsArray){
//[self release]; // release object before reassignment to avoid leak - thanks ddickison
UIView * myView = [nibViewsArray objectAtIndex: 0];
//Check the top level object in NIB is same type as this custom class
//if wrong you probably forgot to use correct NIB name in loadNibNamed:
if([myView isMemberOfClass:[self class]]){
self = (ScreenMenuButtonsView *)myView;
}else{
NSLog(#"ERROR:top level view is not same type as custom class - are you loading the correct nib filename in loadNibNamed:\n%#", myView);
}
}else{
NSLog(#"ERROR:nibViewsArray is nil");
}
}
}
loadNibName: load
ScreenMenuButtonsView_iPad.xib
Files Owner: no set NSObject
VIEW: ScreenMenuButtonsView
+ UIButton
+ UIButton
+ UIButton
But top view is ScreenMenuButtonsView
so creates a new instance and calls awakeFromNib
and I get infinite loop.
I know why the loops occurring and instance on ScreenMenuButtonsView is loading another instance from the xib.
My question is in
ScreenMenuButtonsView_iPad.xib
Files Owner: no set NSObject
VIEW: ScreenMenuButtonsView
+ UIButton
+ UIButton
+ UIButton
should I change
VIEW: UIView
but what about outlets and actions for the button:
should I change
Files Owner: no set NSObject
to be the view class
Files Owner: ScreenMenuButtonsView

Resources