I've been struggling with a bug, and I found a work-around, but I'd like to understand what is exactly going on. it has something to do with UIButton target actions misfiring depending on different subview hierarchies, inside a subclass.
Brief summary: I have a subclass of NSObject with a UIView property object, a UIButton attached to it, and a target added to the button calling a function inside the subclass. Inside the main ViewController, I init the subclass and add its view to the view stack, click the button, and it throws me to main.mm with the error - EXC_BAD_ACCESS, gives me little feedback. so the hierarchy looks like this:
-CustomClass
--UIView <-this is added as a subview to the View Controller
---UIButton (onRelease calling a function)
so I fixed it by changing the custom class to be a subclass of UIView instead of NSObject, then add its #property UIView to be a subview of the custom class (and the button is still attached to the subview), and then in the main View Controller, I add the custom class itself as a subview, not the class's subview property object. then the button successfully calls the function. so the new arrangement looks like this:
-CustomClass (now UIView) <-this is added as a subview to the View Controller
--UIView <-this is added as a subview to CustomClass
---UIButton (onRelease calling a function)
then, i realized i can just keep the CustomClass a subclass of UIView for both instances, the problem persists with the original setup if everything else is unchanged.
okay, more detail, here's code:
CustomClass:
.h
#interface Temp : UIView
#property UIView *subview;
#property UIButton *but;
#end
.m
-(id) init{
self = [super initWithFrame:[[UIScreen mainScreen] bounds]];
if(self){
_subview = [[UIView alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
//[self addSubview:_subview]; // FOR THE FIX
_but = [UIButton buttonWithType:UIButtonTypeRoundedRect];
[_but setTitle:#"OKAY" forState:UIControlStateNormal];
[_but setBackgroundColor:[UIColor blackColor]];
[_but setFrame:CGRectMake(50, 50, 200, 200)];
[_subview addSubview:_but];
[_but addTarget:self action:#selector(pageTurn) forControlEvents:UIControlEventTouchUpInside];
}
return self;
}
-(void) pageTurn{
NSLog(#"WORKS");
}
inside view controller:
Temp *temp = [[Temp alloc] init];
[self.view addSubview:temp.subview];
//[self.view addSubview:temp]; // FOR THE FIX, instead of above line
Who's holding onto temp?
If temp isn't referenced by anyone then it's released. At that point the target is zombie and of course you will crash. temp.subview is being held by self.view.
In the second setup, adding temp as a subview of self.view keeps a reference to it.
You can fix this by adding a Temp * property in the view controller.
self.temp = [[Temp alloc] init];
[self.view addSubview:self.temp.subview];
You're messing with all kinds of view hierarchy stuff you don't need to, which is likely the cause of the problem. I created a Test UIView subclass that had a UIButton instance variable that I added as a subview in the Test object, there's no need to add another view as a subview and then add the button to the subview, and then in your view controller add the button's subview to the view - it's WAY more complicated than it needs to be.
In a nutshell - Create the Temp UIView, add the button as a subview, then in your view controller class add the Temp UIView as a subview. Simple as that, here is the code:
- (id)init {
self = [super initWithFrame:[[UIScreen mainScreen] bounds]];
if(self){
_but = [UIButton buttonWithType:UIButtonTypeRoundedRect];
[_but setTitle:#"OKAY" forState:UIControlStateNormal];
[_but setBackgroundColor:[UIColor blackColor]];
[_but setFrame:CGRectMake(100, 100, 200, 200)];
[_but addTarget:self action:#selector(pageTurn) forControlEvents:UIControlEventTouchUpInside];
[self addSubview:_but];
}
return self;
}
- (void)pageTurn {
NSLog(#"WORKS");
}
Then added an instance to my view controller:
Test *temp = [[Test alloc] init];
temp.backgroundColor = [UIColor redColor];
[self.view addSubview:temp];
This was the result:
Related
i meet a very strange thing today: that my custom ViewController occasionally miss in responder chain
first i have a custom UIView, with a UIButton as its subview, then i set the target-action as usual:
[closeButton addTarget:self.controller action:#selector(closeButtonPressed) forControlEvents:UIControlEventTouchUpInside];
of course, i implement the closeButtonPressed method in my custom UIViewController
in most time, it just worked fine. but occasionally, my ViewController miss the tap event, and finally i found that , the tap event pass to the AppDelegate, and the closeButtonPressed method in AppDelegate is invoked!
i spent a whole day in this problem, and still don't know why. can you give me a clue? thanks a lot. following is the code, hope it can help:
the code to init and present my ViewController and View:
YLSRegisterStepOneViewController *step1Controller = [[YLSRegisterStepOneViewController alloc] initWithNibName:nil bundle:nil];
step1Controller.view = [[YLSRegisterStepOneView alloc] initWithFrame:CGRectMake(0, 0, 540, 720) OperType:operType];
step1Controller.modalPresentationStyle = UIModalPresentationFormSheet;
step1Controller.modalTransitionStyle = UIModalTransitionStyleCoverVertical;
[self presentViewController:step1Controller animated:YES completion:nil];
the code of the UIView:
UIButton *closeButton = [UIButton buttonWithType:UIButtonTypeRoundedRect];
closeButton.frame = CGRectMake(10, 10, 50, 50);
[closeButton setTitle:NSLocalizedString(#"button_close", #"") forState:UIControlStateNormal];
[closeButton addTarget:self.controller action:#selector(closeButtonPressed) forControlEvents:UIControlEventTouchUpInside];
[self addSubview:closeButton];
the code of the UIViewController:
-(void) closeButtonPressed
{
[self.presentingViewController dismissViewControllerAnimated:YES completion:nil];// to dismiss self
}
additional: aside the UIButton, there is a UITextField. when this problem happens, if i click the UITextField, then click the UIButton, UIViewController works fine
You set controller property of YLSRegisterStepOneView like this:
YLSRegisterStepOneViewController *step1Controller = [[YLSRegisterStepOneViewController alloc] initWithNibName:nil bundle:nil];
YLSRegisterStepOneView *step1View = [[YLSRegisterStepOneView alloc] initWithFrame:CGRectMake(0, 0, 540, 720) OperType:operType];
step1Controller.view = step1View;
step1View.controller = step1Controller;
You add the button in YLSRegisterStepOneView's initWithFrame: method, don't you?
So [closeButton addTarget:self.controller action:#selector(closeButtonPressed) forControlEvents:UIControlEventTouchUpInside]; self.controller here is still nil.
step1View.controller = step1Controller; is called after [closeButton addTarget:self.controller action:#selector(closeButtonPressed) forControlEvents:UIControlEventTouchUpInside];
updated:
You say the responder chain confuses you, I think the problem is that you assign view controller's view property like this step1Controller.view = step1View; which is not recommended. If you want to use your custom view as viewcontroller's view, do this:
Override loadView method in viewcontroller.
You can override this method in order to create your views manually. If you choose to do so, assign the root view of your view hierarchy to the view property. The views you create should be unique instances and should not be shared with any other view controller object. Your custom implementation of this method should not call super.
- (void)loadView
{
YLSRegisterStepOneView *step1View = [[YLSRegisterStepOneView alloc] initWithFrame:CGRectMake(0, 0, 540, 720) OperType:operType];
self.view = step1View ;
}
I don't see where you are setting the controller property of the custom view which you are passing here:
[closeButton addTarget:*self.controller* action:#selector(closeButtonPressed) forControlEvents:UIControlEventTouchUpInside];
I think if you create/set the controller property in your view it would solve the problem (unless you are already doing it but code is not here).
I have a UINavigationController class, I want to add on a button with the method addSubview but its not working
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
if (self) {
// Custom initialization
UIButton *testbtn = [[UIButton alloc] initWithFrame:CGRectMake(20, 90,28,20)];
[self.view addSubview:testbtn];
}
return self;
}
I assume because you're trying to do this on a navigation controller, you want a bar button item on the toolbar. You need to do this in the UIViewController, not the UINavigationController:
UIBarButtonItem * doneButton = [[UIBarButtonItem alloc] initWithTitle:#"Done"
style:UIBarButtonSystemItemDone
target:self
action:#selector(buttonPressed:)];
[self.navigationItem setRightBarButtonItem:doneButton];
Also, you should grab a cup of coffee and read through the "overview" section of the UINavigationController class reference. It'll take about 10 minutes and you'll be happy you did.
If I'm wrong, and you do want a UIButton (not a UIBarButtonItem), you also need to do that in a UIViewController subclass. Also, you should use its factory method, not a typical alloc/init:
UIButton *btn = [UIButton buttonWithType:UIButtonTypeRoundedRect];
btn.frame = CGRectMake(20, 90,28,20)
I don't believe you can add a button to a UINavigationController - it doesn't actually have a view of its own. The UINavigationController is more of a behind-the-scenes organizer for holding and displaying other UIViewControllers.
You'll need to take your [self.view addSubview:testbtn] and put that in the code of a UIViewController, instead of in the code for the UINavigationViewController. And as David Doyle pointed out in his answer, it's considered better practice to put something like that in viewDidLoad rather than in initWithNibName.
If you want to modify a View Controller's view, its not a good idea to do so in the init method. The nib file that extracts the resources that create the View Controller's view takes a small amount of time to complete.
You're better off modifying the View Controller's view by overriding the method -[UIViewController viewDidLoad] like so:
- (void)viewDidLoad
{
UIButton *testbtn = [[UIButton alloc] initWithFrame:CGRectMake(20, 90,28,20)];
[self.view addSubview:testbtn];
}
UIViewController *viewController = [[UIViewController alloc] init];
UIView *view1 = [[UIView alloc] init];
[viewController.view addSubview:view1];
[view1 release];
if I want release viewController;
[viewController release];
do I need manual release view1 before release viewController?
UIView *view = (UIView *)[[viewController.view subviews] objectAtIndex:0];
[view release];
[viewController release];
should I do this? or just release viewController?
No, you dont have to do that. Just release viewController and it will release all of its subviews internally. Rest will be taken care by framework.
If you are not using ARC, your code will look like this,
UIViewController *viewController = [[UIViewController alloc] init];
UIView *view1 = [[UIView alloc] init];
[viewController.view addSubview:view1];
[view1 release];
[viewController release];
Since you have allocated both viewController and view1 once, you have to release it once as shown above. You dont have to do a release again since you are not doing any retain on this after that.
If you do this,
UIView *view = (UIView *)[[viewController.view subviews] objectAtIndex:0];
[view release];
It will mostly result in a crash when viewController is released since you are releasing it twice and viewController's subviews are also getting released internally.
Here once thing you have to note is that, addSubview retains view1 as mentioned in Apple documentation.
The view to be added. This view is retained by the receiver.
After being added, this view appears on top of any other subviews.
This will be released once viewController is released and you dont have to manually release it since you dont own it.
This is the correct way.
UIViewController *viewController = [[UIViewController alloc] init];
UIView *view1 = [[UIView alloc] init];
[viewController.view addSubview:view1];
[view1 release];
[viewController release];
When you add a view as subview it'll be retained by the viewcontroller.
addSubview:
Adds a view to the end of the receiver’s list of subviews.
- (void)addSubview:(UIView *)view
Parameters
view
The view to be added. This view is retained by the receiver. After being added, this view appears on top of any other subviews.
Discussion
This method retains view and sets its next responder to the receiver,
which is its new superview.
Views can have only one superview. If view already has a superview and
that view is not the receiver, this method removes the previous
superview before making the receiver its new superview.
Reference UIView
Important: A view controller is the sole owner of its view and any
subviews it creates. It is responsible for creating those views and
for relinquishing ownership of them at the appropriate times such as
when the view controller itself is released
Reference : UIViewController Class
UIView *view = (UIView *)[[viewController.view subviews] objectAtIndex:0];
[view release];
It'll surely crash when you call release on viewController.
I have made a barbuttonitem programmatically on a toolbar overlay. All of this appears when my camera is accessed. The button functions fine and the selector is fully capable of calling it.
Here's the overlay in .m , take a look at the doneButton selector.
- (UIView*)CommomOverlay {
//Both of the 428 values stretch the overlay bottom to top for overlay function. It
doesn't cover it.
UIView* view = [[UIView alloc] initWithFrame:CGRectMake(0,0,320,420)];
UIImageView *FrameImg = [[UIImageView alloc] initWithFrame:CGRectMake(0,0,320,420)];
[FrameImg setImage:[UIImage imageNamed:#"newGraphicOverlay.png"]];
FrameImg.userInteractionEnabled = YES;
UIToolbar *myToolBar = [[UIToolbar alloc] initWithFrame:CGRectMake(0, 428, 320, 50)];
UIBarButtonItem *doneButton = [[UIBarButtonItem alloc]
initWithBarButtonSystemItem:UIBarButtonSystemItemDone target:self
action:#selector(doneButtonPressed)];
[myToolBar setItems:[NSArray arrayWithObjects: cancelButton, flexiSpace2, flexiSpace,
doneButton, nil] animated:YES];
[view addSubview:FrameImg];
[view addSubview:myToolBar];
return view;
}
Here's method I'm using for my void function.
//NewViewController is the nib I want to bring up when done is clicked.
-(void)doneButtonPressed {
NewViewController *UIView = [[NewViewController alloc] initWithNibName:nil bundle:nil];
UIView.modalTransitionStyle = UIModalTransitionStyleCrossDissolve;
[self presentModalViewController:UIView animated:NO];
[UIView release];
NSLog(#"Acknowledged!");
}
The button is being called, but nothing happens. As you can see I added an NSLog function to see whether it was being called or not. "Acknowledged" shows up in the log when the button is clicked, so it's not a problem with the selector. The app doesn't crash when i use the button either, which should occur had I done some heavily faulty coding (in fact I don't even get any warnings about it), as far as Xcode is considered, everything is fine which, needless to say isn't.
I also have my .h files of NewViewController in place correctly in the view controller in which this code is being implemented.
UIView is the name of a class. Don't use it as the name of a variable. At best it is hugely confusing and is a probable reason for your problems. Rename UIView to something like myView everywhere in doneButtonPressed.
I have an UINavigationController which the user navigates with.
When pushing a specific UIViewController onto the navigation stack, a "settings" button appear in the navigationBar. When the user clicks this button I would like to flip the current view/controller, i.e. everything on screen, including the navigationBar, over to a settings view.
So I have a SettingsViewController which I would like to flip to from my CurrentViewController that lives on a navigationController stack.
I get all kinds of strange behavior trying to do this, the UIViews belonging to the SettingsViewController will start to animate, sliding into place, the navigationButtons moves around, nothing acts as I would think.
-(void)settingsHandler {
SettingViewController *settingsView = [[SettingViewController alloc] init];
[UIView beginAnimations:nil context:nil];
[UIView setAnimationDuration:1.0];
[UIView setAnimationTransition:UIViewAnimationTransitionFlipFromRight
forView:self.navigationController.view
cache:YES];
[self.navigationController.view addSubview:settingsView.view];
[UIView commitAnimations];
}
The above results in the views flipping correctly, but the subviews of the SettingsViewController are all positioned in (0, 0) and after the transition, they 'snap' into place?
Is it because I instantiate and add my subviews in viewDidLoad, like this?
- (void)viewDidLoad {
UIImageView *imageBg = [[UIImageView alloc] initWithFrame:CGRectMake(0.0f, 0.0f, 320.0f, 460.0f)];
[imageBg setImage:[UIImage imageNamed:#"background.png"]];
[self.view addSubview:imageBg];
[imageBg release];
SettingsSubview *switchView = [[SettingsSubview alloc] initWithFrame:CGRectMake(0.0f, 0.0f, 320.0f, 460.0f)];
[self.view addSubview:switchView];
[switchView release];
[super viewDidLoad];
}
1: How should I correctly do the "flip" transition, from within the UIViewController in the UINavigationController, to a new UIViewController and subsequently from the new UIViewController and back to the "original" UIViewController residing on the UINavigationControllers stack?
2: Should I use a different approach, than the "viewDidLoad" method, when instantiating and adding subviews to a UIViewController?
-question 2 is more of a "best practice" thing. I have seen different ways
of doing it and I am having trouble either finding or understanding the life-cycle documentation and the different threads and posts on the subject. I am missing the "best practice" examples.
Thank You very much for any help given:)
If you want to create your view hierarchy programmatically, the place to do it is in -loadView. To do so you must create the view yourself, add all of its subviews, and then assign it to the view property, like this:
- (void)loadView {
UIView *containerView = [[UIView alloc] initWithFrame:CGRectMake(0.0f, 0.0f, 320.0f, 460.0f)];
UIImageView *imageBg = [[UIImageView alloc] initWithFrame:CGRectMake(0.0f, 0.0f, 320.0f, 460.0f)];
[imageBg setImage:[UIImage imageNamed:#"background.png"]];
[containerView addSubview:imageBg];
[imageBg release];
SettingsSubview *switchView = [[SettingsSubview alloc] initWithFrame:CGRectMake(0.0f, 0.0f, 320.0f, 460.0f)];
[containerView addSubview:switchView];
[switchView release];
self.view = containerView;
[containerView release];
}
It helps to understand the context in which this method gets called, and how it behaves by default. The first time the view property of a UIViewController is accessed, the default getter method calls -loadView to lazy-load the view. The default implementation of -loadView loads the view from a nib if one was specified. Otherwise it creates a plain UIView object and sets that as the controller's view. By overriding this method, you can ensure that your view's hierarchy will be fully formed the first time it is accessed.
-viewDidLoad should be used for any subsequent setup that needs to occur after the view hierarchy is fully loaded. This method will get called whether the view is loaded from a nib or constructed programmatically in loadView.