I've got a problem with understanding, how the iOS view controllers and alert controllers work in a specific case:
I have a custom UINavigationController in which there is my UIViewController. My Navigation controller has overridden dismissViewControllerAnimated:completion method. From this UIViewController I present new UIAlertController. Up to the point where the user clicks any button in the Alert, everything works fine. However, the strange part is, my custom UINavigationController's dismissViewControllerAnimated:completion method is being called (I don't want that, if possible...)
The Alert is presented in a regular manner (from the UIViewController within the UINavigationController):
UIAlertController *alert = [UIAlertController alertControllerWithTitle:title message:message preferredStyle:UIAlertControllerStyleAlert];
UIAlertAction *okAction = [UIAlertAction actionWithTitle:#"yep" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
[self takeOrder:data];
}];
UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:#"nope" style:UIAlertActionStyleCancel handler:^(UIAlertAction * _Nonnull action) {
}];
[confirmOrderAcceptAlert addAction:okAction];
[confirmOrderAcceptAlert addAction:cancelAction];
[self presentViewController:alert animated:YES completion:nil];
Is there any option to prevent this behavior? Why does this happen in the first place?
EDIT:
The code for dismissViewControllerAnimated:completion:
- (void)dismissViewControllerAnimated:(BOOL)flag completion:(void (^)(void))completion {
self.isHeroEnabled = NO;
[super dismissViewControllerAnimated:flag completion:completion];
}
I'm using Hero library to animate transitions, could this be the case?
As you are subclassing UINavigationController, it is will definitely call dismissViewControllerAnimated:completion.
To avoid it from disturbing the library code, check for specific ViewController Types.
Eg:
- (void)dismissViewControllerAnimated:(BOOL)flag completion:(void (^)(void))completion {
if(![self.visibleViewController isKindOfClass:[UIAlertController class]]){
self.isHeroEnabled = NO;
}
[super dismissViewControllerAnimated:flag completion:completion];
}
That's how the UINavigationController works
If you don't want to set that HeroEnabled for the actions called due to alerts. You might have to do something like
if(![self.visibleViewController isKindOfClass:[UIAlertController class]]) {
self.isHeroEnabled = NO
}
Related
I need to do some UI tasks when user selects a UI tab item. Following delegate is available,
-(void)tabBar:(UITabBar *)tabBar didSelectItem:(UITabBarItem *)item
Although, to answer for my particular question the internal UI transition issue is not important here, I am still sharing a overview code snippet.
-(void)tabBar:(UITabBar *)tabBar didSelectItem:(UITabBarItem *)item
{
AppDelegate *delegate = (AppDelegate*)[[UIApplication sharedApplication] delegate];
[delegate.rootTabBarController showConsentErrorPage];
}
However, my UI task inside this delegate shows a glitch on the transition as it does the work after the tab is already shown. I want to perform the UI task first before the UI being visible. Any such delegate of trick to resolve this issue?
This may help you (without additional information, I can't really say).
subclass UITabBarController conforming to <UITabBarControllerDelegate>
in that new custom UITabBarController , be sure to set self.delegate = self; in viewDidLoad
implement shouldSelectViewController
if that controller is your "needs consent to see" view controller,
check if the user has already given consent
if so, return YES (i.e. allow the tab to be selected)
if not, present your "Ask Consent" controller and return NO
if the user gives consent, navigate to that tab
Here is some sample code...
With this option, we present the "Ask Consent" controller, and only navigate to the "needs consent to see" tab when the user selects "Yes":
- (BOOL)tabBarController:(UITabBarController *)tabBarController shouldSelectViewController:(UIViewController *)viewController {
if ([viewController isKindOfClass:NeedsConsentViewController.class]) {
NeedsConsentViewController *vc = (NeedsConsentViewController *)viewController;
// whatever you're using to track the user's consent
if (vc.hasConsent) {
// allow the tab to be selected
return YES;
}
// configure / instantiate your "Consent" view controller
UIAlertController * alert = [UIAlertController
alertControllerWithTitle:#"Yes/No"
message:#"Need your consent..."
preferredStyle:UIAlertControllerStyleAlert];
UIAlertAction* okButton = [UIAlertAction
actionWithTitle:#"Yes"
style:UIAlertActionStyleDefault
handler:^(UIAlertAction * action) {
// however you're setting your user consent tracking
vc.hasConsent = YES;
// show that tab
[self setSelectedViewController:vc];
}];
[alert addAction:okButton];
UIAlertAction* noButton = [UIAlertAction
actionWithTitle:#"No"
style:UIAlertActionStyleDefault
handler:^(UIAlertAction * action) {
// user said NO... nothing else to do
}];
[alert addAction:noButton];
[self presentViewController:alert animated:YES completion:nil];
// don't show the tab
return NO;
}
// all other tabs
return YES;
}
With this option, we present the "Ask Consent" controller and navigate to the "needs consent to see" tab behind it. If the user answers "No" we navigate back to the previously selected tab:
- (BOOL)tabBarController:(UITabBarController *)tabBarController shouldSelectViewController:(UIViewController *)viewController {
NSInteger curTabIDX = self.selectedIndex;
if ([viewController isKindOfClass:NeedsConsentViewController.class]) {
NeedsConsentViewController *vc = (NeedsConsentViewController *)viewController;
// whatever you're using to track the user's consent
if (vc.hasConsent) {
// allow the tab to be selected
return YES;
}
// configure / instantiate your "Consent" view controller
UIAlertController * alert = [UIAlertController
alertControllerWithTitle:#"Yes/No"
message:#"Need your consent..."
preferredStyle:UIAlertControllerStyleAlert];
UIAlertAction* okButton = [UIAlertAction
actionWithTitle:#"Yes"
style:UIAlertActionStyleDefault
handler:^(UIAlertAction * action) {
// however you're setting your user consent tracking
vc.hasConsent = YES;
// we've already navigated to the tab, with the Consent VC presented on top of it
// so nothing else to do
}];
[alert addAction:okButton];
UIAlertAction* noButton = [UIAlertAction
actionWithTitle:#"No"
style:UIAlertActionStyleDefault
handler:^(UIAlertAction * action) {
// user said NO, so return to the previous tab
[self setSelectedIndex:curTabIDX];
}];
[alert addAction:noButton];
[self presentViewController:alert animated:YES completion:nil];
// show the tab behind the Consent VC
return YES;
}
// all other tabs
return YES;
}
Note: This is Example Code Only and is not intended to be, nor should be considered, "production ready."
I'm having an issue where while when the thumbnail is tapped, this action works.
-(IBAction)thumbnailTapped:(UIGestureRecognizer *)tap
{
if (_customItem.file.isEmpty)
{
[self openCamera]; // opens camera if no image is present
}
else
{
_isTransitioningToFullViewOrCamera = true;
[self performSegueWithIdentifier:#"CustomFileViewFromItem" sender:self]; // if file is present then it shows the picture blown up.
}
}
and the following code is called:
-(void) openCamera
{
_isTransitioningToFullViewOrCamera = true;
NSString *ext = _customItem.file ? _customItem.file.ext : JpgExt;
if ([CustomCameraController isCameraAvailableForExt:ext forViewController:self])
[CustomCameraController launchCameraForExt:ext forViewController:self];
}
but when called from a UIAlertAction, the camera does not open at all.
UIAlertController *actionSheet;
actionSheet = [UIAlertController alertControllerWithTitle:#"Select an action:" message:nil preferredStyle:UIAlertControllerStyleAlert];
[actionSheet addAction:[UIAlertAction actionWithTitle:#"Cancel" style:UIAlertActionStyleCancel handler:^(UIAlertAction *action) {
// Cancel button tappped.
[self dismissViewControllerAnimated:YES completion:^{
}];
}]];
[actionSheet addAction:[UIAlertAction actionWithTitle:#"Replace" style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) {
self dismissViewControllerAnimated:YES completion:^{
}];
[self openCamera];
}]];
// Present action sheet.
[self presentViewController:actionSheet animated:YES completion:nil];
if [self openCamera] is placed within the completion block or not, it doesn't change anything.
I was wondering if anyone had any experience with this; when the UIAlertController is not present (such as with single tap) the [self openCamera] opens up the camera and you can take a picture; however when called after the UIAlertController is presented this the camera will not open and fails silently.
"Ask and you shall receive"
there should be a maxim when you've been running up against a wall for days with no answers, only to ask the question and then find the answer on your own.
The answer was to comment out
//self dismissViewControllerAnimated:YES completion:^{
// }];
as it dismisses the view that contains the openCamera function.
I can't believe i didn't realize that until now.
Mission accomplished though! :)
Two View Controllers in my app subclass the same class that has common methods. I would like to launch an alertController (fire an alert) from code in a method of this common sub-classed controller. But the following code is not launching anything.
Can anyone suggest the right way to point to the subclassed VC to get the alertController to launch?
Thanks in advance for any suggestions.
View controller1 wired to storyboard subclasses common class as follows:
#interface IDManageItemsVC : IDCommonVC <UIAlertViewDelegate>
The common VC subclasses CoreVC which has even more common methods for the whole app:
#import "IDCoreVC.h"
#interface IDCommonVC : IDCoreVC<UITableViewDelegate,UITableViewDataSource,UIAlertViewDelegate>
I am trying to fire the alert from code in commonVC (the superclass for the class wired to storyboard) as follows:
-(void)fireAlert {
UIAlertController *alert = [UIAlertController alertControllerWithTitle:#"Delete?" message:nil preferredStyle:UIAlertControllerStyleAlert];
UIAlertAction* yesButton = [UIAlertAction
actionWithTitle:#"OK"
style:UIAlertActionStyleDefault
handler:^(UIAlertAction * action)
{
//run code
}];
UIAlertAction* noButton = [UIAlertAction
actionWithTitle:#"Not Now"
style:UIAlertActionStyleDefault
handler:^(UIAlertAction * action)
{
//if chosen run code
}];
[alert addAction:noButton];
[alert addAction:yesButton];
if ([alert respondsToSelector:#selector(setPreferredAction:)]) {
[alert setPreferredAction:yesButton];
}
/* following points to VC not in hierarchy so commented out
id rootViewController = [UIApplication sharedApplication].delegate.window.rootViewController;
if([rootViewController isKindOfClass:[UINavigationController class]])
{
rootViewController = ((UINavigationController *)rootViewController).viewControllers.firstObject;
}
if([rootViewController isKindOfClass:[UITabBarController class]])
{
rootViewController = ((UITabBarController *)rootViewController).selectedViewController;
}
[rootViewController presentViewController:alertInvite animated:YES completion:nil]; */
//Following does not do anything
[self presentViewController:alert animated:YES completion:nil];
}
Edit:
Using the following method, with a breakpoint, I visually verified that topViewController is the right one and then presented the alertview from it and it still did not display. The only thing I noticed is that when I visually examined the alertview, it appears blank with just a slight white curve in the upper left where a rounded corner might be against a white rectangle. So perhaps, there is something wrong with the way I'm creating the alertview.
UIViewController *currentTopVC = [self currentTopViewController];
currentTopVC.presentViewController.........
- (UIViewController *)currentTopViewController {
UIViewController *topVC = [[[[UIApplication sharedApplication] delegate] window] rootViewController];
while (topVC.presentedViewController) {
topVC = topVC.presentedViewController;
}
return topVC;
}
and instead of [self presentViewController..., the following:
UIViewController *currentTopVC = [self currentTopViewController];
[currentTopVC presentViewController:alertInvite animated:YES completion:nil];
I copied and pasted your code and replaced
[self presentViewController:alertInvite animated:YES completion:nil];
with
[self presentViewController:alert animated:YES completion:nil];
and it worked.
This doesn't look like a inheritance issue to me. Are you by any chance calling this method from viewDidLoad: or any method that gets called before the view controller is actually shown? If so, try calling it from viewDidAppear:
With the latest iOS 8.3 release, our app starts to have a weird behavior.
After finishing textfield editing, the user can click the close button which brings up an UIAlertView. When the user clicks discard in the alertview, alertview and current view are dismissed. But somehow the keyboard shows up after the view is gone which is quite annoying to users.
After some debugging, it seems that the keyboard is shown for the last textfield that the user has accessed before closing the view. I tried various ways to endEditing for the current view in many places (before showing UIAlertView, after clicking a button in the UIAlertView; I even set the focus to another UI element of the view). It didn't solve the problem.
But for this particular issue, I'm not sure if it's a common issue or something we need to fix. Everything works perfectly before iOS 8.3.
We understand that UIAlertView is deprecated for iOS 8. We're starting to migrate to UIAlertController. But if there's any workaround, we'd love to hear.
Here's some code snippet.
- (IBAction)closeTapped:(UIButton *)sender
{
// try to resign first responder
// [self.tfName resignFirstResponder];
// [self.tfPosition resignFirstResponder];
[self.view endEditing:YES];
if(self.orderDetails.isOpen && self.orderItemChanged)
{
UIAlertView* saveAlert = [[UIAlertView alloc] initWithTitle:#"Unsaved Changes"
message:#"Your changes have not been saved. Discard changes?"
delegate:self
cancelButtonTitle:#"Cancel"
otherButtonTitles:#"Save", #"Discard", nil];
[saveAlert show];
}
else
{
[self close];
}
}
- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex
{
switch(buttonIndex)
{
case 1: // Save
{
[self save];
break;
}
case 2: // Discard
{
[self close];
break;
}
}
}
- (void)close
{
[self.delegate dismissEditOrderItemVC];
}
If your deployment target is iOS 8+, try UIAlertController.
Here's a quick fix for UIAlertView: delay the invocation of showing the alert view when your text field or text view resigns first responder.
[self performSelector:#selector(showAlertView) withObject:nil afterDelay:0.6];
If anyone struggles with this, I hope this will help:
if (NSClassFromString(#"UIAlertController")) {
UIAlertController* alert = ...
}
else {
UIAlertView* alert = ...
}
you need to change alert for ios 8.3
first put this in your view
#define IS_IOS8 [[UIDevice currentDevice].systemVersion floatValue] >= 8.0
then
if (IS_IOS8) {
UIAlertController *alertVC = [UIAlertController alertControllerWithTitle:#"Unsaved Changes" message:#"Your changes have not been saved. Discard changes?" preferredStyle:UIAlertControllerStyleAlert];
UIAlertAction *saveAction = [UIAlertAction
actionWithTitle:#"Save"
style:UIAlertActionStyleCancel
handler:^(UIAlertAction *action)
{
[self save];
}];
UIAlertAction *cancelAction = [UIAlertAction
actionWithTitle:#"Cancel"
style:UIAlertActionStyleCancel
handler:^(UIAlertAction *action)
{
[alertVC dismissViewControllerAnimated:YES completion:nil];
}];
UIAlertAction *discardAction = [UIAlertAction
actionWithTitle:#"Discard"
style:UIAlertActionStyleCancel
handler:^(UIAlertAction *action)
{
[alertVC dismissViewControllerAnimated:YES completion:nil];
}];
[alertVC addAction:saveAction];
[alertVC addAction:cancelAction];
[alertVC addAction:discardAction];
[self.view.window.rootViewController presentViewController:alertVC animated:YES completion:nil];
this will help you as it helps me in same problem.
above code is compatible with both ios 7 & 8
I too, had a keyboard popping up (with the cursor in the last-used textView) after closing a UIAlertController and here is a very simple fix:
Immediately before building and presenting the UIAlertController,
Using [_activeTextView resignFirstResponder]; the keyboard will reappear.
Using [self.view endEditing:YES]; the keyboard will NOT reappear.
I hope this helps you.
Try using the below code. It works fine for iOS 8 and below version
if (IS_OS_8_OR_LATER) {
UIAlertController *alertVC = [UIAlertController alertControllerWithTitle:title message:msg preferredStyle:UIAlertControllerStyleAlert];
UIAlertAction *cancelAction = [UIAlertAction
actionWithTitle:#"OK"
style:UIAlertActionStyleCancel
handler:^(UIAlertAction *action)
{
}];
[alertVC addAction:cancelAction];
[[[[[UIApplication sharedApplication] windows] objectAtIndex:0] rootViewController] presentViewController:alertVC animated:YES completion:^{
}];
}
else{
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:title message:msg delegate:self cancelButtonTitle:#"Ok" otherButtonTitles:nil, nil];
[alert show];
}
}
If a text field is the first responder, it will automatically pop up the keyboard when the alert is dismissed. Ensure the first responder is properly dismissed with:
[textField resignFirstResponder]
Remember: in a table or scroll view, sometimes the field must be visible on the screen to properly dismiss the responder.
If there are no first responders active, keyboard should not appear when alert is dismissed.
For the particular case in this question, I would recommend setting a delegate method to listen for the "done" button and resigning the first responder in the delegate callback.
Alternatively, when beginning editing, you can store a reference to the currently active text field, then in your "clickedButtonAtIndex" method you can resign the active text field if it is still active.
I've noticed some weird behavior with textField keyboards and alertViews as well... Maybe make a bool called disableKeyboard and use it like this:
- (BOOL)textFieldShouldBeginEditing:(UITextField *)textField {
if (disableKeyBoard) {
disableKeyboard = NO;
return NO;
} else {
return YES;
}
}
- (void)alertView:(UIAlertView *)alertView willDismissWithButtonIndex:(NSInteger)buttonIndex {
disableKeyboard = YES;
}
This is just a workaround and doesn't address the core issue, whatever it is. For this method to work you need to set the alertView and textField delegate methods in your header.
In previous versions of iOS I was able to call show on a UIAlertView in the App Delegate. More specifically, show was called in:
func applicationDidBecomeActive(application: UIApplication)
Since UIAlertViews disregarded the view hierarchy in most cases, this solution worked no matter where the user was in the app.
With the introduction of UIAlertController this problem becomes a little trickier. UIAlertController is now a subclass of UIViewController and needs to be presented just like any other UIViewController. While presenting the UIAlertController from the keyWindow's rootViewController works, it's not the ideal solution.
Does anyone have any ideas on replicating [UIAlertView show] functionality for a UIAlertController? Any way to show the UIAlertController on app active without traversing the view hierarchy?
I figured out a solution that I believe to be more elegant than the answer I posted previously. I'll copy and paste the answer I posted to a similar question. Follow the link at the bottom of my post if you just want to see the code.
The solution is to use an additional UIWindow.
When you want to display your UIAlertController:
Make your window the key and visible window (window.makeKeyAndVisible())
Just use a plain UIViewController instance as the rootViewController of the new window. (window.rootViewController = UIViewController())
Present your UIAlertController on your window's rootViewController
A couple things to note:
Your UIWindow must be strongly referenced. If it's not strongly referenced it will never appear (because it is released). I recommend using a property, but I've also had success with an associated object.
To ensure that the window appears above everything else (including system UIAlertControllers), I set the windowLevel. (window.windowLevel = UIWindowLevelAlert + 1)
Lastly, I have a completed implementation if you just want to look at that.
https://github.com/dbettermann/DBAlertController
Here's what I ended up with:
public class func visibleViewController() -> UIViewController? {
return self.visibleViewController(UIApplication.sharedApplication().keyWindow?.rootViewController?)
}
private class func visibleViewController(viewController: UIViewController?) -> UIViewController? {
if viewController?.presentedViewController == nil {
println("Visible view controller: \(viewController)")
return viewController
} else if let navigationController = viewController as? UINavigationController {
return self.visibleViewController(navigationController.topViewController)
} else if let tabBarController = viewController as? UITabBarController {
return self.visibleViewController(tabBarController.selectedViewController)
} else {
return self.visibleViewController(viewController?.presentedViewController)
}
}
Try This
UIAlertController * alert= [UIAlertController
alertControllerWithTitle:#"title"
message:#" Your message hear"
preferredStyle:UIAlertControllerStyleAlert];
UIAlertAction *okBtnAction = [UIAlertAction actionWithTitle:#"Ok" style:UIAlertActionStyleDefault handler:^(UIAlertAction *action){
//Do what action u want.
[alert dismissViewControllerAnimated:YES completion:nil];
}];
[alert addAction:okAction];
UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:#"cancel" style:UIAlertActionStyleDefault handler:^(UIAlertAction *action){
[alert dismissViewControllerAnimated:YES completion:nil];
//do something when click button
}];
[alert addAction:Action];
UIViewController *vc = [[[[UIApplication sharedApplication] delegate] window] rootViewController];
[vc presentViewController:alert animated:YES completion:nil];
Try using JSAlertView which handles both UIAlertView and UIAlertController APIs.
It provides short and easy methods for displaying alerts and handles multiple alerts fired at same time, very well.