How present a UIAlert in a Skscene - ios

When presenting a UIAlert in SKScene nothing shows up
Here is the code
var alertController = UIAlertController(title: "Nothing Selected",
message: "You have selected a picture.",
preferredStyle: UIAlertControllerStyle.alert)
alertController.addAction(UIAlertAction(title: "HI!", style: UIAlertActionStyle.cancel, handler: nil))
self.view?.window?.rootViewController?.present(alertController, animated: true, completion: nil)

From within the scene you need to present the Alert Controller at the root View Controller level.
if let vc = self.scene?.view?.window?.rootViewController {
vc.present(alertController, animated: true, completion: nil)
}

Try to present from the top view controller returned by this extension (taken from this post):
extension UIApplication {
class func topViewController(controller: UIViewController? = UIApplication.shared.keyWindow?.rootViewController) -> UIViewController? {
if let navigationController = controller as? UINavigationController {
return topViewController(controller: navigationController.visibleViewController)
}
if let tabController = controller as? UITabBarController {
if let selected = tabController.selectedViewController {
return topViewController(controller: selected)
}
}
if let presented = controller?.presentedViewController {
return topViewController(controller: presented)
}
return controller
}
}

Related

pushing ViewController over current ViewController from deeplink swift

I am using deeplink in my application and from the central area, I am trying push a screen in another ViewController based on my deeplink.
The issues I have is that when I cancel the pushed ViewController, the entire application stack is dismissed and I just want to pop back to the presenting viewcontroler.
func getCurrentNanvigationController() -> UINavigationController? {
//targeted UINavigationController
var navigationController: UINavigationController? = nil
if let nav = window?.rootViewController as? UINavigationController {
if let topNav = window?.topViewController()?.navigationController {
navigationController = topNav
}
else{
navigationController = nav
}
}
// Wallet Module is the stand alone module, which means its not embeded in the Navigator app
else if let tabBar = window?.rootViewController as? UITabBarController,
let nav = tabBar.selectedViewController as? UINavigationController {
navigationController = nav
}
else {
//should not happen, window root controller shouldbe be either UINavigationController or UITabBarController
}
return navigationController
}
public extension UIWindow {
func topViewController() -> UIViewController? {
var top = self.rootViewController
while true {
if let presented = top?.presentedViewController {
top = presented
} else if let nav = top as? UINavigationController {
top = nav.visibleViewController
} else if let tab = top as? UITabBarController {
top = tab.selectedViewController
} else {
break
}
}
return top
}
}
This is the currentViewController that I am trying to push another controller over from the deeplink
#objc func addNavBarItemTapped() {
let storyBoard = UIStoryboard(storyboard: .addVC, bundle: .main)
let controller = storyBoard.instantiateViewController(withIdentifier: "AddViewController")
self.navigationController?.pushViewController(controller, animated: true)
}
How can I effectively push over the AddViewController because that is the presenting ViewController and when I dismiss the presented ViewController from the navigation stack, I am not removing the entire app navigation but instead returning back to AddViewController
How the dismissal of this view controller is achieved is like this.
public extension UIViewController {
func alert(title: String,
message: String? = nil ,
attributedMessage: NSMutableAttributedString? = nil,
okAction: AlertActionButton = ("ok_button".fpxLocalizedText, .default, nil),
cancelAction: AlertActionButton = (nil, .cancel, nil),
complete: (() -> Void)? = nil) {
let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert)
if let attributedMessage = attributedMessage {
alertController.setValue(attributedMessage, forKey: "attributedMessage")
}
let oKAction = UIAlertAction(title: okAction.0, style: okAction.1, handler: okAction.2)
if let cancelButtonTitle = cancelAction.0 {
let cancelAction = UIAlertAction(title: cancelButtonTitle, style: cancelAction.1, handler: cancelAction.2)
alertController.addAction(cancelAction)
}
alertController.addAction(oKAction)
self.present(alertController, animated: true, completion: complete)
}
}
In the presented Viewcontroller
public func declineButtonClicked() {
alert(title: "decline_warning_title".fpxLocalizedText,
message: "decline_warning_message".fpxLocalizedText,
okAction: ("yes_button".fpxLocalizedText, .destructive, { _ in self.sendDeclineRequest() }),
cancelAction: ("decline_cancel_button".fpxLocalizedText, .cancel, { _ in self.dismiss(animated: true, completion: nil) }))
}
Also sometimes if I have a presented Viewcontroller and need to show this ViewController, it is often presented in the background of the presented viewcontroller
In iOs push duty perform with UINavigationController
wherever you need to push so you need to UINavigationController.
You should Create another UInavigationController in Deep Link,
let storyBoard = UIStoryboard(storyboard: .addVC, bundle: .main)
let controller = storyBoard.instantiateViewController(withIdentifier: "AddViewController")
let navigationController = UINavigationController(rootViewController:controller)
self.navigationController?.present(navigationController, animated: true, completion: nil)
or
window?.getCurrentNanvigationController().present(navigationController, animated: true, completion: nil)
so now you have a new navigation controller that work stand alone.

How to present UIAlertController in front of modal sheet in Swift

I'm trying to show an alert with a textfield from a modal in swift, but getting an error when I try to show it.
I have to use a UIAlertController because the default alert in Swift doesn't support Textfields currently.
I am calling my modal from my base view with
.sheet(isPresented: $showingLocationSheet) {
LocationSelectionView(selectedLocations: $locations)
}
From my LocationSelectionView, I then call the alert as follows:
let alert = UIAlertController(title: "Add Location", message: "Enter Location Name", preferredStyle: .alert)
alert.addTextField { (name) in
name.placeholder = "Bathroom"
}
let add = UIAlertAction(title: "Add", style: .default) { _ in
print("add Location")
}
let cancel = UIAlertAction(title: "Cancel", style: .destructive)
alert.addAction(add)
alert.addAction(cancel)
DispatchQueue.main.async {
UIApplication.shared.windows.first?.rootViewController?.present(alert, animated: true)
}
but then I receive this error:
Attempt to present <UIAlertController: 0x7fc8a78c7c00> on <_TtGC7SwiftUI19UIHostingControllerGVS_15ModifiedContentVS_7AnyViewVS_12RootModifier__: 0x7fc8a800b8d0> (from <_TtGC7SwiftUI19UIHostingControllerGVS_15ModifiedContentVS_7AnyViewVS_12RootModifier__: 0x7fc8a800b8d0>) which is already presenting <_TtGC7SwiftUI29PresentationHostingControllerVS_7AnyView_: 0x7fc8a6e51980>.
I'm not quite sure how to get it to show here. I've also tried using an extension on the UIAlertController like this:
public extension UIAlertController {
func show() {
let win = UIWindow(frame: UIScreen.main.bounds)
let vc = UIViewController()
vc.view.backgroundColor = .clear
win.rootViewController = vc
win.windowLevel = UIWindow.Level.alert + 1 // Swift 3-4: UIWindowLevelAlert + 1
win.makeKeyAndVisible()
vc.present(self, animated: true, completion: nil)
}
as well as getting the top most controller like so:
func topmostController() -> UIViewController? {
if var topController = UIApplication.shared.keyWindow?.rootViewController {
while let presentedViewController = topController.presentedViewController {
topController = presentedViewController
}
return topController
}
return nil
}
but no luck. Any ideas?

Present alert after dismissing View Controller

I'm using the newest Xcode and Swift version.
I'm presenting a specific View Controller like this:
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let contactViewController = storyboard.instantiateViewController(identifier: "contactViewController")
show(contactViewController, sender: self)
I'm dismissing this View Controller like this:
self.presentingViewController?.dismiss(animated: true, completion: nil)
I want to present an UIAlertController right after dismissing the View Controller.
This:
self.presentingViewController?.dismiss(animated: true, completion: nil)
let alertMessage = UIAlertController(title: "Your message was sent", message: "", preferredStyle: .alert)
let alertButton = UIAlertAction(title: "Okay", style: UIAlertAction.Style.default)
alertMessage.addAction(alertButton)
self.present(alertMessage, animated: true, completion: nil)
… of course doesn't work because I cannot present an UIAlertController on a dismissed View Controller.
What's the best way to present this UIAlertController after the View Controller is dismissed?
You can do it in completion handler by getting top controller like this
self.presentingViewController?.dismiss(animated: true, completion: {
let alertMessage = UIAlertController(title: "Your message was sent", message: "", preferredStyle: .alert)
let alertButton = UIAlertAction(title: "Okay", style: UIAlertAction.Style.default)
alertMessage.addAction(alertButton)
UIApplication.getTopMostViewController()?.present(alertMessage, animated: true, completion: nil)
})
Using this extension
extension UIApplication {
class func getTopMostViewController() -> UIViewController? {
let keyWindow = UIApplication.shared.windows.filter {$0.isKeyWindow}.first
if var topController = keyWindow?.rootViewController {
while let presentedViewController = topController.presentedViewController {
topController = presentedViewController
}
return topController
} else {
return nil
}
}
}
Use Jawad Ali's extension, we could anchor the current presented ViewController.
And if you want to dismiss that alert later, you could do it in another completion handler as below code showed. In my case, I save a song to one playlist and dismiss this playlist and show a short time alert to let user know that saving ok.
DispatchQueue.main.async {
self?.removeSpinner()
self?.dismiss(animated: true, completion: {
let alert = UIAlertController(title: "Save to playlist", message: nil, preferredStyle: .alert)
UIApplication.getTopMostViewController()?.present(alert, animated: true, completion: {
Timer.scheduledTimer(withTimeInterval: 1.0, repeats: false) { _ in
alert.dismiss(animated: true)
}
})
})
}

Dispatch asyncAfter for multiple UIAlertControllers

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
}
}

AlertController is not in the window hierarchy

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)

Resources