I would like to do some actions and present some UI right before and right after any UIAlertController dismisses itself (animation is finished) due to user tapping one of the alert's buttons.
How can I get notified that user pressed some button in my UIAlertController and it is going to be dismissed and then is dismissed?
In docs it is advised against subclassing UIAlertController. I still have tried my luck subclassing, thinking that maybe it internally calls func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) on itself. Something like self.dismiss(..., but it doesn't seem to be the case on iOS10.
I have also tried to add 'manual' dismissing into UIAlertAction handler:
let alert = UIAlertController.init(...
let defaultAction = UIAlertAction(title: "OK", style: .default, handler: { action in
alert.dismiss(animated: true, completion: {
print("Dismissed")
})
})
alert.addAction(defaultAction)
But it seems that alert is dismissed after button press but before calling handler. Anyhow it doesn't work as well. Even if it worked it would be a bit bothersome to remember to add my code into each and every UIAlertAction handler.
I would appreciate any ideas.
although subclassing is not advised you could use a simple subclass like this:
class CustomAlertController: UIAlertController {
var willDisappearBlock: ((UIAlertController) -> Void)?
var didDisappearBlock: ((UIAlertController) -> Void)?
override func viewWillDisappear(_ animated: Bool) {
willDisappearBlock?(self)
super.viewWillDisappear(animated)
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
didDisappearBlock?(self)
}
}
you could then use it like this:
let alert = CustomAlertController(title: "Alert", message: "This is an alert. Press Yes or No.", preferredStyle: .alert)
alert.willDisappearBlock = { alert in
print("\(alert) will disappear")
}
alert.didDisappearBlock = { alert in
print("\(alert) did disappear")
}
alert.addAction(UIAlertAction(title: "Yes", style: .default, handler: { (yesAction) in
print("User tapped Yes.")
}))
alert.addAction(UIAlertAction(title: "No", style: .cancel, handler: { (yesAction) in
print("User tapped No.")
}))
present(alert, animated: true) {
print("presentCompletion")
}
output is in the following order:
presentCompletion
will disappear
did
disappear
User tapped Yes.
You can disable the closing animation altogether like this:
class InstantCloseAlertController: UIAlertController {
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
UIView.setAnimationsEnabled(false)
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
UIView.setAnimationsEnabled(true)
}
}
This will trigger the action handler instantly.
But I'm also currently working on exactly what you're asking (keeping the animation). I got it all sorted out but needs some more work. It involves lot's of hacking, haha. I'll post it when it's done.
Related
I cannot seem to get the alert view to popup at startup of my view. Code is below.
import UIKit
class StartController: UIViewController
{
override func viewDidLoad()
{
super.viewDidLoad()
self.view.backgroundColor = UIColor.white;
startTest();
}
func startTest()
{
let alerta = UIAlertController(title: "Invalid Test", message: "Testing alert controller", preferredStyle: UIAlertControllerStyle.alert);
alerta.addAction(UIAlertAction(title: "OK", style: UIAlertActionStyle.default, handler: nil));
self.present(alerta, animated: true, completion: nil);
}
}
the issue is that in viewDidLoad the view hierarchy is not fully set. If you use viewDidAppear, then the hierarchy is set.
If you really want to call this alert in viewDidLoad you can do so by wrapping your presentation call in this GCD block to cause a slight delay
DispatchQueue.main.async {
// Run UI Updates or call completion block
startTest()
}
or use in
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
startTest()
}
Call the startTest() in viewDidAppear method. It works for me.
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
startTest()
}
func startTest()
{
let alerta = UIAlertController(title: "Invalid Test", message: "Testing alert controller", preferredStyle: UIAlertControllerStyle.alert);
alerta.addAction(UIAlertAction(title: "OK", style: UIAlertActionStyle.default, handler: nil));
self.present(alerta, animated: true, completion: nil);
}
Just try to put it on viewDidAppear: method
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
startTest()
}
I have a childViewController which is pushed from parentViewController. In childViewController I want to block pop action in a particular condition.
I wrote this code in viewWillDisappear: But I guess need to do this somewhere else.
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
if changesMade {
let alertController = UIAlertController(title: "Alert", message: "Changes made are not saved. Do you wish to save changes made?", preferredStyle: .alert)
let cancelOption = UIAlertAction(title: "Cancel", style: .cancel)
let saveOption = UIAlertAction(title: "Save", style: .default, handler: { (action) in
self.saveSession()
})
alertController.addAction(saveOption)
alertController.addAction(cancelOption)
present(alertController, animated: true)
}
}
Actually, there is a very simple solution: navigationItem.hidesBackButton = true - this will hide "BACK" button and disable swipe-to-back feature. 🤓
There are two cases here:
User can pop using back button.
User can pop using interactive pop gesture of navigation controller.
I think you should use a custom back button and name it Done and write your logic of showing alert on press of this button.
Using the custom back button would disable the interactive pop gesture by default and you will be spared from playing the dance of enabling/disabling interactivePopGesture on navigation controller.
block pop action till your changes are not saved like this
if changesMade {
let alertController = UIAlertController(title: "Alert", message: "Changes made are not saved. Do you wish to save changes made?", preferredStyle: .alert)
let cancelOption = UIAlertAction(title: "Cancel", style: .cancel)
let saveOption = UIAlertAction(title: "Save", style: .default, handler: { (action) in
self.saveSession()
self.navigationController?.popViewController(animated: true)
})
alertController.addAction(saveOption)
alertController.addAction(cancelOption)
present(alertController, animated: true)
}
Update - Add this below custom button and its action in child View controller which is being pushed from parent View Controller
so, without satisfying your condition user can not move from child to parent again
For customising action of navigation backButton you need to manually add a back Button using below line , you can Customise barButton being added here in DidLoad
self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: "Back", style: UIBarButtonItemStyle.plain, target: self, action: #selector(self.backToInitial(sender:)))
It will perform required Action
#objc func backToInitial(sender: AnyObject) {
if changesMade {
let alertController = UIAlertController(title: "Alert", message: "Changes made are not saved. Do you wish to save changes made?", preferredStyle: .alert)
let cancelOption = UIAlertAction(title: "Cancel", style: .cancel)
let saveOption = UIAlertAction(title: "Save", style: .default, handler: { (action) in
self.saveSession()
self.navigationController?.popViewController(animated: true)
})
alertController.addAction(saveOption)
alertController.addAction(cancelOption)
present(alertController, animated: true)
}
}
and I do not think you can stop default back button action task of navigation Controller as it is designed the way to perform it
But yes you can manage it in ViewWillDisappear as :
override func viewWillDisappear(_ animated: Bool) {
if self.isMovingFromParentViewController || self.isBeingDismissed {
self.navigationController?.popViewController(animated: false) //here task but will not result as Expected output
}
}
-----------------Re-Update ---------------
in swift I used a objective-C class to get output as expected now, childViewController pop action is being controller from a alert using default back button that we get from navigation controller
You can customise you pop action to perform or not until your condition is not satisfied
Github Link - https://github.com/RockinGarg/NavigationBackButton.git
I found a much more elegant solution (in my opinion).
It works no matter how the user triggers the pop (accessibility escape, swipe back gesture, tapping back) since it overrides the built in pop methods that the system uses.
Swift 5
public class DiscardSafeNavigationController:UINavigationController {
/// Should the pop be prevented? Set this to `true` when you have changes which need to be protected
public var hasUnsavedChanges:Bool = false
/// Show a prompt on the top most screen asking the user if they wish to proceed with the pop
/// - Parameter discardCallback: The callback to use if the user opts to discard
private func confirmDiscardChanges(discardCallback:#escaping (()->())) {
let alertController = UIAlertController.init(title: "Discard changes", message: "Are you sure you want to discard any unsaved changes?", preferredStyle: UIAlertController.Style.alert)
alertController.addAction(UIAlertAction.init(title: "Discard", style: UIAlertAction.Style.destructive, handler: { (_) in
discardCallback()
//User elected to discard and so, at this point, they no longer have changes to save
self.hasUnsavedChanges = false
}))
alertController.addAction(UIAlertAction.init(title: "Cancel", style: UIAlertAction.Style.cancel, handler: nil))
self.topViewController?.present(alertController, animated: true, completion: nil)
}
override func popViewController(animated: Bool) -> UIViewController? {
//If there aren't unsaved changes, popping is safe
if !hasUnsavedChanges {
return super.popViewController(animated: animated)
}else {
//Changes have been made. Block the pop and first check with the user before continuing
confirmDiscardChanges {
super.popViewController(animated: animated)
}
return nil
}
}
}
and when you want to enable discard protection from any child view controllers, simply use:
(self.navigationController as? DiscardSafeNavigationController)?.hasUnsavedChanges = true
and then any time the navigation controller is asked to pop it, the navigation controller will ask the user first.
Here's a simple action sheet,
let choice = UIAlertController(title: "Choose", message: nil, preferredStyle: .actionSheet)
choice.addAction(UIAlertAction(title: "Camera", style: .default, handler: { _ in
self.happyCamera() }))
choice.addAction(UIAlertAction(title: "Album", style: .default, handler: { _ in
self.happyAlbum() }))
choice.addAction(UIAlertAction.init(title: "Cancel", style: .cancel, handler: nil))
somewhere?.present(choice, animated: false, completion: nil)
When the action sheet appears (note that present#animated is false) it just clicks on to the screen, no cheesey animation.
However, when the user taps one of the three choices, or, taps "off", the action sheet leaves the screen by using the cheesey animation.
(In 10.3 specifically, it slides downwards off the screen.)
Is there a way to turn off that exit animation?
If you subclass UIAlertController...it doesn't work?
As DS suggests below, you could subclass UIAlertController.
However - strangely - it does nothing. Here's a test
func _test() {
let msg = SuperiorUIAlertController(
title: "Hello", message: "Hello",
preferredStyle: UIAlertControllerStyle.alert)
msg.addAction(UIAlertAction(
title: "OK", style: UIAlertActionStyle.default,
handler: nil))
let win = UIWindow(frame: UIScreen.main.bounds)
let vc = UIViewController()
vc.view.backgroundColor = .clear
win.rootViewController = vc
win.windowLevel = UIWindowLevelAlert + 1
win.makeKeyAndVisible()
vc.present(msg, animated: false, completion: nil)
}
class SuperiorUIAlertController: UIAlertController {
override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) {
print("You should see this! \(flag)")
self.dismiss(animated: false, completion: completion)
}
}
Indeed, the text "You should see this" never appears.
I hate to answer my own question, but as of late 2017, there is no way. Weird right?
Hope this fact helps someone.
The one more way you can Override dismiss method of viewController. If you don't wanna override other animations check animated flag value or make a flag in below method.
Make your AlertController globally
var choice = UIAlertController()
Make sure add this method in your viewController which you are presented alert
Dismiss presented alert without animation like below
override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) {
self.choice.dismiss(animated: false, completion: nil)
}
Try subclassing UIAlertController like this:
class InstantCloseAlertController: UIAlertController {
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
UIView.setAnimationsEnabled(false)
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
UIView.setAnimationsEnabled(true)
}
}
I have an app with a popover. As I'm coming out of the popover. I am dismissing the popover via a UIAlertController (user answers Yes). Before dismissing the popover, though, I am calling a function on the delegate. Within that function is another UIAlertController. The second UIAlertController is not displaying because of the following error:
Attempting to load the view of a view controller while it is deallocating is not allowed and may result in undefined behavior.
To demo this here, I created a quick project that shows the problem. It is just a view controller with a button that calls the popover and a button on the popover that closes it and calls a delegate function containing another UIAlertController.
This is the code for the view controller that calls the popover:
//Delegate function called from popover
func doSomeStuff() {
let alert = UIAlertController(title: "Some Stuff", message: "Do you want to do some stuff", preferredStyle: .Alert)
alert.addAction(UIAlertAction(title: "Yes", style: .Default, handler: {action in
print("We did something here.")
}))
alert.addAction(UIAlertAction(title: "No", style: .Cancel, handler: nil))
presentViewController(alert, animated: true, completion: nil)
}
#IBAction func callPopover(sender: UIButton) {
let popoverVC = self.storyboard?.instantiateViewControllerWithIdentifier("PopoverView") as! PopoverController
popoverVC.modalPresentationStyle = UIModalPresentationStyle.Popover
popoverVC.preferredContentSize = CGSizeMake(200, 200)
if let popoverController = popoverVC.popoverPresentationController {
popoverController.backgroundColor = UIColor.lightGrayColor()
popoverController.permittedArrowDirections = UIPopoverArrowDirection(rawValue: 0)
popoverController.sourceRect = CGRectMake(CGRectGetMidX(self.view.bounds), CGRectGetMidY(self.view.bounds),0,0)
popoverController.sourceView = callPopoverButton
popoverController.delegate = self
popoverVC.delegate = self
self.presentViewController(popoverVC, animated: true, completion: nil)
}
}
//Allows popover to present on devices besides iPad.
func adaptivePresentationStyleForPresentationController(controller: UIPresentationController) -> UIModalPresentationStyle{
return UIModalPresentationStyle.None
}
The callPopover function is the action for the button on the first screen.
This is the code for the popover screen:
var delegate: ViewController!
override func viewDidLoad() {
super.viewDidLoad()
}
#IBAction func returnToMainView(sender: UIButton) {
let alert = UIAlertController(title: "Dismiss Popover", message: "Do you want to dismiss this popover?", preferredStyle: .Alert)
alert.addAction(UIAlertAction(title: "Yes", style: .Default, handler: {action in
self.delegate.doSomeStuff()
self.dismissViewControllerAnimated(true, completion: nil)
}))
alert.addAction(UIAlertAction(title: "No", style: .Cancel, handler: nil))
self.presentViewController(alert, animated: true, completion: nil)
}
When tapping the popover button on the first screen, the popover displays correctly:
Tapping the return button displays the alert:
Clicking Yes returns from the alert and should display the second alert, but that's where I get the error.
I think that the second alert is not displaying because the popover has not finished being dismissed, but have no idea how to get around it.
I see that the you are presenting the second popup view controller from the first popup view controller which is being dismissed. That is causing the issue.
Instead why can't you present the second view controller from the navigation controller instead of first popup view controller.
Change this line of presentViewController(alert, animated: true, completion: nil) to self.navigartionController.presentViewController(alert, animated: true, completion: nil) in yourdoSomeStuff() method so that the first popup can dismiss freely and the navigation controller actually presents your second popup. Hope this helps!
I would like to prevent the UIAlertController from dismissing.
I have a UIAlertAction that simply appends a string into the UIAlertTextField, however, once tapped it dismisses the view controller [undesired]. I've tried adding an NSNotification with undesired results.
UIAlertAction *pasteMessage = [UIAlertAction actionWithTitle:#"Paste Message" style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) {
UITextField *textField = alertC.textFields.firstObject;
textField.text = [textField.text stringByAppendingString:[NSString stringWithFormat:#"%#", copiedString]];
}];
I've also tried setting no to pasteMessage by:
[alertC canPerformAction:#selector(dismissViewControllerAnimated:completion:) withSender:pasteMessage];
-(void)dismissViewControllerAnimated:(BOOL)flag completion:(void (^)(void))completion {
UIAlertController *alertController = (UIAlertController *)self.presentedViewController;
UIAlertAction *paste = alertController.actions.firstObject;
if (paste) {
flag = NO;
} else {
flag = YES;
}
}
Edit, i'm not looking to prevent the tapping of UIAlertAction i'm looking to prevent the UIAlertController from dismissing when tapping on said action. The action can be enabled/disabled whatever, but my goal is to simply paste the copied message into the UITextField by pressing an action (hence the reason I don't want it to be dismissed)
I also realize setting the BOOL to dismissViewControllerAnimated: simply sets it to not animate the view controllers dismissal, I don't want it to imply it was for stopping the actual dismissal process. Simply offering the things I've tried in relation to my goal. I've also tried presenting a new UIAlertController when selecting pasteMessage auto-populating the new UIAlertControllers textField with the copied message, it works, but I feel like it's too hacky for what could be done.
EDIT: Updated for Swift 5
EDIT: Updated to include #skywalker's feedback
So I actually got this to work. In short, it involves adding a long-press gesture recognizer to the UIAlertController that triggers before the dismissal occurs.
First, create lazily loaded computed variables in your view controller for your UIAlertController and the UIAlertAction you want to prevent from triggering so that self is accessible via the gesture recognizer's selector method you'll be attaching to the alert (self in the selector insinuates that all of this is inside a view controller).
lazy var alert: UIAlertController = {
let alert = UIAlertController(title: "Title", message: "Message", preferredStyle: .alert)
alert.addTextField(configurationHandler: nil)
let appendAction = self.appendAction
let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: nil)
alert.addAction(appendAction)
alert.addAction(cancelAction)
let gestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(append(sender:)))
gestureRecognizer.minimumPressDuration = 0.0
alert.view.addGestureRecognizer(gestureRecognizer)
return alert
}()
lazy var appendAction: UIAlertAction = {
return UIAlertAction(title: "Paste Message", style: .default, handler: nil)
}()
Make sure your gesture recognizer above is a UILongPressGestureRecognizer set with a minimum press duration of 0. That way you can access the state of the gesture (for when the user touches down) before the action is triggered fully. There you can disable the UIAlertAction, implement your custom code, and reenable the action after the gesture has completed (user has touched up). See below:
#objc func append(sender: UILongPressGestureRecognizer) {
switch sender.state {
case .began:
appendAction.isEnabled = false
case .ended:
// Do whatever you want with the alert text fields
print(alert.textFields?[0].text)
appendAction.isEnabled = true
default:
return
}
}
Also, make sure that the view controller owning the presentation of this alert conforms to UIGestureRecognizerDelegate in order to recognize simultaneous gestures.
extension YourViewController: UIGestureRecognizerDelegate {
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
}
Then, just present the UIAlertController wherever.
func showAlert() {
self.present(alert, animated: true, completion: nil)
}
This is obviously a hack, but there's no other way that I know to achieve this without a hack since it's not meant to be achieved. For example, the gesture recognizer is tied to the UIAlertController so the user can trigger that method if they tap anywhere on the alert (besides the cancel button).
ORIGINAL ANSWER:
This is as close as I could come to a hack-a-round. If there was some way to customize the dismissal transition time to nothing then you could set animated: to false and it would look like the same alert, but I don't think it's possible
class ViewController: UIViewController {
#IBAction func alert(sender: AnyObject) {
let alert = UIAlertController(title: "title", message: "message", preferredStyle: .Alert)
alert.addTextFieldWithConfigurationHandler(nil)
let appendAction = UIAlertAction(title: "Append text", style: .Default) { _ in
var textField = alert.textFields![0] as UITextField
// Append text here
self.presentViewController(alert, animated: true, completion: nil)
}
let cancelAction = UIAlertAction(title: "Cancel", style: .Cancel, handler: nil)
alert.addAction(appendAction)
alert.addAction(cancelAction)
self.presentViewController(alert, animated: true, completion: nil)
}
}
I'm only familiar with swift
Pretty much the same question is answered here
Text field on alert supports paste option, so there is no real reason to have separate button on alert to indicate "paste" option.
Otherwise you should mimic UIAlertController and reimplement it with desiread behavior.