When a user purchased completion handler notify me and dismiss viewController. However, I want to display/show an alert to the user after viewController dismissed. At the moment when I step through in the debugger, it goes through the code but the alert isn't being shown. Still getting inbuilt in apple one that says All set. Is there a way I can display my alert after dismissing the viewController.
override func viewWillDisappear(_ pAnimated: Bool) {
super.viewWillDisappear(pAnimated)
self.notifyForUserHasPurchasedProduct {
self.presentingViewController?.dismiss(animated: true, completion: {
UIAlertController.bs_showAlertFrom(self, title: "AppName", message: "Thank you. Your purchase was successful")
})
}
}
You need to call self.present(alert, animated: true) to show alert. When ViewController self is not present, you need to change code to presentedViewController.present(alert, animated: true)
I have builded some functions:
extension UIViewController {
func topMostViewController() -> UIViewController {
if let presented = self.presentedViewController {
return presented.topMostViewController()
}
if let navigation = self as? UINavigationController {
return navigation.visibleViewController?.topMostViewController() ?? navigation
}
if let tab = self as? UITabBarController {
return tab.selectedViewController?.topMostViewController() ?? tab
}
return self
}
}
func getRootController () -> UIViewController { // function in global scope
return (UIApplication.shared.delegate?.window!!.rootViewController)!
}
And then use them like here:
override func viewWillDisappear(_ pAnimated: Bool) {
super.viewWillDisappear(pAnimated)
self.notifyForUserHasPurchasedProduct {
self.presentingViewController?.dismiss(animated: true, completion: {
let alert = UIAlertController(title: "AppName", message: "Thank you. Your purchase was successful", preferredStyle: .alert)
let topC = getRootController().topMostViewController()
topC.present(alert, animated: true, completion: nil)
})
}
}
Related
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.
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()
}
}
I have a dispatch async where I expect 4 alerts pop up on the screen..and then each get dismissed before a new alert is to be shown. (I've set a 3 second delay in between my Alert)
class ViewController: UIViewController {
var counter = 1
override func viewDidLoad() {
super.viewDidLoad()
for _ in 1...4{
DispatchQueue.main.asyncAfter(deadline: .now() + 3.0 * Double(counter) , execute: {
print("called")
self.showAlert()
})
}
}
func showAlert(){
let alert = UIAlertController(title: "sampleTitle \(counter)", message: "sampleMessage \(counter)", preferredStyle: .alert)
let action = UIAlertAction(title: "OK", style: .default, handler: nil)
alert.addAction(action)
counter += 1
if self.presentedViewController != nil {
dismiss(animated: true, completion: nil)
UIApplication.topViewController()?.present(alert, animated: true, completion: nil)
}else{
UIApplication.topViewController()?.present(alert, animated: true, completion: nil)
}
}
}
Problem1: But for some reason the entire for loop is executed without any delay in between. I'm guessing I'm not understanding something about main queue being a serial queue.
Problem2: And I also get the following logs in my console, even though I'm dismissing the presentedViewController.
called
called
called
called
2017-06-26 11:10:57.000 topViewAndAlertTest[3360:210226] Warning: Attempt to dismiss from view controller <topViewAndAlertTest.ViewController: 0x7fe630c03350> while a presentation or dismiss is in progress!
2017-06-26 11:10:57.001 topViewAndAlertTest[3360:210226] Warning: Attempt to present <UIAlertController: 0x7fe630c04180> on <UIAlertController: 0x7fe630f06fe0> while a presentation is in progress!
2017-06-26 11:10:57.001 topViewAndAlertTest[3360:210226] Warning: Attempt to dismiss from view controller <topViewAndAlertTest.ViewController: 0x7fe630c03350> while a presentation or dismiss is in progress!
2017-06-26 11:10:57.001 topViewAndAlertTest[3360:210226] Warning: Attempt to present <UIAlertController: 0x7fe630c06b40> on <UIAlertController: 0x7fe630f06fe0> while a presentation is in progress!
FYI My topviewcontroller is using the code from this answer
Problem3: Only 2 alerts pop...I never see the 3rd, 4th alerts!
EDIT:
After rmaddy's suggestion, my errors are slightly changed:
called
called
called
called
2017-06-26 11:59:33.417 topViewAndAlertTest[4834:441163] Warning: Attempt to dismiss from view controller <topViewAndAlertTest.ViewController: 0x7fb596d05a30> while a presentation or dismiss is in progress!
2017-06-26 11:59:33.417 topViewAndAlertTest[4834:441163] Warning: Attempt to dismiss from view controller <topViewAndAlertTest.ViewController: 0x7fb596d05a30> while a presentation or dismiss is in progress!
I get 2 less warnings. But still: As soon as alert 1 is on screen, alert 2 dismisses it and that's it! No delay no 3rd,4th alert!
Details
xCode 8.3.2, Swift 3.1
Full Code
import UIKit
class ViewController: UIViewController {
var counter = 1
private var alertViewController: UIAlertController?
override func viewDidLoad() {
super.viewDidLoad()
DispatchQueue.global(qos: .utility).async {
for _ in 1...4 {
sleep(2)
DispatchQueue.main.async {
print("called")
self.showAlert()
}
}
}
}
private func createAlertView() -> UIAlertController {
let alertViewController = UIAlertController(title: "sampleTitle \(counter)", message: "sampleMessage \(counter)", preferredStyle: .alert)
let action = UIAlertAction(title: "OK", style: .default, handler: nil)
alertViewController.addAction(action)
return alertViewController
}
func showAlert(){
let presentAlert = {
DispatchQueue.main.async { [weak self] in
if let _self = self {
_self.alertViewController = _self.createAlertView()
UIApplication.topViewController()?.present(_self.alertViewController!, animated: true, completion: nil)
}
}
}
DispatchQueue.main.async { [weak self] in
if let alertViewController = self?.alertViewController {
alertViewController.dismiss(animated: true) {
presentAlert()
}
} else {
presentAlert()
}
self?.counter += 1
}
}
}
extension UIApplication {
class func topViewController(base: UIViewController? = (UIApplication.shared.delegate as! AppDelegate).window?.rootViewController) -> UIViewController? {
if let nav = base as? UINavigationController {
return topViewController(base: nav.visibleViewController)
}
if let tab = base as? UITabBarController {
if let selected = tab.selectedViewController {
return topViewController(base: selected)
}
}
if let presented = base?.presentedViewController {
return topViewController(base: presented)
}
return base
}
}
I have trouble to display my UIAlertController because I'm trying to show it in a Class which is not an ViewController.
I already tried adding it:
var alertController = UIAlertController(title: "Title", message: "Message", preferredStyle: .Alert)
UIApplication.sharedApplication().keyWindow?.rootViewController?.presentViewController(alertController, animated: true, completion: nil)
Which is not working...
I didn't find any solution that worked for me yet.
I wrote this extension over UIAlertController to bring back show().
It uses recursion to find the current top view controller:
extension UIAlertController {
func show() {
present(animated: true, completion: nil)
}
func present(animated: Bool, completion: (() -> Void)?) {
if let rootVC = UIApplication.shared.keyWindow?.rootViewController {
presentFromController(controller: rootVC, animated: animated, completion: completion)
}
}
private func presentFromController(controller: UIViewController, animated: Bool, completion: (() -> Void)?) {
if
let navVC = controller as? UINavigationController,
let visibleVC = navVC.visibleViewController
{
presentFromController(controller: visibleVC, animated: animated, completion: completion)
} else if
let tabVC = controller as? UITabBarController,
let selectedVC = tabVC.selectedViewController
{
presentFromController(controller: selectedVC, animated: animated, completion: completion)
} else if let presented = controller.presentedViewController {
presentFromController(controller: presented, animated: animated, completion: completion)
} else {
controller.present(self, animated: animated, completion: completion);
}
}
}
Now it's as easy as:
var alertController = UIAlertController(title: "Title", message: "Message", preferredStyle: .Alert)
alertController.show()
This should work.
UIApplication.sharedApplication().windows[0].rootViewController?.presentViewController(...)
Create a helper function that you call from the current view controller and pass the current view controller as a parameter:
func showAlertInVC(
viewController: UIViewController,
title: String,
message: String)
{
//Code to create an alert controller and display it in viewController
}
If you solution is not working it probably because of there is no window at that moment. I had the same problem when I was trying to show alert view in application:DidFinishLoadingWithOptions method. In this case my solution was to check if root view controller is available, and if it's not, then add notification for UIApplicationDidBecomeActiveNotification
NSNotificationCenter.defaultCenter().addObserverForName(UIApplicationDidBecomeActiveNotification,
object: nil,
queue: NSOperationQueue.mainQueue()) {
(_) in
//show your alert by using root view controller
//remove self from observing
}
}
I've just created a Single View Application project with ViewController class. I would like to show a UIAlertController from a function which is located inside my own class.
Here is my class with an alert.
class AlertController: UIViewController {
func showAlert() {
var alert = UIAlertController(title: "abc", message: "def", preferredStyle: .Alert)
self.presentViewController(alert, animated: true, completion: nil)
}
}
Here is ViewController which executes the alert.
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
}
#IBAction func showAlertButton(sender: AnyObject) {
var alert = AlertController()
alert.showAlert()
}
}
This is what I get instead of a beautiful alert.
Warning: Attempt to present UIAlertController: 0x797d2d20 on Sprint1.AlertController: 0x797cc500 whose view is not in the window hierarchy!
What should I do?
If you're instancing your UIAlertController from a modal controller, you need to do it in viewDidAppear, not in viewDidLoad or you'll get an error.
Here's my code (Swift 4):
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
let alertController = UIAlertController(title: "Foo", message: "Bar", preferredStyle: .alert)
alertController.addAction(UIAlertAction(title: "OK", style: .cancel, handler: nil))
present(alertController, animated: true, completion: nil)
}
Let's look at your view hierarchy. You have a ViewController.
Then you are creating an AlertController, you are not adding it to your hierarchy and you are calling an instance method on it, that attempts to use the AlertController as presenting controller to show just another controller (UIAlertController).
+ ViewController
+ AlertController (not in hierarchy)
+ UIAlertController (cannot be presented from AlertController)
To simplify your code
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
}
#IBAction func showAlertButton(sender: AnyObject) {
var alert = UIAlertController(title: "abc", message: "def", preferredStyle: .Alert)
self.presentViewController(alert, animated: true, completion: nil)
}
}
This will work.
If you need the AlertController for something, you will have to add it to the hierarchy first, e.g. using addChildViewController or using another presentViewController call.
If you want the class to be just a helper for creating alert, it should look like this:
class AlertHelper {
func showAlert(fromController controller: UIViewController) {
var alert = UIAlertController(title: "abc", message: "def", preferredStyle: .Alert)
controller.presentViewController(alert, animated: true, completion: nil)
}
}
called as
var alert = AlertHelper()
alert.showAlert(fromController: self)
You can use below function to call alert from any where just include these method in AnyClass
class func topMostController() -> UIViewController {
var topController: UIViewController? = UIApplication.shared.keyWindow?.rootViewController
while ((topController?.presentedViewController) != nil) {
topController = topController?.presentedViewController
}
return topController!
}
class func alert(message:String){
let alert=UIAlertController(title: "AppName", message: message, preferredStyle: .alert);
let cancelAction: UIAlertAction = UIAlertAction(title: "OK", style: .cancel) { action -> Void in
}
alert.addAction(cancelAction)
AnyClass.topMostController().present(alert, animated: true, completion: nil);
}
Then call
AnyClass.alert(message:"Your Message")
Write the following 3 lines, all we need to do is this.
Swift 3.0
private func presentViewController(alert: UIAlertController, animated flag: Bool, completion: (() -> Void)?) -> Void {
UIApplication.shared.keyWindow?.rootViewController?.present(alert, animated: flag, completion: completion)
}
Swift 2.0
private func presentViewController(alert: UIAlertController, animated flag: Bool, completion: (() -> Void)?) -> Void {
UIApplication.sharedApplication().keyWindow?.rootViewController?.presentViewController(alert, animated: flag, completion: completion)
}
If you want to create a separate class for displaying alert like this, subclass NSObject not UIViewController.
And pass the ViewControllers reference from which it is initiated, to the showAlert function so that you can present alert view there.
Here is the code of an UIAlertController in a Utility.swift class (not a UIViewController) in Swift3, Thanks Mitsuaki!
private func presentViewController(alert: UIAlertController, animated flag: Bool, completion: (() -> Void)?) -> Void {
UIApplication.shared.keyWindow?.rootViewController?.present(alert, animated: flag, completion: completion)
}
func warningAlert(title: String, message: String ){
let alert = UIAlertController(title: title, message: message, preferredStyle: UIAlertControllerStyle.alert)
alert.addAction(UIAlertAction(title: "OK", style: UIAlertActionStyle.default, handler: { (action) -> Void in
}))
// self.present(alert, animated: true, completion: nil)
presentViewController(alert: alert, animated: true, completion: nil)
}
let alert = UIAlertController(title: "", message: "YOU SUCCESSFULLY\nCREATED A NEW\nALERT CONTROLLER", preferredStyle: .alert)
func okAlert(alert: UIAlertAction!)
{
}
alert.addAction(UIAlertAction(title: "OK", style: .default, handler: okAlert))
let scenes = UIApplication.shared.connectedScenes
let windowScene = scenes.first as? UIWindowScene
let window = windowScene?.windows.first
var rootVC = window?.rootViewController
if var topController = rootVC
{
while let presentedViewController = topController.presentedViewController
{
topController = presentedViewController
}
rootVC = topController
}
rootVC?.present(alert, animated: true, completion: nil)
It helped me to stick a slight delay between the viewDidLoad method and firing the alert method:
[self performSelector:#selector(checkPhotoPermission) withObject:nil afterDelay:0.1f];
This worked for me:
- (UIViewController *)topViewController{
return [self topViewController:[UIApplication sharedApplication].keyWindow.rootViewController];
}
- (UIViewController *)topViewController:(UIViewController *)rootViewController
{
if (rootViewController.presentedViewController == nil) {
return rootViewController;
}
if ([rootViewController.presentedViewController isMemberOfClass:[UINavigationController class]]) {
UINavigationController *navigationController = (UINavigationController *)rootViewController.presentedViewController;
UIViewController *lastViewController = [[navigationController viewControllers] lastObject];
return [self topViewController:lastViewController];
}
UIViewController *presentedViewController = (UIViewController *)rootViewController.presentedViewController;
return [self topViewController:presentedViewController];
}
Implementation:
UIViewController * topViewController = [self topViewController];
Using with alert:
[topViewController presentViewController:yourAlert animated:YES completion:nil];
You can send an alert from any class in your app (that uses UIKit: #import <UIKit/UIKit.h> )
Source here.
// I always find it helpful when you want to alert from anywhere it's codebase
// if you find the error above mentioned in the question' title.
let controller = UIAlertController(title: "", message: "Alert!", preferredStyle: UIAlertController.Style.alert)
let action = UIAlertAction(title: "Cancel" , style: UIAlertAction.Style.cancel, handler: nil)
controller.addAction(action)
// Find Root View Controller
var rootVC = UIApplication.shared.windows.first?.rootViewController
if var topController = rootVC {
while let presentedViewController = topController.presentedViewController {
topController = presentedViewController
}
rootVC = topController
}
rootVC?.present(controller, animated: true, completion: nil)