UIButton touches up IBAction causing EXC_BAD_ACCESS with ARC - objective-c

There have been a few questions on StackOverflow where users have experienced the same problem as I am having. However, none of their solutions fit my case. (See here, here, here and here for some of the SO questions I've read but have not found helpful.)
In my case, I have a NIB that has a couple UIButtons, with an associated Controller View. The view is relatively old to my project and I've been able to use these buttons without any trouble until today. After making a few code changes that were not related to the button behavior, I've run into an error that crashes the app, breaks the code at the main() function and gives me an EXC_BAD_ACCESS error message whenever I touch any of the buttons on my View.
How or why might this happen? I've actually commented out almost all functional code, especially that which I modified earlier today and I still can't stop the error from occurring.
My project is using Automatic Reference Counting and I haven't seen this error before. Furthermore, I did not modify the NIB, nor the IBAction associated with the buttons so I don't see what would cause this. The only way to stop the error is to unlink my UIButtons in my NIB to the IBActionmethods defined in my Controller View header file.
The only "unique" aspect of my use-case is that I load either one or two instances of this view, within another sub view controller. The number of instances of the broken view that are loaded is contingent on the number of objects in an array. Below is the code that I use to instantiate and load these views as subviews of another view.
//Called else where, this starts the process by creating a view that
//will load the problematic view as a sub-view either once or twice.
- (id)initWithPrimarySystemView:(SystemViewController *)svc
{
//First we create our parent, container view.
self = [super initWithNibName:#"ContainerForViewInstaniatedFromArrayObjs" bundle:nil];
if (self)
{
//Assign parent DataModel to local instance
[self setDataModel:((DataModelClass*)svc.DataModel)];
for (AnotherModel* d in DataModel.ArrayOfAnotherModels)
{
//Instantiate the SubViewController.
SubViewController* subsvc = [[SubViewController alloc]
initWithNibName:#"Subview"
bundle:nil
subviewPosition:d.Position ];
//Add the SubViewControllers view to this view.
[subsvc.view setFrame:CGRectMake((d.Position-1)*315, 0, 315, 400)];
[self.view addSubview:subsvc.view];
}
[self setDefaultFrame: CGRectMake(0, 0, 640, 400)];
}
return self;
}
This works perfectly and, previously, hadn't even caused any trouble with the buttons that were on the associated view, however, now all UIButtons crash the app when tapped.
The initialization function for SubViewController, as well as the viewDidLoad method contain nothing other than the standard, auto-generated code that is added when you create a new ViewController.
What can I do to either fix or diagnose this problem?

See my comments in your code:
{
SubViewController* subsvc = [[SubViewController alloc] initWithNibName:#"Subview" bundle:nil subviewPosition:d.Position ];
//!i: By default, subsvc is a __strong pointer, so your subview has a +1 retain count
// subsvc owns subsvc.view, so subsvc.view has a +1 retain count as well
//Add the SubViewControllers view to this view.
[subsvc.view setFrame:CGRectMake((d.Position-1)*315, 0, 315, 400)];
[self.view addSubview:subsvc.view];
//!i: This bumps subsvc.view to +2, as self.view strong-references it
//!i: subsvc is going out of scope, so the reference count on subsvc will drop
// to 0 and it is dealloc'd. subsvc.view's retain count drops to +1, as it
// is still referenced by self.view
//
// Most likely, in -[SubViewController dealloc], you were not doing a
// setTarget:nil, setAction:nil on the button. Thus, the button now
// has a dangling pointer and will crash when hit
}
To fix this, add each SubViewController instance to an array owned by the master view controler. That will keep the SubViewController instances around to receive the button taps.

Make sure in your dealloc you call:
[button removeTarget:nil
action:NULL
forControlEvents:UIControlEventAllEvents];
Even though you though you didn't need "dealloc's" in ARC, you do because of what iccir explained.

Related

Objective-c: recursive call of viewDidLoad after addSubview

I am working in Xcode 4.5.2.
I have the following problem:
When i make projects from non-empty projects - that is the views are initialised from the storyboard, sometimes when i add the code
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
UIButton *b = [UIButton buttonWithType:UIButtonTypeRoundedRect];
b.frame = CGRectMake(20, 20, 100, 100);
[self.view addSubview:b];
to the viewDidLoad method it is recursively called again after the addSubview method call and sometimes (bless the sky) it doesn't.
Can anyone tell me what's the problem and how to solve it?
My gratitude.
Edit: As #WDUK correctly states, the following answer is wrong. It would explain recursive in loadView, not viewDidLoad.
Old, wrong answer:
Recursive calls occur when after the call to -[super viewDidLoad]
the view property has not been set and you then call self.view.
You probably just forgot to connect the view to the file's owner's
view outlet.
When the device runs out of available memory, it could dealloc the view if it is not currently visible. You should always make sure you handle this behaviour correctly.
I could happen when you for example switch to another view when using a navigation or tabbarcontroller.

When is it safe to manipulate screen controls through code in iOS/Objective-C

I have a question regarding iOS (or perhaps more accurately Objective-C) and properties. I have a UIView with a UISegmentedControl, by default it has 3 segments. I have a message which accepts a parameter and based on this parameter I may want to remove one of the segments. In UIView A I do this:
MyViewController *myview = [[[MyViewController alloc] initWithNibName:#"MyViewController" nib:nil] autorelease];
[[self navigationController] pushViewController:myview animate:YES];
[myview showItem:item];
In UIView B this happens in showItem:
-(void) showItem:(Item*)item{
if (item.removeSegment){
[segmentControl removeSegmentAtIndex:0 animate:NO];
}
}
I have noticed that the segment only gets removed when I call showItem after I have pushed it on the navigation controller. When I swap those two line, so I first call showItem and then push the view, the UISegmentedControl still has three segments instead of two.
This just feels wrong, it seems like bad practice that my code will break if someone doesn't call two messages in the right order. Is there a better way to do this? I've been looking at some sort of a property lifecyle that I can use, I am very familiar with this from ActionScript 3, but I have been unable to find anything on the subject.
(as an aside: in AS3 I would make a property, in the setter I don't manipulate any screen controls but call InvalideProperties. My overriden methode CommitProperties will be called once the entire object and child controls have been created. In CommitProperties I check if my property value has changed and this is where I would remove the segment.)
A common way of doing something like this is to create an Item *item property in MyViewController and set that when myview is created. So, your code becomes:
MyViewController *myview = [[[MyViewController alloc] initWithNibName:#"MyViewController" nib:nil] autorelease];
myview.item = item;
[[self navigationController] pushViewController:myview animate:YES];
MyViewController would then use that property in its viewWillAppear: method to configure its own segment control.
I think what you are falling prey to is myview->segmentControl doesn't exist until myview.view is referenced because of the lazy load of the view.
-(void) showItem:(Item*)item{
[self view]; // NO OP TO FORCE LOAD!!
if (item.removeSegment){
[segmentControl removeSegmentAtIndex:0 animate:NO];
}
}
Should work for you. Hope this helps!

Adding Subviews to NSView show up, but can not be removed

In my application window I have two NSViews. On the left the NSView ("Menu") contains a few buttons. When one of the buttons is clicked it should change the contents of the right NSView ("Content").
For each of the views on the right I have a separate NSViewControllers that get loaded and their views gets added as a subview. When a further button gets pressed on the left the added subviews on the right should be removed and the new view should be loaded as a subview.
To accomplish this I load my Menu in AppDelegate with the following:
MenuVC *menuSubView = [[MenuVC alloc] initWithNibName:#"MenuVC" bundle: nil];
menuSubView.contentView = (NSView*)[self contentView];
[[self menuView] addSubview:[menuSubView view]];
This works fine. As you can see I have a NSView pointer in the Menu VC which points to the contentView so that I can populate it with the subviews.
Now as a method for one of the button presses I do the following:
SomeContentVC *subView = [[SomeContentVC alloc] initWithNibName:#"SomeContentVC" bundle:nil];
[self.contentView addSubview:[subView view]];
This does not work.
If I however add a subview from the awakeFromNib method of the MenuViewController implementation (in the case of default content when the app opens) it works. However when I try to remove that subview using
[[self.contentView setSubviews:[NSArray array]];
I can't. Interesting is also that if I try to count the number of subviews (even after having added one in the awakeFromNib method) it returns 0 subviews for self.contentView. Why? How can I get it to work properly?
Thanks
The fact that messaging self.contentView achieves nothing except, for some things, returning 0 probably means that self.contentView is nil.
Do you perhaps have two instances of MenuVC by accident? Perhaps one instantiated in a NIB and one instantiated in code?
When in doubt, log everything. Log self in various methods. Log menuSubView just after you create it. Log menuSubView.contentView just after you assign it. Etc. Eventually, you'll probably see that you're interacting with different objects than you thought you were.

Replacing Storyboard Segue with pushViewController causes strange behaviour

I can't seem to figure this out for the life of me. I have a custom table view cell, in that cell I have a few buttons configured. Each button connects to other view controllers via a storyboard segue. I've recently removed these segues and put a pushViewController method in place. Transition back and forth across the various views works as expected however the destination view controller is not displaying anything! I have some code below as an example.
Buttons have this method set:
[cell.spotButton1 addTarget:self action:#selector(showSpotDetails:) forControlEvents:UIControlEventTouchUpInside];
// etc...
[cell.spotButton4 addTarget:self action:#selector(showSpotDetails:) forControlEvents:UIControlEventTouchUpInside];
// etc...
showSpotDetails Method contains this code:
- (void)showSpotDetails:(id)sender
{
// determine which button (spot) was selected, then use its tag parameter to determine the spot.
UIButton *selectedButton = (UIButton *)sender;
Spot *spot = (Spot *)[spotsArray_ objectAtIndex:selectedButton.tag];
SpotDetails *spotDetails = [[SpotDetails alloc] init];
[spotDetails setSpotDetailsObject:spot];
[self.navigationController pushViewController:spotDetails animated:YES];
}
The details VC does receive the object data.
- (void)viewDidLoad
{
[super viewDidLoad];
NSLog(#"spotDetailsObject %#", spotDetailsObject_.name);
}
The NSLog method below does output the passed object. Also, everything in the details view controller is as it was. Nothing has changed on the details VC. It just does not render anything ever since I removed the segue and added the pushViewController method. Perhaps I am missing something on the pushViewController method? I never really do things this way, I try to always use segues...
Any suggestions?
Welcome to the real world. Previously, the storyboard was a crutch; you were hiding from yourself the true facts about how view controllers work. Now you are trying to throw away that crutch. Good! But now you must learn to walk. :) The key here is this line:
SpotDetails *spotDetails = [[SpotDetails alloc] init];
SpotDetails is a UIViewController subclass. You are not doing anything here that would cause this UIViewController to have a view. Thus you are ending up a with blank generic view! If you want a UIViewController to have a view, you need to give it a view somehow. For example, you could draw the view in a nib called SpotDetails.xib where the File's Owner is an SpotDetails instance. Or you could construct the view's contents in code in your override of viewDidLoad. The details are in the UIViewController documentation, or, even better, read my book which tells you all about how a view controller gets its view:
http://www.apeth.com/iOSBook/ch19.html
The reason this problem didn't arise before is that you drew the view in the same nib as the view controller (i.e. the storyboard file). But when you alloc-init a SpotDetails, that is not the same instance as the one in the storyboard file, so you don't get that view. Thus, one solution could be to load the storyboard and fetch that SpotDetails instance, the one in the storyboard (by calling instantiateViewControllerWithIdentifier:). I explain how to do that here:
http://www.apeth.com/iOSBook/ch19.html#SECsivc

objective-c addSubView retain count

I was under the impression that adding a subview to a view goes like this:
UITableViewController *sitesel = [[UITableViewController alloc] initWithStyle:UITableViewStyleGrouped];
sitesel.view.frame = CGRectMake(0,0,100,100);
[self.left addSubview:sitesel.view];
[sitesel release];
But it seems I should not release the sitesel (the controller)?
So should I release the view or what, I had this retain stuff nailed a while ago, but it's slipped. (And to use a TableView, you have to subclass UITableViewController right?)
(self.left is a subview of self.view, added in a nib)
addSubview does retain the view, that's not the problem. Your issue is that the controller for the view goes away a little later.
You shouldn't release the view, because that's none of your business. You didn't create it, you didn't touch it. Leave it alone.
In order to keep things working, it needs to stay connected to a valid controller. Hence, you must not release the controller, but keep it around. Add a property like #property(retain) UITableViewController *siteController; and then do self.siteController = sitesel; before you release the controller. This way everything stays in memory.
PS: For cleanness, you should probably change the view in the accessor for sitesel. Just to make sure it always comes and goes along the controller. Your method would then get even shorter, just setting the controller.
ADDED: That setter could look like that, requiring you to set only the controller and the view being updated transparently:
- (void)setSiteselController:(UITableViewController *)ctrl {
if (_sitesel)
[_sitesel.view removeFromSuperview];
[_sitesel autorelease];
_sitesel = [ctrl retain];
if (_sitesel) {
_sitesel.view.frame = CGRectMake(0,0,100,100);
[self.left addSubview: _sitesel.view];
}
}
Your original code will then shrink to this much cleaner version:
UITableViewController *sitesel = [[UITableViewController alloc] initWithStyle: UITableViewStyleGrouped];
self.siteselController = sitesel;
[sitesel release];
PPS: You don need an controller for a UITableView to work. It's just much simpler!