Swift UIKit / presenting UIAlert blocks pushing VC - ios

I got simple UIViewController + UIAlert extension:
extension UIViewController {
func alert(title: String = "", message: String){
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: Localized.ok(), style: .default, handler: nil))
present(alert, animated: true, completion: nil)
}
}
Within the ViewController I got a method:
func findUser() {
userService.findUser { (userinfo, error) in
if error != nil {
if let errText = error?.localizedDescription {
self.alert(message: errText)
}
self.doAuth()
return
}
}
}
this doAuth() method should redirect to loginViewController using:
navigationController?.pushViewController(loginViewController, animated: false)
The problem is, that in this scenario, this push doesn't work (nothing appears) (I click OK button on the alert, alert dissapears but loginViewController is not pushed)
I refactored extension a little bit:
extension UIViewController {
func alert(title: String = "", message: String, action completion: (() -> Void)? = nil){
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: Localized.ok(), style: .default, handler: { _ in
completion?()
}))
present(alert, animated: true, completion: nil)
}
}
so findUser() method is calling doAuth() in differently:
func findUser() {
userService.findUser { (userinfo, error) in
if error != nil {
if let errText = error?.localizedDescription {
self.alert(message: errText){ [weak self] in
self?.doAuth()
}
}
return
}
}
}
and it works!
Problem is I have no idea why. And what could have happened in the first scenario?
I feel it should be some simple explanation, but I can't figure it out.
Edit
The explanation is simple and was printed in the console:
pushViewController:animated: called on <UINavigationController 0x7f86050b4400>
while an existing transition or presentation is occurring; the navigation stack
will not be updated.
So doAuth() (with pushing VC method) was called while alert was visible/presented, so alert took the focus and VC couldn't be pushed.
cc: #Paulw11 # cookednick

Problem is navigating and presenting in same if statement
If error != nil , means it only show the alert not try to doAuth().
But you are calling doAuth() in same if block then it is trying to present alert as well as to navigate
func findUser() {
userService.findUser { (userinfo, error) in
if error != nil {
if let errText = error?.localizedDescription {
self.alert(message: errText)
}
return
}
//Out side if block
self.doAuth()
}
}

Related

iOS - UIAlertController actions not triggering handler

I'm extremely new to iOS. I'm trying to show a dialog to the user to get some input, but the actions are never triggered. I've been searching on the net for hours and no answer seem to work for me.
Here's the function I'm trying to use to show the dialog:
private func showAmountDialog(type: String, onComplete: #escaping (Double) -> Void) {
let alert = UIAlertController(title: "Enter an amount", message: nil, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: LzStrings.Common_Cancel, style: .cancel, handler: nil))
alert.addTextField(configurationHandler: { textField in
textField.placeholder = "0.00 \(type)"
textField.keyboardType = .decimalPad
})
alert.addAction(UIAlertAction(title: LzStrings.Common_OK, style: .default) { (UIAlertAction) in
if let input = alert.textFields?.first?.text, let amount = Double(input) {
print("Your amount: \(amount)")
}
})
self.present(alert, animated: true)
}
self here is my ViewController which has a parent of UIViewController type and several other protocols.
What I might be doing wrong?
EDIT: The way I knew it isn't executing is using break-points and not by relying on print("...")
Also, since I added the TextField right before adding the action, the nullability check is useless and the textFields.first is never nil, so in both cases, a break-point should be triggered or the print("...") should be executed, which neither of them is happening.
EDIT 2: Since the if statement can do a little distraction, I edited my code this way and tested again:
alert.addAction(UIAlertAction(title: LzStrings.Common_OK, style: .default) { (UIAlertAction) in
if let input = alert.textFields?.first {
if let amount = Double(input.text ?? "") {
print("Your amount: \(amount)")
} else {
print("Can't cast this string to double")
}
} else {
print("Text field is null")
}
})
Still, no feedback from the dialog.
PS: Even the Cancel button doesn't work.
EDIT 3: My dismiss function is overridden in the super class, but it passes completion closure normally:
override open func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) {
if let navigationController = self.navigationController as? NavigationController {
navigationController.dismiss(animated: flag, completion: completion)
} else {
super.dismiss(animated: flag, completion: completion)
}
}
After having a conversation with one of my colleagues, we found out that to show standard UIAlertController we must use this:
self.view.window!.rootViewController?.present(alert, animated: true, completion: nil)
Instead of this
self.present(alert, animated: true, completion: nil)
It fixed my issue. I hope someone will find this helpful.
Another option is to use an extention for ViewController:
extension UIViewController {
//Show a basic alert
func showAlert(alertText : String, alertMessage : String) {
let alert = UIAlertController(title: alertText, message: alertMessage, preferredStyle: UIAlertControllerStyle.alert)
alert.addAction(UIAlertAction(title: "Got it", style: UIAlertActionStyle.default, handler: nil))
//Add more actions as you see fit
self.present(alert, animated: true, completion: nil)
}
}

How to detect Add button presses in PKAddPassesViewController

I'm trying to detect when the users tap on the "Add" button in the PKAddPassesViewController.
I added addPassesViewControllerDidFinish() so that when passVC is dismissed, function addPassesViewControllerDidFinish() will be called.
override func viewDidLoad() {
self.pass = try PKPass(data: downloadedData! as Data)
let passVC = PKAddPassesViewController(pass: self.pass)
self.present(passVC!, animated: true)
// when passVC is dimissed by the user, addPassesViewControllerDidFinish is expected to be called, but it never gets called.
}
func addPassesViewControllerDidFinish(_ controller: PKAddPassesViewController) {
print("enter DidFinish")
let passLib = PKPassLibrary()
// Get your pass
guard let pass = self.pass else { return }
if passLib.containsPass(pass) {
print("if start")
// Show alert message for example
let alertController = UIAlertController(title: "", message: "Successfully added to Wallet", preferredStyle: .alert)
alertController.addAction(UIAlertAction(title: "OK", style: .default, handler: { _ in
controller.dismiss(animated: true, completion: nil)
}))
controller.show(alertController, sender: nil)
print("if end")
} else {
// Cancel button pressed
print("else start");
controller.dismiss(animated: true, completion: nil)
print("else end");
}
}
However, when passVC is dimissed by the user, func addPassesViewControllerDidFinish() never gets called at all.
There are three things you should fix:
1. Extend PKAddPassesViewControllerDelegate in your ViewController class.
2. Double check if you added delegate to your PKAddPassesViewController: VC?.delegate = self, which will link your delegate to PKAddPassesViewControllerDelegate.
3. Inside addPassesViewControllerDidFinish, dismiss controller first. Then do whatever you want inside passLib.containPass. The alertController is no longer belonged to the controller, maybe to its parent view.

Prevent presenting the UIAlertViewController after navigating to the other view

I have one scenario when the user did not use the application for more than 5 min app will show a popup with session expiration message.
The code for session expiration is added in the appDelegate and from there the popup will be presented on the current view controller.
code is
#objc func applicationDidTimeout(notification: NSNotification) {
if (window?.rootViewController?.isKind(of: UITabBarController.self))! {
for view in window?.rootViewController?.view.subviews ?? [(window?.rootViewController?.view)!] {
if view.isKind(of: MBProgressHUD.self) {
return
}
}
if window?.rootViewController?.presentedViewController != nil {
window?.rootViewController?.dismiss(animated: true, completion: {
self.showMessage(message: Message.sessionTimeout)
})
} else {
self.showMessage(message: Message.sessionTimeout)
}
}
}
fileprivate func showMessage(message: String) {
let alert = UIAlertController(title: appName, message: message, preferredStyle: .alert)
let actionOkay = UIAlertAction(title: "OK", style: .default) { (action) in
DispatchQueue.main.async {
UIView.transition(with: self.window!, duration: 0.3, options: UIView.AnimationOptions.transitionCrossDissolve, animations: {
CommonFunctions.setLoginAsRootVC()
}, completion: nil)
}
}
alert.addAction(actionOkay)
self.window?.rootViewController?.present(alert, animated: true, completion: nil)
}
Now if the user is doing some data entry and at that time, if the user leaves application ideal for 5 min or more the keyboard will dismiss and the session expiration message shown there.
But as the text field's delegate method textFieldShouldEndEditing has some validation and if that validation fails it shows a popup with the message and ok button.
So when the user taps on the ok button in the session expiration message popup, it will redirect the user to the login screen but due to the text field's delegate method validation, it shows one pop up in the login screen.
Code for the validation fail message popup is
fileprivate func showErrorMessage(message: String) {
let alert = UIAlertController(title: appName, message: message, preferredStyle: .alert)
let actionOkay = UIAlertAction(title: "OK", style: .default) { (action) in
self.txtField.becomeFirstResponder()
}
alert.addAction(actionOkay)
self.present(alert, animated: true, completion: nil)
}
How to prevent the popup from being present in the login screen?
I try to get the proper way to prevent the popup from appearing on the login screen.
But Finally, I found one heck to solve this issue.
I have declared one boolean in AppDelegate and set it's value to false when I want to prevent the popup from appearing and then revert it back to true when I want to show the popup.
I know this is not the elegant or efficient solution for the issue, but it works for now.
If anyone knows the better answer can post here, I'm still open to any better solution.
#objc func applicationDidTimeout(notification: NSNotification)
{
let visibleView : UIViewController = self.getVisibleViewControllerFrom(self.window?.rootViewController)!
self.showMessage(message: Message.sessionTimeout,Controller: visibleView)
}
fileprivate func showMessage(message: String , Controller : UIViewController) {
let alert = UIAlertController(title: appName, message: message, preferredStyle: .alert)
let actionOkay = UIAlertAction(title: "OK", style: .default) { (action) in
//Now apply your code here to set login view controller as rootview
// This controller is for demo
window!.rootViewController = UIStoryboard(name: "Main", bundle:
nil).instantiateViewController(withIdentifier: "loginview")
window!.makeKeyAndVisible()
}
alert.addAction(actionOkay)
Controller.present(alert, animated: true, completion: nil)
}
//MARK:- Supporting method to get visible viewcontroller from window
func getVisibleViewControllerFrom(_ vc: UIViewController?) -> UIViewController? {
if let nc = vc as? UINavigationController {
return self.getVisibleViewControllerFrom(nc.visibleViewController)
} else if let tc = vc as? UITabBarController {
return self.getVisibleViewControllerFrom(tc.selectedViewController)
} else {
if let pvc = vc?.presentedViewController {
return self.getVisibleViewControllerFrom(pvc)
} else {
return vc
}
}
}
Try this code, I've use this code many times may be it's work for you.

Show and dismis UIAlertController in Swift 4

I declared a global variable for UIAlertViewController for me to be able to show and dismiss it in different method inside my class.
I displayed two kinds of alert: First, alert with button which will be displayed when an error is encountered or to display an information message. Second is an alert without button which will be displayed like a progress message.
Here is the sample code:
private var alert: UIAlertController? // global declaration
private func showProgressMessage(sender viewController: UIViewController, message alertMessage: String)
{
DispatchQueue.main.async
{
self.alert= UIAlertController(title: "", message: alertMessage, preferredStyle: .alert)
viewController.present(self.alert!, animated: true, completion: nil)
}
}
private func showAlertMessage(sender viewController: UIViewController, title alertTitle: String, message alertMessage: String)
{
DispatchQueue.main.async
{
self.alert= UIAlertController(title: alertTitle, message: alertMessage, preferredStyle: .alert)
self.alert!.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
viewController.present(self.alert!, animated: true, completion: nil)
}
}
private func method1()
{
DispatchQueue.global().async
{
// some code here
self.showProgressMessage(sender: self, message: "Processing...")
// some code here
}
}
private func method2()
{
// some code here
self.alert!.dismiss(animated: false)
{
self.showAlertMessage(sender: self, message: "Done")
}
self.displayOtherViewController()
}
private func displayOtherViewController()
{
self.alert?.dismiss(animated: false)
{
if let viewController = self.storyboard?.instantiateViewController(withIdentifier: "Sample")
{
let view = viewController as! SampleViewController
view .modalTransitionStyle = .crossDissolve
self.present(view , animated: true, completion: nil)
}
}
}
In method2, displaying the alert again will take a few seconds to display, same with the view controller.
What is the proper way to show and dismis the UIAlertController in Swift 4?
Seems like your code is initiated from a background thread.
Even dismiss must be called on main thread
Try this:
private func method2() {
DispatchQueue.main.async {
self.alert!.dismiss(animated: false) {
self.showAlertMessage(sender: self, message: "Done")
}
self.displayOtherViewController()
}
}

Check if alertview is displayed

In my app I display tow alert views. The second alert view should pop up if the first has been closed. Now I check if an alert view is displayed like this:
let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
let viewController = appDelegate.window!.rootViewController as! ViewController
if viewController.view.window != nil {
}
else {
let alertView = UIAlertController(title: NSLocalizedString("IAP", comment: "comment"), message: NSLocalizedString("IAP1", comment: "comment"), preferredStyle: .Alert)
alertView.addAction(UIAlertAction(title: "Ok", style: .Cancel, handler: nil))
viewController.presentViewController(alertView, animated: true, completion: nil)
}
I send the 2nd alert view if the first isn't displayed anymore. But if the first view is still displayed the 2nd alert view doesn't pops up anymore. So my question is if there's a waiting line for alert views and how can I solve this problem?
You should define a handler for the first action and present the 2nd alertView within the handler.
So instead of
UIAlertAction(title: "Ok", style: .Cancel, handler: nil)
you should do
UIAlertAction(title: "Ok", style: .Cancel) { (action) -> Void in
// Present the other alertView
}
If you are using a navigation controller, the general way to see if an alert is already displayed is to check the presentedViewController property.
if let _ = navigationController.presentedViewController {
print("is already presenting \(navigationController.presentedViewController)")
} else {
navigationController.presentViewController(alert, animated:true, completion:nil)
}
For a fast patch in any UIViewController : (Xcode 8.3.1 & Swift 3.1)
func blabla() {
if presentedViewController != nil {
delay(0.5, closure: {
self.blabla()
})
return
}
// other alert code here
}
Simple way to check in Swift
var isAlertViewPresenting: Bool {
get {
if self.presentedViewController is UIAlertController {
return true
}
return false
}
}

Resources