ARC and ViewControllers - objective-c

I have a little misunderstanding regarding ARC. I'm creating a new UIViewController using the following code:
CGRect screenRect = [[UIScreen mainScreen] bounds];
LocationProfileView *locationProfile = [[LocationProfileView alloc] initWithLocation:l];
locationProfile.view.frame = CGRectMake(0, screenRect.size.height, screenRect.size.width, 400);
[appDelegate.window addSubview:locationProfile.view];
[UIView animateWithDuration:.25 animations:^{
locationProfile.view.frame = CGRectMake(0, 0, screenRect.size.width, screenRect.size.height);
}];
In its UIVIew I put a button which removes the view from screen. The problem with this is that locationProfile gets deallocated immediately after its being added to screen, so everytime I'm trying to tap on "Close" button (which calls a method in LocationProfileView class) my application will crash.
So I added a property:
#property(nonatomic, strong) LocationProfileView *locationProfile;
and changed the second line of code in:
locationProfile = [[LocationProfileView alloc] initWithLocation:l];
but now my class won't be deallocated until I initiate it again (because it loses the reference to the first instance of LocationProfileView?). What should I do to make my class being deallocated every time I tap on "Close" button? I guess that setting locationProfile to nil would work, but this means that I'll have to call a method in the main class (the one which contains the code block).
What is the proper way for doing this? Sorry if my questions is too noobish.
Note:
l is an instance of a custom class which contains some infos to be displayed in LocationProfileView's UIVIew.

- (void)closeButtonCallBack {
[self.locationProfile removeFromSuperview];
self.locationProfile = nil;
}
i am assuming the target of your close button is the viewcontroller itself
a strong pointer will the retain the object until the viewController itself is deallocated, unless you assign to it nil
a local variable will be deallocated when it goes out of scope
ALTERNATIVELY
without using the strong pointer, you can do this
LocationProfileView *locationProfile = [[LocationProfileView alloc] initWithLocation:l];
UIButton *close = [UIButton buttonWithType:UIButtonTypeRoundedRect];
close.frame = CGRectMake(0, 100, 100, 30);
[close addTarget:locationProfile action:#selector(removeFromSuperview) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:close];

In your original example,
LocationProfile *locationProfile=...
is a local variable. So it's released as soon as you return from the constructor. That's what you observed.
When you make it a strong property, the view controller retains the locationProfile:
#property(nonatomic, strong) LocationProfileView *locationProfile;

Related

EXEC_BAD_ACCESS while popping out a new View inside existing View

I am trying to call a new view inside my existing view
TransactionFinish *childView= [[TransactionFinish alloc] initWithNibName:#"TransactionFinish" bundle:nil];
childView.view.frame = self.view.frame;
childView.view.frame=CGRectMake(10, 10, self.view.frame.size.width-20, self.view.frame.size.height-20);
childView.view.alpha = 0.0f;
[self.view addSubview:childView.view];
[UIView animateWithDuration:0.5 delay:0.0 options:UIViewAnimationCurveEaseOut
animations:^{
childView.view.alpha = 1.0f;
}
completion:^(BOOL finished) {
}];
It is going inside ViewDidLoad() of TransactionFinish(I have tried debugging it) but it gives me Thread EXEC_BAD_ACCESS(code=1,address=0x31f54e62) with green color
The issue lays with the lifetime of childView. You instantiate and store a reference to it into a local variable:
TransactionFinish *childView= [[TransactionFinish alloc] initWithNibName:#"TransactionFinish" bundle:nil];
If you are using ARC, when the local variable goes out of scope, the object referenced by it (childView) is deallocated.
If you are not using ARC, I suppose you are doing:
[childView release];
somewhere to avoid childView to be leaked (as the code you pasted above would imply).
Either hypothesis would explain why you get the crash: when viewDidLoad is called, the controller has already been deallocated.
Adding childView view to self.view:
[self.view addSubview:childView.view];
will retain childView.view but not childView. So the controller is deallocated, while its view is not.
A fix to this is creating a strong property in your class to store a reference to your childView controller:
#property(nonatomic, strong) TransactionFinish *childView;
Another possibility is using controller containment; you could do something li
[vc willMoveToParentViewController:self];
[self addChildViewController:childView];
[self.view addSubview:childView.view]; // or something like this.
[childView didMoveToParentViewController:self];
but this will only work on iOS5+.

removeFromSuperview doesn't work properly

In class
#interface StartScene : UIView
I call an instance of
#interface HelpView : UIView {
GameOverMenu* gorm;
PlayScene* tView;
}
and use addSubview. I also got huge code here
-(void) removemyself {
[tView removeFromSuperview];
[gorm removeFromSuperview];
[self removeFromSuperview];
}
-(void)restartPlay {
[tView removeFromSuperview];
[self playSceneDidLoad];
}
-(void)gameOverDidLoad {
[tView removeFromSuperview];
gorm = [[GameOverMenu alloc]initWithFrame:CGRectMake(0, 0, 320, 520)];
gorm.backgroundColor = [UIColor colorWithPatternImage:[UIImage imageNamed:#"backGround.jpg"]];
[gorm checkScore:Scores];
[self addSubview:gorm];
}
-(void)playSceneDidLoad {
[gorm removeFromSuperview];
tView = [[PlayScene alloc]initWithFrame:CGRectMake(0, 0, 320, 520)];
tView.backgroundColor = [UIColor colorWithPatternImage:[UIImage imageNamed:#"backGround.jpg"]];
[self addSubview:tView];
[tView ooneFingerTwoTaps];
}
And two sub classes of HelpView:
#interface PlayScene : HelpView
#interface GameOverMenu : HelpView <UITextFieldDelegate>
In StartScene when I push on a button, an instance of HelpView is created and in init method playSceneDidLoad is called.
Inside the PlayScene there is restart button which calls restartPlay method. When game is lost gameOverDidLoad method is called.
And In both PlayScene and GameOverMenu there are quit button, which calls removemyself method, that are supposed to return player to the main menu.
At first glance it should work fine, but if I press restart button for several times and than try to press Quit, it occurs that the views were not removed from superview, one press on a quit button only now removes them one by one.
And we stop on the HelpView, it didn't remove itself (even if I try to call [super removeFromSuperview]; somewhere.
I need to remove views correctly in time and to get to the main menu (StartScene) when quit is pressed. I don't think that a lot of views covering each other is a good variant. What is the problem?
Well I occurs that the point is that if super class' method is called from the subclass and there is such a command [self removeFromSuperview]; or [(someOtherSubview) removeFromSuperview];, it is subclass that uses self or (someOtherSubview). If our subclass doesn't have the pointed subView, than the command would do nothing. And if there is [self removeFromSubview];, subclass would remove itself.
Actually I solved this problem by using buttons as subView of superclass.

Performing selector on UILabel generates crash?

I read that UILabels aren't meant to respond to touch events, and that I could just use a UIButton. However, I have to subclass UILabel anyways to override another method, so I thought I might as well use a label to keep changes to my code a minimum.
How can I get my label to respond to touch events? The code and error displayed are below.
UILabel *tempLabel = [[UILabel alloc] initWithFrame:CGRectMake(startingPoint, 5, 10, 22)];
tempLabel.text = equationText;
tempLabel.font = [UIFont systemFontOfSize:13];
[tempLabel sizeToFit];
[view addSubview:tempLabel];
[tempLabel addTarget:self action:#selector(updateLabel:) forControlEvents:UIControlEventTouchUpInside]; // UNRECOGNIZED SELECTOR SENT TO INSTANCE
Since UILabel isn't a control, you can't send the -addTarget:action:forControlEvents: message. You must remove that line from your application since your label is not a control and will never respond to that message. Instead, if you wanted to use your label, you could set it interactive and add a gesture recognizer to it:
// label setup code omitted
UITapGestureRecognizer* tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(updateLabel:)];
[tempLabel setUserInteractionEnabled:YES];
[tempLabel addGestureRecognizer:tap];
[tap release]; // if not using ARC
The callback for the gesture recognizer will be passed the instance of the gesture recognizer that triggered it, not the control like an action message would. To get the instance of the label that triggered the event, message the passed-in gesture recognizer with -view. So, if your updateLabel: method might be implemented as below:
- (void)updateLabel:(UIGestureRecognizer*)recognizer
{
// Only respond if we're in the ended state (similar to touchupinside)
if( [recognizer state] == UIGestureRecognizerStateEnded ) {
// the label that was tapped
UILabel* label = (UILabel*)[recognizer view];
// do things with your label
}
}
Also, the gesture recognizer will call the action method with multiple states, similar to those found in the -touchesBegan:... series of methods. You should check that you're only committing work while the recognizer is in the appropriate state. For your simple tap gesture recognizer, you probably only want to do work when the recognizer is in the UIGestureRecognizerStateEnded state (see the example above). For more information on gesture recognizers, see the documentation for UIGestureRecognizer.
//create label
_label = [[UILabel alloc] initWithFrame:CGRectMake(self.view.center.x-75,self.view.frame.size.height-60, 150, 50)];
_label.backgroundColor = [UIColor clearColor];
_label.textColor=[UIColor whiteColor];
_label.text = #"Forgot password ?";
UITapGestureRecognizer *recongniser = [[UITapGestureRecognizer alloc]initWithTarget:self action:#selector(tapAction)];//ADD ACTION TO LABEL
[_label setUserInteractionEnabled:YES];
[_label addGestureRecognizer:recongniser];
//NAVIGATE TO ONEVIEW TO ANOTHER VIEW
-(void) tapAction //METHOD TO ADD IT TO LABEL SELECTOR
{
_forgotviewController=[[ForgotPassword alloc]init];
[self.navigationController pushViewController:self.forgotviewController animated:YES];
}
Smartest thing to do here is to use a UIButton to do what you are trying to do.
But if you really want to subclass UILabel, make sure to set userInteractionEnabled to YES.
The documentation says:
New label objects are configured to disregard user events by default.
If you want to handle events in a custom subclass of UILabel, you must
explicitly change the value of the userInteractionEnabled property to
YES after initializing the object.
And addTarget: action: forControlEvents: wouldn't work, because UILabel isn't descended from UIControl. One place you can catch your events by implementing UIResponder's touchesBegan:withEvent: method in your subclass.
Here is swift 2.1 version for UILabel tap
let label = UILabel(frameSize)
let gesture = UITapGestureRecognizer(target: self, action: "labelTapped:")
labelHaveAccount.userInteractionEnabled = true
labelHaveAccount.addGestureRecognizer(gesture)
func labelTapped(gesture:UIGestureRecognizer!){
//lable tapped
}

Can't access UIView in external method

I'm new to Objective-C, so the way I'm going about this might be ludicrous, but here goes:
I have a login form in my iPhone application. When the user has entered their credentials, they hit Done in the top right corner, which triggers an IBAction and a custom progress indicator pops up. I've created this indicator by using a class containing an instance method named showProgressIndicator. showProgressIndicator creates and returns a UIView, which I then add to my view like so:
ProgressIndicatorElement *ProgressIndicator = [[ProgressIndicatorElement alloc] init];
box = [ProgressIndicator showProgressIndicator];
[self.view addSubview:box];
I have of course declared box as a UIView in my header file. The progress indicator pops up beautifully and in the meantime I'm doing a behind-the-scenes URL request that, when finished, calls another method in my view controller named receivedServerResponse. Now, what I want to do is to remove the progress indicator, which is why I'm doing this:
- (void)receivedServerResponse {
[box removeFromSuperview];
}
But nothing happens at all. I'm not getting any errors or warnings, and the code is being highlighted just as if everything was running smoothly. I've tried retaining the indicator in my IBAction, but that doesn't help either.
Hope you can help out!
Updated:
Here is the showProgressIndicator method:
- (UIView *)showProgressIndicator {
box = [[UIView alloc] initWithFrame:CGRectMake(85, 190, 210, 140)];
box.backgroundColor = [UIColor colorWithRed:0.0 / 255 green:0.0 / 255 blue:0.0 / 255 alpha:.6];
box.layer.cornerRadius = 8;
UIActivityIndicatorView *spinner = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge];
spinner.layer.frame = CGRectMake((box.layer.bounds.size.width - spinner.layer.bounds.size.width)/2, 20, spinner.layer.bounds.size.width, spinner.layer.bounds.size.height);
[spinner startAnimating];
[box addSubview:spinner];
UILabel *titleInBox = [[UILabel alloc] initWithFrame:CGRectMake(0, 65, 150, 20)];
titleInBox.font = [UIFont boldSystemFontOfSize:16];
titleInBox.textColor = [UIColor whiteColor];
titleInBox.textAlignment = UITextAlignmentCenter;
titleInBox.backgroundColor = [UIColor clearColor];
titleInBox.text = #"Authorizing...";
[box addSubview:titleInBox];
return box;
}
Second update:
#Deepak just pointed out in the comments that I might be running two different instances of my view controller, which actually seems to be the case. In the external class that handles the aforementioned URL request, I get back to the view controller's receivedServerResponse method by doing this:
- (void)requestFinished:(ASIHTTPRequest *)request {
NSString *responseString = [request responseString];
SignInViewController *viewController = [[SignInViewController alloc] init];
[viewController receivedServerResponse];
}
Without spreading myself too thin (probably too late ;)), ASIHTTPRequest is set up so that if you call one method that performs an asynchronous URL request, a predefined method called requestFinished (above) is called, which is why I've had to call my view controller this way, because I can't access the returned value in an easier way (that I know of).
Creating a new instance of SignInViewController is not the correct way. It only seems correct to maintain a weak reference (assigned property) of the SignInViewController object. Say your class is RequestHandler.
#interface RequestHandler: [..] {
}
#property (nonatomic, assign) SignInViewController * signInViewController;
#end
#implementation RequestHandler
#synthesize signInViewController;
[..]
- (void)requestFinished:(ASIHTTPRequest *)request {
NSString *responseString = [request responseString];
[signInViewController receivedServerResponse];
}
#end
So when you're creating a RequestHandler object within the SignInViewController instance, you do,
RequestHandler * requestHandler = [[RequestHandler alloc] init];
requestHandler.signInViewController = self;
[..]
Note, you can also look at delegation and notifications.
I think part of the problem may be with memory management. If showProgressIndicator does not return an autoreleased object, try releasing box after adding it as a subview, like so:
[self.view addSubview:box];
[box release];
box may not disappear if box is not deallocated when removed from the superview.
My other recommendation is that instead of doing it the way you are doing, creating a view, adding it, and then trying to removing it, you might want to try adding box as a subview when the login view is created and setting its hidden property to YES then unhiding it later when necessary.
Based on your update: You have some memory management issues in showProgressIndicator. Whenever you alloc an object, you should release it. In this case, release all of your variables after adding them as subviews as I mentioned above. box however should be returned as an autoreleased object since showProgressIndicator does not know when it will need to be released. For that you should replace return box; with return [box autorelease];
You need to send the activity indicator a stopAnimating message when you want the animation to stop. There's no need to remove it from its superview; instead, simply make sure that its hidesWhenStopped property is set to YES.
How about adding box view on window in appDelgate? Give a tag to your boxView and in the remove method get the boxView back by using tag. For example if you give tag 99
- (void)receivedServerResponse {
UIView *box = [window viewWithTag:99];
[box removeFromSuperview];
}
also you don't need to declare an instance variable in header file. and you can access progress indicator anywhere in the application.
Without spreading myself too thin (probably too late ;)), ASIHTTPRequest is set up so that if you call one method that performs an asynchronous URL request, a predefined method called requestFinished (above) is called, which is why I've had to call my view controller this way, because I can't access the returned value in an easier way (that I know of).
ASIHTTPRequest calls -requestFinished: on the object you set as the request's delegate. You should design your classes such that this delegate object either has a reference to the view controller you want it to act on or has some means of notifying that view controller to take action.
The easiest solution might be to make the controller the request's delegate.

Create subview on awakeFromNib

I'm trying to create a NSImageView programmatically as a subview of another NSImageView when awakeFromNib is called.
My code is as follows (Fader is defined in MyImageView.h):
#implementation MyImageView
- (void)awakeFromNib {
Fader = [NSImageView initWithFrame: [self frame]];
}
I get the warning message "NSImageView may not respong to +initWithFrame". When I build, the app simply frizzes without showing anything, and I have to "force quit".
What am I doing wrong?
You’ve forgotten to send +alloc in order to allocate the object. Change that line to:
Fader = [[NSImageView alloc] initWithFrame: [self frame]];