Displaying a UIAlertController in GameScene (SpriteKit/Swift) - ios

The simple way to display a UIAlertView, in swift is:
let alert = UIAlertView()
alert.title = "Alert!"
alert.message = "A wise message"
alert.addButtonWithTitle("Ok, thank you")
alert.show()
But this is now depreciated in iOS 9 and recommends using UIAlertController:
let myAlert: UIAlertController = UIAlertController(title: "Alert!", message: "Oh! Fancy", preferredStyle: .Alert)
myAlert.addAction(UIAlertAction(title: "OK", style: .Default, handler: nil))
self.presentViewController(myAlert, animated: true, completion: nil)
Which is great, but I'm using SpriteKit and inside a GameScene, which gives an error of Value of type 'GameScene' has no member 'presentViewController'...
Do I need to switch back to my ViewController to present this or is there a way to call it from a GameScene.
I found THIS answer, but it's Objective-C.

There are many ways to handle this situation, I do not recommend Jozemite Apps answer, because this will cause problems on apps with more than 1 view controller.(you want to present the alert on the current view controller, not the root)
My preferred way of doing it is through delegation.
What needs to be done is create a protocol to handle messaging:
import Foundation
protocol ViewControllerDelegate
{
func sendMessage(message:String);
}
In your view controller:
class ViewController : UIViewController, ViewControllerDelegate
{
...
func sendMessage(message:String)
{
//do alert view code here
}
//in the view controllers view did load event
func viewDidLoad()
{
var view = self.view as! GameSceneView
view.delegate = self
}
In your view code:
var delegate : ViewControllerDelegate
Finally in game scene where you want to present:
self.view.delegate?.sendMessage(message)
This way allows limited access to the VC, and can be modified with more options when needed.
Another way is to set up a notification system, and use NSNotificationCenter to pass a message from the scene to the current VC and have it send a message;
in ViewController
func viewDidLoad()
{
NSNotificationCenter.defaultCenter().addObserver(self,selector:"AlertMessage:",name:"AlertMessage",object:nil);
}
func AlertMessage(notification:NSNotification)
{
if(let userInfo = notification.userInfo)
{
let message = userInfo["message"]
....//do alert view call here
}
}
In Game scene code:
...at the spot you want to send a message
let userInfo = ["message":message];
NSNotificationCenter.defaultCenter.postNotificationNamed("AlertMessage",object:nil,userInfo:userInfo)
Another approach is to save the view controller pointer to game scene view:
//in Game Scene View code
var viewController : UIViewController;
//in the view controllers view did load event
func viewDidLoad()
{
var view = self.view as! GameSceneView
view.viewController = self
}
//finally in game scene where you want to present
let myAlert: UIAlertController = UIAlertController(title: "Alert!", message: "Oh! Fancy", preferredStyle: .Alert)
myAlert.addAction(UIAlertAction(title: "OK", style: .Default, handler: nil))
self.view.viewController.presentViewController(myAlert, animated: true, completion: nil)
Yet another way is to make your view controller global.
In view controller code:
private var _instance : UIViewController
class ViewController : UIViewController
{
class var instance
{
get
{
return _instance;
}
}
func viewDidLoad()
{
_instance = self;
}
}
Then just call
ViewController.instance!.
whenever you need access to your view controller.
Each of these methods have there strengths and weaknesses, so choose whatever way works best for you.

Try using this. I work in SpriteKit and I used this code for my in app purchase messages in my game, Chomp'd.
self.view?.window?.rootViewController?.presentViewController(myAlert, animated: true, completion: nil)

The answer from #Knight0fDragon is nice, but I think it's a little long.
I will just put here another solution using Podfile of Cocoapoad for new comers (others guys having the same problem).
first you need to install cocoapod and initiate it for your project (very easy to do; check some YouTube video or this link.
in your obtained Podfile, copy, and paste this: pod 'SIAlertView'. It is "A UIAlertView replacement with block syntax and fancy transition styles". (More details here. Please give credit to the libraries Authors you're using.
Finally, in your GameScene.swift file add the following after the import or after the closing bracket of the class GameScene
private extension GameScene {
func showPauseAlert() {
let alertView = SIAlertView(title: "Edess!!", andMessage: "Congratulations! test testing bla bla bla")
alertView.addButtonWithTitle("OK", type: .Default) { (alertView) -> Void in
print("ok was pushed")
}
alertView.show()
}
}
You can add many button with title if you want, and do whatever action you want. Here I just print "Ok was pushed".
The above cited links plus this one helped me to understand and work with this UIAlertView easily in my multiple-level game, to display alert on the "Pause" button of the game.

Related

Standalone Alert in Swift?

Is there a way to display an alert, when I may not have a handle on a ViewController?
I'm creating a centralized function for logging errors to the console, to my database, and ideally I'd like to pop up a notification for the user. I'd rather not have to pass in a ViewController every time I log an error.
You ca present it from your rootViewController or the current displayed viewController.
Since you start your application at least you have a UIWindow object and most
probably at least the rootViewController for that window.
UIApplication.shared.keyWindow!.rootViewController
You can present it from here anywhere in your code, just make a method an extension in UIApplication or UIViewController.
Or you can search for the top most view controller. Again you can make an extension in UIApplication that present your alert from there:
class func topViewController(rootViewController: UIViewController? = UIApplication.shared.keyWindow?.rootViewController) -> UIViewController? {
guard let rootViewController = rootViewController else {
return nil
}
guard let presented = rootViewController.presentedViewController else {
return rootViewController
}
switch presented {
case let navigationController as UINavigationController:
return topViewController(rootViewController: navigationController.viewControllers.last)
case let tabBarController as UITabBarController:
return topViewController(rootViewController: tabBarController.selectedViewController)
default:
return topViewController(rootViewController: presented)
}
}
Taking a an alert view as an example:
extension UIApplication {
static func presentAlert(with title: String, message: String) {
guard let rootVc = UIApplication.shared.keyWindow?.rootViewController else {
return
}
let alert = UIAlertController(title: title, message: message, preferredStyle: UIAlertControllerStyle.alert)
alert.addAction(UIAlertAction(title: "Okay", style: UIAlertActionStyle.default, handler: nil))
rootVc.present(alert, animated: true, completion: nil)
}
}
}
You can always add a view on the window. Search about adding view to the window of the application.
Create a UIView and add it as the frontmost subview of the window (which is always readily available).
This could be a UIView subclass that loads from a nib, that has a label and a dismiss button, and so on. Thus it can simple and self-contained.
I actually use something like this for debugging purposes in my own apps. When I print to the console I also put up a temporary UILabel in front of everything so that I see what I'm logging.

Warning: Attempt to present ZMNavigationController on **.ViewController whose view is not in the window hierarchy

Guys i am facing an odd problem with NavigationController. Existing answers did not help at all !!!!
Here is basic scenario of the app:
There are two views - Main and Second view
In main view there is a button when i happen to tap goes into second view using segue.
In second view after i enter a certain field in text view and click on a button called "join" it triggers "joinMeeting()" function
and meeting should be joined.
However, when i do that debugger shows me:
"Warning: Attempt to present on
<***.ViewController: *****> whose view is not in the window
hierarchy!"
So i have read most of the tread and given that it happens because of viewDidAppear method but i have nth to be done before viewDidAppear. Everything happens after button is clicked.
joinMeeting() is successfully called and print method returns 0 which means no issue(https://developer.zoom.us/docs/ios/error-codes/) and successful SDK connection however after this "Warning" error is shown in debugger and nothing happens in the app.
If it helps following is the code that triggers joinBtn:
/**
Triggers when Join Button is clicked from second view.
*/
#IBAction func joinMeeting(_ sender: Any) {
if( activityID.text == "" ) {
let alert = UIAlertController(title: "Field is Blank", message: "Activity ID cannot be blank.", preferredStyle: UIAlertControllerStyle.alert)
alert.addAction(UIAlertAction(title: "Ok", style: UIAlertActionStyle.default, handler: nil))
self.present(alert, animated: true, completion: nil)
return;
}
let ms: MobileRTCMeetingService? = MobileRTC.shared().getMeetingService()
if ms != nil {
ms?.delegate = self;
// //For Join a meeting
let paramDict: [AnyHashable: Any] = [
kMeetingParam_Username: kSDKUserName,
kMeetingParam_MeetingNumber: activityID.text!,
]
let ret: MobileRTCMeetError? = ms?.joinMeeting(with: paramDict)
print("onJoinaMeeting ret:\(String(describing: ret))")
}
}
Please help if anyone knows or have an idea on what i am missing here.
Here is what solved the issue:
Storyboard Configuration:
ViewController --Segue: Show--> JoinViewController
#IBAction func onClickJoin(_ sender: AnyObject) {
//Main storyBoard
let initialVC = UIStoryboard(name: "Main", bundle:nil).instantiateInitialViewController() as! UIViewController
let appDelegate = (UIApplication.shared.delegate as! AppDelegate)
appDelegate.window?.rootViewController = initialVC
//Rest of the code
}
Just Add Following code on that controller in which you want to perform calling:
override func viewWillAppear(_ animated: Bool) {
let appDelegate = UIApplication.shared.delegate as? AppDelegate
appDelegate?.window?.rootViewController = self
}
Unfortunately, none of the above solutions worked for me.
So Here is my solution.
Add this line
MobileRTC.shared().setMobileRTCRootController( self.navigationController)
=> When user taps of Join Call Button.
Make sure these conditions should meet as well.
ViewController which is used to open the ZOOM meeting should be a part of root navigation controller
DO NOT present modally the current Zoom Meeting Joining View Controller. Always push it to the root navigation controller.

Navigate to a UIViewController from a UIAlertView in Swift 2.3

I am new to Swift and iOS development. I have been trying to create an app-wide check for internet connectivity, that allows the user to retry (reload the current view). The checkInternetConnection() function below is called during viewWillAppear() in my BaseUIViewController class.
The Reachability.isConnectedToNetwork() connectivity-check works fine. But so far, I have not been able to figure out a way to reload the current view after the user presses the 'Retry' button in the alert. in fact, I have not been able to figure out a way to reload the current view in any scenario.
One thing I know I am doing wrong in the following attempt (since there is a compile error telling me so), is that I am passing a UIViewController object as a parameter instead of a string on this line: self.performSegueWithIdentifier(activeViewController, sender:self), but I suspect that this is not my only mistake.
func checkInternetConnection() {
if (Reachability.isConnectedToNetwork()) {
print("Internet connection OK")
} else {
print("Internet connection FAILED")
let alert = UIAlertController(title: NSLocalizedString("Error!", comment: "Connect: error"), message: NSLocalizedString("Could not connect", comment: "Connect: error message"), preferredStyle: UIAlertControllerStyle.Alert)
alert.addAction(UIAlertAction(title: NSLocalizedString("Retry", comment: "Connect: retry"), style: .Default, handler: { action in
print("Connection: Retrying")
let navigationController = UIApplication.sharedApplication().windows[0].rootViewController as! UINavigationController
let activeViewController: UIViewController = navigationController.visibleViewController!
self.performSegueWithIdentifier(activeViewController, sender:self)
}))
self.presentViewController(alert, animated: true, completion: nil)
}
}
BaseUIViewController can perform app-wide check for connectivity, so all your ViewControllers will be inherited from BaseUIViewController.
And each ViewController will have different behaviour after connectivity check.
One thing you can do is, in your BaseUIViewController you can define a block that performs action after connectivity check failed.
Here's the example:
class BaseViewUIController: UIViewController {
var connectivityBlock: (() -> Void)?
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
checkInternetConnection()
}
func checkInternetConnection() {
if Reachability.isConnectedToNetwork() {
print("Internet connection OK")
} else {
if let b = connectivityBlock {
b()
}
}
}
}
And in your inherited ViewControllers:
class MyViewController: BaseUIViewController {
override func viewDidLoad() {
super.viewDidLoad()
connectivityBlock = {
print("Do View Refresh Here.")
}
}
}
Then After the check in BaseUIViewController, the code in connectivityBlock will be executed so that each VC can deal with it differently.
You can also define a function in BaseUIViewController and each subclass will override that function, after connectivity check failed, call the function.
Try navigate using application's windows
let destinationVC = UIApplication.shared.delegate?.window
destinationVC .present(activeViewController, animated: true, completion: nil)

Is it possible to block execution of my code until UIAlertController is dismissed?

is there anyway to wait the user to press the button that dismiss the alertController in swift 3, by using DispatchQueue or something else?
Just move all the code you want to execute after the alert is dismissed into a separate method. When you're adding UIAlertActions, make all the actions' handler parameter call that method. This way, whichever button the user presses, your code will always be executed!
You mean something like this?
alertController.displayAndWaitUntilDismissed()
// This line is only reached after the alert controller is dismissed
print("Alert controller dismissed.")
Theoretically, yes, you could use a dispatch semaphore to block until the alert is dismissed. But it’s a bad idea – I can’t even think of a scenario where it would be acceptable. Simply accept that you have to deal with it asynchronously, by executing the desired code in the alert controller action.
You MUST NOT block the main thread and waiting for dismissing your alert so the only way is doing this asynchronously.
For instance you can use next common approach with UIViewControllerTransitioningDelegate:
fileprivate var AssociatedObjectKey: UInt8 = 0
extension UIAlertController : UIViewControllerTransitioningDelegate {
var onDissmiss: (() -> Void)? {
set {
transitioningDelegate = newValue != nil ? self : nil
objc_setAssociatedObject(self, &AssociatedObjectKey, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
get {
return objc_getAssociatedObject(self, &AssociatedObjectKey) as? (() -> Void)
}
}
public func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
onDissmiss?()
return nil
}
}
How to use:
let alert = UIAlertController(title: "Title", message: "Message", preferredStyle: .alert)
let ok = UIAlertAction(title: "OK", style: .default)
alert.addAction(ok)
alert.onDissmiss = {
print("dissmiss")
}
present(alert, animated: true)

IOS SWIFT 2 - display the same alert with different titles in different scene

i need to display the same alert with different titles in each time
so i want to create a function that takes a string and changes the title of the alert and then display this alert
my question is : where should i create this function ? and how call it from another viewControllers ?
You can extend UIAlertController by implementing your own custom class, then you present this controller where you want it to be presented just changing title. Below is the sample code, it works. Hope you will understand the concept.
import UIKit
extension UIAlertController {
class func alertControllerWithTitle(title: String, message: String) -> UIAlertController {
let controller = UIAlertController(title: title, message: message, preferredStyle: .Alert)
controller.addAction(UIAlertAction(title: "OK", style: .Default, handler: nil))
return controller
}
}
class ViewController: UIViewController {
let someProperty = 0
var alertTitle = ""
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewDidAppear(animated: Bool) {
someMethod()
}
func someMethod() {
switch someProperty {
case 0:
alertTitle = "someProperty is 0"
case 1:
alertTitle = "someProperty is 1"
default:
alertTitle = "someProperty is default"
}
//present alert
let controller = UIAlertController.alertControllerWithTitle(alertTitle, message: "some message")
presentViewController(controller, animated: true, completion: nil)
}
}
The function can be created as a stand-alone function (i.e. not part of any struct or class). It can exist anywhere in the same module as the classes that call it such as the view controllers. Personally, I would place it in a file on its own (called <yourFunctionName>.swift) close to the view controllers, or in a utility file containing similar functions.
Because the function is defined in the same module as the view controllers, it can be called directly by them.
Create a common class , place this function in that class, it should return a configured UIAlertController , create a protocol for that function in that common class with the alert function having Title as parameter.Conform this protocol to the class you want to display the Alert in, call it by its delegate and present it in a self.presentViewController(.....

Resources