I'm writing an iPhone app. Starting from a view controller in a navigation stack [called EditCreatorController], I am presenting a custom modal view controller [called BMSStringPickerController]. I have created a delegate protocol, etc. per the Apple guidelines for passing data back to the first view and using that view to dismiss the modal view. I even get the expected data back from the modal controller and am able to dismiss it just fine. The problem is, at that point, almost any action I take on the original view controller leads to debugger errors like
-[EditCreatorController performSelector:withObject:withObject:]: message sent to deallocated instance 0x3a647f0
or
-[EditCreatorController tableView:willSelectRowAtIndexPath:]: message sent to deallocated instance 0x3c12c40
In other words, it seems like the original view controller has evaporated while the modal view was showing. This is true no matter which of the two delegate callbacks is invoked.
Here is the code from the parent controller that invokes the modal view:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
if (indexPath.row == 1) { // selection on creator type row
// create a string picker to choose new creator type from list
BMSStringPickerController *picker = [[BMSStringPickerController alloc] initWithNibName:#"BMSStringPickerController" bundle:nil];
picker.delegate = self;
picker.stringChoices = [NSArray arrayWithObjects:#"composer", #"lyricist", #"arranger", #"original artist", #"other", nil];
picker.currentChoice = creator.type;
picker.title = #"Creator Type";
// wrap it in a nav controller so we can get tile bar etc. (from VC prog guide p. 93)
UINavigationController *newNavigationController = [[UINavigationController alloc]
initWithRootViewController:picker];
[self.navigationController presentModalViewController:newNavigationController animated:YES];
[newNavigationController release];
[picker release];
}
}
And here are the delegate callbacks:
- (void)stringPickerController:(BMSStringPickerController *)picker didPickString:(NSString *)string {
NSLog(#"received string back: %#", string);
typeLabel.text = string; // only change the label for now; object only changes if done button pressed
[self.tableView reloadData];
[self dismissModalViewControllerAnimated:YES];
}
- (void)stringPickerControllerDidCancel:(BMSStringPickerController *)picker {
NSLog(#"picker cancelled");
[self dismissModalViewControllerAnimated:YES];
}
Another weird thing (perhaps a clue) is that although I get the "received string back" NSLog message, and assign it to typeLabel.text (typeLabel is an IBOutlet to a label in my table view), it never appears there, even with the table reload.
Anyone have some ideas?
Maybe you release the delegate in dealloc of BMSStringPickerController?
It may not solve your problem, but I suggest telling the picker to dismiss itself (in the delegate methods), allowing the responder chain to correctly handle the dismiss:
[picker dismissModalViewControllerAnimated:YES];
The default behavior when there is a memory warning is the release the view of all view controllers that are not visible. So if there was a memory warning while in your modal view controller, its parent view controller could have its view unloaded.
When this happens, viewDidUnload is called on the view controller so that you can release any references you hold into the view. If you have references that you didn't retain they will become invalid when the view is unloaded. Maybe this is happening in your case?
See the UIViewController reference Memory Management section for details. The UIViewController method didReceiveMemoryWarning: releases the view if the view is not currently visible and then calls viewDidUnload.
Related
I have a modal view controller which fetches a password. Since I don't want the password written to disk if my application is interrupted, I want to cear the password on applicationWillResignActive. (For those who may comment, I know the secure text field does not properly zeroize).
I've tried the following code, and my view controller is never located. For the first set of code (UIView) I believe its because I'm mixing and matching views and view controllers. I'm not sure why the second set of code (UIViewController) is not working since a few folks have suggested it.
How does one enumerate view controllers and locate a controller of interest? I simply want to send clearPassworAndPin to PasswordPromptController if present (since viewWillDisappear is not always sent when the view disappears).
Modified 7KV7 and Jhaliya answer is below (it worked). The 'if' statement using viewController.modalViewController was executed 5 times (once for each controller in the tab view). So the single modal controller of interest was sent the clearPasswords message 5 times.
for (UIViewController * viewController in viewsControllers)
{
if ([viewController isKindOfClass:passwordPromptClass])
{
[(PasswordPromptController *)viewController clearPassworAndPin];
}
else
{
if(viewController.modalViewController)
[self clearPasswords:[NSArray arrayWithObjects:viewController.modalViewController, nil]];
}
}
Using UIViews (no joy)
- (void)applicationWillResignActive:(UIApplication *)application
{
if(application.windows != nil)
[self clearPasswords:application.windows];
}
- (void)clearPasswords:(NSArray *)subviews
{
Class passwordPromptClass = [PasswordPromptController class];
for (UIView * subview in subviews)
{
if ([subview isKindOfClass:passwordPromptClass])
[(PasswordPromptController *)subview clearPassworAndPin];
}
}
Using UIViewController (no joy)
- (void)applicationWillResignActive:(UIApplication *)application
{
if(tabBarController.viewControllers != nil)
[self clearPasswords:tabBarController.viewControllers];
}
- (void)clearPasswords:(NSArray *)viewsControllers
{
Class passwordPromptClass = [PasswordPromptController class];
for (UIViewController * viewController in viewsControllers)
{
if ([viewController isKindOfClass:passwordPromptClass])
[(PasswordPromptController *)viewController clearPassworAndPin];
}
}
NSArray *array = [self.navigationController viewControllers];
yourViewController = [array objectAtIndex:yourChoiceOfIndex];
Hope it helps.
At the point where you present the PasswordPromptController as a modalViewController could you not store it as an instance variable? Then, in your applicationWillResignActive: callback you will have a handle to the VC to message against.
Be sure to release and nullify your reference to the PasswordPromptController reference when it gets dismissed.
you will have to go through the navigation stack to get the controllers.
Use UINavgationController below method to get all viewController in your navigation stack.
#property(nonatomic, copy) NSArray *viewControllers
hmm..., I have to say I like to give alternative suggestions/solutions as many folks have tried to answer you question specifically.
If you found it's not easy to find the PasswordPromptController by enumerating view controllers, you can just declare (alloc/init) that controller in your app delegate, whenever you need to use it in other controllers, get it through app delegate, do something like presenting as a modal view.
When you want to do something against it in your app delegate, e.g. clear the pwd, it's super easy because you have the reference to it.
I'm trying to change the hidden property of a button and this is done in a method (View one):
-(void)changeSong:(NSString *)songName {
[[SimpleAudioEngine sharedEngine] playBackgroundMusic:[[NSBundle mainBundle] pathForResource:songName ofType:#"mp3"]];
musicPlaying = YES;
playButton.hidden = YES;
pauseButton.hidden = NO;
}
This method is called from another view:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
MainMenuController *mainMenu = [[MainMenuController alloc] initWithNibName:#"MainMenu" bundle:nil];
[mainMenu changeSong:[songs objectAtIndex:indexPath.row]];
mainMenu = nil;
[mainMenu release];
[[self navigationController] popViewControllerAnimated:YES];
}
I know that the changeSong method is being called correctly because the music changes. However, the hidden property of the items don't change. I've tried calling [self.view setNeedsDisplay]; but this doesn't do anything.
Thanks
It looks like the MainMenuController hasn't finished initializing by the time you call -changeSong, which is why everything in MainMenuController is nil.
To solve this, either delay your call to -changeSong by using
[mainMenu performSelector:#selector(changeSong:) withObject:[songs objectAtIndex:indexPath.row] afterDelay:0.01];
or make your tableview the MainMenuController's delegate, and when MainMenuController is finished loading from the nib (using - (void)awakeFromNib in MainMenuController), call the delegate's method to change the song.
Since you're delaying the call in both cases, you'll have to be careful not to release the view controller before you do, so you'll have to change that code a little.
When initializing a view controller from a nib using -initWithNibName:bundle:, the actual view and its subviews aren't unarchived until the first time the view controller's view property is accessed, per the documentation:
The nib file you specify is not loaded right away. It is loaded the first time the view controller’s view is accessed. If you want to perform additional initialization after the nib file is loaded, override the viewDidLoad method and perform your tasks there.
Try calling [mainMenu view] right after you initialize it from the nib. That will hydrate the view hierarchy from the nib.
However, I guess I don't understand why you're unarchiving a view controller from a nib and calling one of its methods that affects the UI (i.e., hiding or revealing buttons) without pushing that view controller to a navigation controller or presenting it modally. -changeSong: is a method on MainMenuController, so simply calling it right after you initialize MainMenuController won't have any effect on the buttons that it manages.
(Unrelated: You're setting mainMenu to nil before releasing it, which effectively means mainMenu can never be released. Call -release first, then, optionally, set it to nil.)
to show a modal uiview out of my mainView I use:
[self presentModalViewController:myController animated:YES];
and in MyController I close that view with:
[self dismissModalViewControllerAnimated:YES];
But how can I know in the mainView that the modal was finished (to redraw my table)?
Currently I set a local variable to YES in my mainView after starting the modal view an react on viewWillAppear:
[self presentModalViewController:myController animated:YES];
_reloadTableData = YES;
-(void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
if (_reloadTableData) {
_reloadTableData = NO;
[_tableView reloadData];
}
}
Is there a better way to do so ?
Generally speaking, it's not appropriate to dismiss the modal view by the modal view itself.
Instead, you should set your main view as the delegate of the modal view. When you modal view finishes its task, it can let its delegate know and let its delegate dismiss it. This is the very common so-called delegate design pattern in Objective-C.
btw, you may want to consult with some code samples to gain a better understanding of this delegate pattern. I suggest you take a look at one of Xcode's default templates - the Utility Application template. It has a very succinct and simple and straightforward delegate structure built inside.
I need to add this to my dismiss button :-
[self dismissModalViewControllerAnimated:YES];
[self release];
else
[self.view removeFromSuperview];
I thought
if( self.navigationController.modalViewController ) {
would work be it nevers true
A couple of things:
1) You shouldn't ever release yourself in an object. If you're presenting a modal view controller, you should perform the release there since the view controller will now be retained by the view controller's .modalViewController property:
(In the parent):
UIViewController *someViewController = [[UIViewController alloc] init];
[self presentModalViewController:someViewController animated:YES];
[someViewController release];
2) The parent will store its child modal view controller in .modalViewController. The child will have its .parentViewController property set in this case. If the view has been added as a subview, its .superview property will be set. These are not mutually exclusive, however, so be careful. Generally speaking, UIViewControllers are intended to host full-screen views, and if you're adding the view as a subview, you should ask yourself if the view should just be a UIView subclass, and move the logic into the parent view controller.
That said, I suppose you could check your case (assuming you don't present modal view controller and add as a subview simultaneously):
if (self.parentViewController) {
[self dismissModalViewControllerAnimated:YES];
} else if (self.view.superview) {
[self.view removeFromSuperview]
}
In the latter superview case, the view controller will still be hanging around, so you'd need to let the other view controller know via delegate method or something to release you. In the first case, if you have released the presented view controller already as I described above, it will be released automatically when the parent view controller sets its .modalViewController property to nil.
Normally for a "dismiss" button I would call a method in the controller that presented the modal controller (use a delegate), not try to dismiss the modal view controller from within itself. I don't quite get what youre trying to do though, but that [self release] looks bad. I don't think you ever want to release self like that.
Try this in you modal viewcontroller:
- (IBAction)close:(id)sender {
[self.parentViewController dismissModalViewControllerAnimated:YES];
}
Then just connect the button's action to that method.
I have a button inside the content of a UIPopoverController. This button runs a method called myAction.
MyAction has the form
- (void) myAction:(id)sender
so, myAction receives the id of the caller button.
Now, inside this method I would like to dismiss the UIPopoverController, but the only thing I have is the ID of the caller button. Remember that the button is inside the UIPopoverController.
Is there a way to discover the ID of the UIPopoverController, given the button ID I already have?
thanks.
Unfortunately no. At least, not within the standard practices. You might be able to travel up the responder stack to find it, but it's a hack, it's buggy, and it's really, really messy.
If you want to dismiss a popover by pushing a button, some place relevant should keep a reference to the popover. Usually that would be the owner of the popover (not the controller showed within the popover). When the button is pressed, it can send a message to the owner controller, which can then dismiss the popover.
You might be tempted to have the controller displayed inside of the popover be the owner of its own popover, but coding this way is brittle, can get messy (again), and may result in retain loops so that neither ever gets released.
You can access the presenting popoverController by accessing "popoverController" with KVC.
[[self valueForKey:#"popoverController"] dismissPopoverAnimated:YES]
I have this working, and I do not think it is a hack. I have a standard split view iPad app. I then added a method on my detail controller (the owner of the pop over) to handle the dismissal.
On the standard split view architechture, both the root and detail view controllers are available via the app delegate. So I bound a button click inside the pop over to call a method which gets the app delegate. From there I call the method on the detail controller to dismiss the pop over.
This is the code for the method on the View Controller that is displayed inside the popover:
- (void) exitView: (id)sender {
MyAppDelegate *appDelegate = (MyAppDelegate *)[[UIApplication sharedApplication] delegate];
[appDelegate.detailViewController exitDrill];
}
Then the simple method to dismiss on the Detail View Controller:
- (void) exitDrill {
if(dtController != nil){
[dtController dismissPopoverAnimated: YES];
[dtController release];
}
}
I like the ability to do this because it give me a way to show a user how they can exit a pop over. This may not be necessary in future versions of the app; for right now, while this paradigm is still new to the platform, I prefer to let the users gexit a display in a couple fo different ways to make sure I minimize frustration.
As Ed Marty already wrote
If you want to dismiss a popover by pushing a button, some place relevant should keep a reference to the popover
This is very true; however, when showing a UIPopoverController, the class opening the popovercontroller keeps this resource already. So, what you could do is to use this class as the delegate class for your Popover Controller.
To do so, you could do the following, which I use in my code.
In the class opening the popover, this is my code:
- (void)showInformationForView:(Booking*)booking frame:(CGRect)rect
{
BookingDetailsViewController *bookingView = [[BookingDetailsViewController alloc] initWithStyle:UITableViewStyleGrouped booking:booking];
[bookingView setDelegate:self];
UINavigationController *navController = [[UINavigationController alloc] initWithRootViewController:bookingView];
self.popController = [[UIPopoverController alloc] initWithContentViewController:navController];
[self.popController setDelegate:self];
[self.popController setPopoverContentSize:CGSizeMake(320, 320)];
rect.size.width = 0;
[self.popController presentPopoverFromRect:rect inView:self.view permittedArrowDirections:UIPopoverArrowDirectionLeft animated:YES];
}
- (void)dismissPopoverAnimated:(BOOL)animated
{
[self.popController dismissPopoverAnimated:animated];
}
So what I am doing here is creating a UINavigationController and setting a BookingDetailsViewController as its rootViewController. Then I am also adding the current class as delegate to this BookingDetailsViewController.
The second thing I added is a dismissal method called dismissPopoverAnimated:animated.
In my BookingDetailsViewController.h I added the following code:
[...]
#property (nonatomic, strong) id delegate;
[...]
And in my BookingDetailsViewController.m I added this code:
[...]
#synthesize delegate = _delegate;
- (void)viewDidLoad
{
UIBarButtonItem *closeButton = [[UIBarButtonItem alloc] initWithTitle:#"Close" style:UIBarButtonItemStylePlain target:self action:#selector(closeView)];
[self.navigationItem setRightBarButtonItem:closeButton];
[super viewDidLoad];
}
- (void)closeView
{
if ([self.delegate respondsToSelector:#selector(dismissPopoverAnimated:)]) {
[self.delegate dismissPopoverAnimated:YES];
}
else {
NSLog(#"Cannot close the view, nu such dismiss method");
}
}
[...]
What happens is that when the "Close" button in the UINavigationController is pressed, the method closeView is called. This method check if the delegate responds to dismissPopoverAnimated:animated and if so, it calls it. If it does not respond to this method it will show a log message and do nothing more (so it wont crash).
I have written my code using ARC, hence there is no memory management.
I hope this helped you.