Modal View destructor not being called when InputAccessoryView is overriden - iphone

I am presenting a UIViewController modally and that view contains various UITextFields. I've overriden the modal views InputAccessoryView so that it shows a simple view, with one button that will ResignFirstResponder from every UITextField, thus dismissing the keyboard.
Recently I've been really cracking down on memory related issues so all my controllers now have a destructor. I've noticed that whenever the keyboard is shown with this overriden InputAccessoryView whenever the Modal View is dismissed the destructor will not be called. Does this mean that the UIViewController is not being destroyed? Am I incorrectly displaying the InputAccessoryView?
The InputAccessoryView code is as follows:
bool accessoryViewInit = false;
UIView accessoryView = new UIView(new RectangleF(0,0,320,30));
public override UIView InputAccessoryView
{
get
{
if (!accessoryViewInit)
{
accessoryView.BackgroundColor = UIColor.FromRGBA(0.0f, 0.0f, 0.f, 0.5f);
UIButton dismiss = new UIButton(new RectangleF(50,1, 200, 28));
dismiss.BackgroundColor = UIColor.Blue;
dismiss.SetTitle("Close Keyboard", UIControlState.Normal);
dismiss.TouchUpInside += delegate(object sender, EventArgs e) {
field1.ResignFirstResponder();
field2.ResignFirstResponder();
field3.ResignFirstResponder();
};
accessoryView.AddSubview(dismiss);
}
return accessoryView;
}
}
I have a feeling that is due to the fact I'm assigning a delegate to the TouchUpInside event for the button, is it keeping a reference to this, thus stopping the whole controller from being destroyed?
I've created a sample project which can be found at https://github.com/lukewhitt/InputAccessoryView-test
To recreate the problem: Run app, Present modal view by touching big red button. Now, if you dismiss the view without showing the keyboard, the destructor will be called. If you make on the UITextFields first responder (showing the keyboard and the InputAccessoryView) then dismiss the modal controller, the destructor won't be called.
EDIT
It would appear this is a bug in Monotouch that will be fixed in an up-coming release. In order to get around the problem, it seems you can't use anonymous delegates when assigning to events. So the dismiss.TouchUpInside would become:
public override UIView InputAccessoryView {
// code before
dismiss.TouchUpInside += HandleDismissTouch;
// rest of code
}
private void HandleDismissTouch (object sender, EventArgs e)
{
field1.ResignFirstResponder();
field2.ResignFirstResponder();
field3.ResignFirstResponder();
}
then in the code that dismisses the modal controller, I've added the following:
if (dismiss != null)
{
dismiss.TouchUpInside -= HandleDismissTouch;
dismiss.Dispose();
dismiss = null;
}
which causes the destructor to be called!

I think you're facing a bug in MT which will be fixed in MT 4. Looks like events handlers may prevent releasing controllers:
UIView events and garbage collection
Unfortunately, Geoff has not replied to the follow-up posters question which implications this has and if using anonymous delegates for events is an issue pre MT4.
I think a valid solution for now would be to manually Dispose().

Related

UIGestureRecognizer overriding "long presses" in UITableViewController

I am using -(NSUInteger)numberOfTouches method of UIGestureRecognizer class to count number of touches in a UITableViewController. (my earlier question)
Although I am able to count them correctly, but it is overriding touch events in such a way that tableView's didSelectRowAtIndexPath method is only called on a very short tap.
On long press, the cell does gets highlighted, but didSelectRowAtIndexPath is not fired.
Please guide what shall be done.
Thanks.
There's a couple of things to try. First of all, your immediate problem is that the gesture recognizer is probably waiting to see if you're going to add taps/touches to satisfy it's requirements for firing. You could also add a long touch recognizer that calls your didSelectRowAtIndexPath, but I would first watch the very good WWDC session on gesture recognizers from 2010 first, given you're a paid developer, and that would give you a good idea of how to correctly implement multiple gesture recognizers so that they fire when you want them.
call it from viewDidLoad() also I implement this code for monotouch it give an idea about longPressGestureRecognizer.
void AddGestureRecognizersToImage (UIImageView imgView)
{
var longPressGesture = new UILongPressGestureRecognizer (this, new Selector
("ShowResetMenu"));
imgView.AddGestureRecognizer (longPressGesture);
}
[Export("ShowResetMenu")]
void ShowResetMenu (UILongPressGestureRecognizer gestureRecognizer)
{
if (gestureRecognizer.State == UIGestureRecognizerState.Began)
{
var menuController = UIMenuController.SharedMenuController;
var resetMenuItem = new UIMenuItem ("Reset", new Selector ("ResetImage"));
var location = gestureRecognizer.LocationInView (gestureRecognizer.View);
BecomeFirstResponder ();
menuController.MenuItems = new [] { resetMenuItem };
menuController.SetTargetRect (new RectangleF (location.X, location.Y, 2, 2), gestureRecognizer.View);
menuController.MenuVisible = true;
imageForReset = gestureRecognizer.View;
}
}

Strange behaviour in viewWillAppear

I have a TabBar Controller with some tab bar item in it.
The first time that a user tap on a tab bar item, I want that a alertview is opened, so that the user can read some little instruction tips.
I have a global variable (say CONFIG), that hold some boolean valeus (CONFIG.tip1AlreadySeen, CONFIG.tip1AllreadySeen, etc.). All these boolean values are initializated to NO.
When the user tap a tab bar item, the viewWillAppear method in its viewcontroller is executed. In this method I put a code like this one:
-(void) viewVillAppear: (BOOL) animated {
extern CONFIG; // <- it's not the actual code but it indicates that a global variable must be used
[super viewWillAppear: animated];
if(CONFIG.tip1AlreadySeen == NO) {
CONFIG.tip1AlreadySeen = YES;
// code for showing the alertview
}
}
The strange thing is that this piece of code works perfectly in one viewcontroller but doesn't work in one another.
With some debug, I fidd out that in the another viewcontroller the code is executed but the assigment CONFIG.tipAlreadySeen = YES doesn't modify the actual value of CONFIG.tipAlreadySeen. This value is still NO. Unbelievable!!!
A little workaround was using the viewDidAppear method for changing the value:
-(void) viewVillAppear: (BOOL) animated {
extern CONFIG; // <- it's not the actual code but it indicates that a global variable must be used
[super viewWillAppear: animated];
if(CONFIG.tip1AlreadySeen == NO) {
// code for showing the alertview
}
}
-(void) viewDidAppear: (BOOL) animated {
extern CONFIG;
CONFIG.tip1AlreadySeen = YES;
}
...But I really did not understand what happened!!! Someone of you could explain this behaviour?
Thanks in advance!
Marco
Why must this be global and not contained in the view controller itself? Just a simple BOOL #property on your view controller that is toggled. And, to maintain this persistent across multiple runs of your application, save out the result to NSUserDefaults, which you in turn check each time you init your view controller.

"Pushing the same view controller instance more than once is not supported" exception

I am using the following code to retrieve some messages and putting them into my inbox.
MyInboxVC *inboxVC=[MyInboxVC get ];
//upload all the pending messages
UINavigationController *devNavController=[[MyappMgr get]getDeveloperNavigationController ];
[devNavController pushViewController:inboxVC animated:YES];
[devNavController setNavigationBarHidden:NO];
I get the exception
Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Pushing the same view controller instance more than once is not supported (<MyInboxVC: 0x1452a0>)'
What does it mean? What am I doing wrong?
I believe when you do some actions really fast this can happens too. I build something in like this:
if(![self.navigationController.topViewController isKindOfClass:[YOURCLASS class]]) {
Firstly handle the crash so it doesnt kill your app:
#try {
[self.navController pushViewController:viewController animated:NO];
} #catch (NSException * e) {
NSLog(#"Exception: %#", e);
} #finally {
//NSLog(#"finally");
}
Then if you get the error use popTo
- (void)pushViewController:(UIViewController *)viewController {
if (viewController) {
#try {
[self.navController pushViewController:viewController animated:NO];
} #catch (NSException * ex) {
//“Pushing the same view controller instance more than once is not supported”
//NSInvalidArgumentException
NSLog(#"Exception: [%#]:%#",[ex class], ex );
NSLog(#"ex.name:'%#'", ex.name);
NSLog(#"ex.reason:'%#'", ex.reason);
//Full error includes class pointer address so only care if it starts with this error
NSRange range = [ex.reason rangeOfString:#"Pushing the same view controller instance more than once is not supported"];
if ([ex.name isEqualToString:#"NSInvalidArgumentException"] &&
range.location != NSNotFound) {
//view controller already exists in the stack - just pop back to it
[self.navController popToViewController:viewController animated:NO];
} else {
NSLog(#"ERROR:UNHANDLED EXCEPTION TYPE:%#", ex);
}
} #finally {
//NSLog(#"finally");
}
} else {
NSLog(#"ERROR:pushViewController: viewController is nil");
}
}
It means that the ViewController returned from [MyInboxVC get] is already in the navigation stack of devNavController. You can not add the same object to the stack multiple times.
Apparently, you already have a MyInboxVC pushed earlier. Insure that you've popped it when it was no longer needed.
That's the "what's it mean" answer, but don't have enough info to know what you need to do to fix it.
My guess is your Navigation Stack is growing larger than you are expecting, meaning you are not popping as often as you should.
Are you performing this as part of a segue? If you are, there is no need to push a VC onto your Navigation Controller because the segue will do it already. That is why your error is occurring - you are pushing a VC that is already on the stack of the NavController.
It means you are pushing the same viewcontroller object to stack again when it's already in there.
[self.navigationController pushViewController:viewControllerObj animated:NO];
[self.navigationController pushViewController:viewControllerObj animated:NO];
check if u r pushing inside a loop or if u've accidentally placed the code more than one time..
The Main Reason for this problem, obviously if the code that pushed the view controller is called more than once. This could occur for many reasons, most common mistake when a callback method is triggered from a background thread, where this method could be executed more than once while it is still pushing the view controller.
Example:
Calling a service api on background thread when tapping a button, which will allow you to press the button more than once, and therefore the callback which pushes the view controller get called more than once.
#Melvin and #Sam solution is valid as long as you do not want to fix the original problem by not pushing more than once.
This is an expected behavior of UINavigationController where an exception is thrown when trying to push a view controller which is already present in the stack (Its there from iOS 2.2).
This was happening to me on a bar button click happening too fast, and was hard to reproduce, unless you went nuts on the button taps. The following fixed it by disabling the the button, starting the nav push, then enabling the button on the main thread (because it would be called after animation from the push occurred).
- (void)showMore
{
self.navigationItem.leftBarButtonItem.enabled = NO;
[self.navigationController pushViewController:moreVC animated:YES];
[self.navigationItem.leftBarButtonItem performSelectorOnMainThread:#selector(setEnabled:) withObject:#(YES) waitUntilDone:NO];
}
Make sure you are not adding the view controller twice in the navigation stack.
Eg - in below example self.mainViewC is pushed twice because it is initially instantiated in the navController, and is then pushed onto the navController again in the last line, which would cause this issue.
navController=[[UINavigationController alloc] initWithRootViewController:self.mainViewC];
self.window.rootViewController = navController;
[self.window makeKeyAndVisible];
[navController pushViewController:self.mainViewC animated:NO];
In this case mainViewC has already been added to stack when initWithRootViewController was written. There is no need of pushViewController again.
In my case i was pushing a viewcontroller, but then also trying to clear the navigation stack so that there was no vc's to pop to after this new VC had shown
self.show(viewController, sender: nil)
if clearNavigationStack {
self.navigationController?.viewControllers = [viewcontroller]
}
you cant do this directly after pushing a viewcontroller, you will need to wait till the viewcontroller has fully shown before trying to reset the navigation stack
Another option that I have experienced is that [MyInboxVC get ] is not returning an instance of a MyInboxVC object at all. A tell tale sign of this would be that the error is saying 'Pushing the same view controller instance more than once is not supported (notTheInboxVC: 0x9e31660)' ie. the class being pushed more than once is not the MyInboxVC expected (a fall through from MyInboxVC not being allocated)
I fixed the same issue (Swift 4) with IB segue using :
override func shouldPerformSegue(withIdentifier identifier: String, sender: Any?) -> Bool {
return navigationController?.topViewController is MainController ? true : false
}
In my case, I was pushing view controller and then trying to set array of navigation stack view controllers immediately after that. So it resulted in random crashes on transition (sometimes it crashed, sometimes it did not):
navigationController?.pushViewController(newViewController, animated: true)
navigationController?.viewControllers = [newViewController]
What I needed to do, is to use single line instead of those two (https://developer.apple.com/documentation/uikit/uinavigationcontroller/1621861-setviewcontrollers):
navigationController?.setViewControllers([newViewController], animated: true)
[devNavController pushViewController:inboxVC animated:NO];
Set animated as NO

How to dismiss keyboard when using DecimalPad

I have a UITableView with a custom cell that has a TextField. I have the DecimalPad comes up, and as we all know, there is no done key. I previously had resolved this type of issue when I had a "Decimal only" textfield on a normal UIView by handling the TouchesEnded event and then checking to see if the TextField was the first responder and if so, it would then resign, but if that technique could work now then I'm not able to figure out who's TouchesEnded I should be using (The UIView that everything is presented on, the UITableView, the Cell, the CellControler, the TextField.. I think I've tried everything).
I'm hoping there's another, cleaner way of dealing with this.
Anyone?
I think David has the best idea - here is some Monotouch code to get you started. You will need to put this in the View Controller where the decimal pad is being shown:
UIView dismiss;
public override UIView InputAccessoryView
{
get
{
if (dismiss == null)
{
dismiss = new UIView(new RectangleF(0,0,320,27));
dismiss.BackgroundColor = UIColor.FromPatternImage(new UIImage("Images/accessoryBG.png"));
UIButton dismissBtn = new UIButton(new RectangleF(255, 2, 58, 23));
dismissBtn.SetBackgroundImage(new UIImage("Images/dismissKeyboard.png"), UIControlState.Normal);
dismissBtn.TouchDown += delegate {
textField.ResignFirstResponder();
};
dismiss.AddSubview(dismissBtn);
}
return dismiss;
}
}
If you're targeting iOS 4.0 or greater you can create an inputAccessoryView containing a Done button to attach to the keyboard that will dismiss the keyboard when tapped. Here is an example from the documentation on creating a simple inputAccessoryView.
You could dismiss it when the user taps on the background; I think that's the most intuitive way.
In Interface Builder, change your View's class to UIControl. This is a subclass of UIView, so your program will work the same way, but you also get the standard touch events.
From here it's simple, create a method for the Touch Down event:
[numberField resignFirstResponder]
Of course it might be slightly different with MonoTouch -- unfortunately I don't know much about it, but wanted to help.
Hopefully you can use the concept, and modify your code accordingly.
Or you may just add some gesture to your main view.
For example:
//Just initialise the gesture you want with action that dismisses your num pad
-(void)textFieldDidBeginEditing:(UITextField *)textField
{
UISwipeGestureRecognizer *swipeToHideNumPad = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:#selector(hideNumPad:)];
swipeToHideNumPad.delegate = self;
swipeToHideNumPad.direction = UISwipeGestureRecognizerDirectionDown;
[swipeToHideNumPad setNumberOfTouchesRequired:1];
[self.view addGestureRecognizer:swipeToHideNumPad];
}
//action
- (void)hideNumPad:(UIGestureRecognizer *)gestureRecognizer
{
[self.amountTextField resignFirstResponder];
}

Reload the view in iphone (in viewWillAppear)

I got a little app that has a button whose click is handled via
- (IBAction)click:(id)sender { }
Now, what I want is after click() runs, I want the view to refresh/reload itself, so that viewWillAppear() is re-called automatically. Basically how the view originally appears.
Of course I can call viewWillAppear manually, but was wondering if I can get the framework to do it for me?
viewWillAppear is where to put code for when your view will appear, so it is more appropriate to put code that will be called repeatedly into another method like -resetView, which can then be called by both viewWillAppear and your click method. You can then call setNeedsDisplay from within resetView.
-(void)resetView
{
//reset your view components.
[self.view setNeedsDisplay];
}
-(void)viewWillAppear
{
[self resetView];
}
- (IBAction)click:(id)sender
{
[self resetView];
}