0

In my iOS Swift app, I start a long running async task (held by a singleton which exists for the lifetime of the app) to upload some data to a server. Typically this task can take up to 10 seconds, during which it is very likely that they will have navigated away from the view controller that started the async task. When the task has finished I'd like to display a message to the user, regardless of where in the app the user is. Is there an easy way to display a dialog box/message/whatever without having to register a new delegate everytime a new view controller is created?

In my android app, which does the same thing, I can display a toast (i.e dialog box) at any time I like, regardless of which fragment (i.e View Controller) is being displayed at that time - without having to implement special behaviour within the fragment, as the toast is displayed on the fragment's parent.

  • I assume you are using a `UIAlertController`. In which case, the problem boils down to getting the currently presented view controller so you can present that alert controller. One approach is here: https://stackoverflow.com/questions/41073915/how-to-get-the-current-displaying-uiviewcontroller-not-in-appdelegate And, you will need to run this on the main thread, not your async thread. – Chris Prince Jul 02 '18 at 14:13

2 Answers2

0

I think the magic you are looking for is

func getTopViewController(_ viewController: UIViewController? = UIApplication.shared.delegate?.window??.rootViewController) -> UIViewController? {
    if let tabBarViewController = viewController as? UITabBarController {
        return getTopViewController(tabBarViewController.selectedViewController)
    } else if let navigationController = viewController as? UINavigationController {
        return getTopViewController(navigationController.visibleViewController)
    } else if let presentedViewController = viewController?.presentedViewController {
        return getTopViewController(presentedViewController)
    } else {
        return viewController
    }
}

I hate using it because it does not adhere to Apple best practices, but in a pinch it will work for simpler storyboards. You can use this view controllers view to add a message to

if let viewController = getTopViewController() {
    let toastLabel = UILabel(frame: frame)
    // set up labels background color and other properties...
    toastLabel.text = "your message" 
    // add the subview to the rootViewController
    viewController.view.addSubview(toastLabel)
    // easy animation
    UIView.animate(withDuration: 0.4, delay: duration, options: .curveEaseOut, animations: {
        toastLabel.alpha = 0.0
    }, completion: {(isCompleted) in
        toastLabel.removeFromSuperview()
    })
} else {
    print("Unable to get top view controller.")
}

Make sure to use this only in the main dispatch queue

DispatchQueue.main.async {
    // ...
}

Below are a couple of techniques to make this more module and object orientated.

One technique is to use a static Toast class.

import UIKit
public class Toast {
    private init() { }
    public static var frame = CGRect(
        x: UIScreen.main.bounds.size.width/2 - (UIScreen.main.bounds.size.width/2 - 16),
        y: UIScreen.main.bounds.size.height - 100,
        width: UIScreen.main.bounds.size.width - 32,
        height: 35
    )
    public static var backgroundColor = UIColor.black.withAlphaComponent(0.6)
    public static var textColor = UIColor.white
    public static var textAlignment = NSTextAlignment.center
    public static var font = UIFont.systemFont(ofSize: 12.0)
    public static var alpha:CGFloat = 1.0
    public static var cornerRadius:CGFloat = 10.0;
    public static func makeToast(
        message: String,
        duration: TimeInterval = 4.0,
        completion: ((_ complete:Bool)->Void)? = nil
    ) {
        if let viewController = getTopViewController() {
            let toastLabel = UILabel(frame: Toast.frame)
            toastLabel.backgroundColor = Toast.backgroundColor
            toastLabel.textColor = Toast.textColor
            toastLabel.textAlignment = Toast.textAlignment;
            toastLabel.font = Toast.font
            toastLabel.alpha = Toast.alpha
            toastLabel.layer.cornerRadius = Toast.cornerRadius
            toastLabel.clipsToBounds  =  true

            toastLabel.text = message

            viewController.view.addSubview(toastLabel)
            UIView.animate(withDuration: 0.4, delay: duration, options: .curveEaseOut, animations: {
                toastLabel.alpha = 0.0
            }, completion: {(isCompleted) in
                toastLabel.removeFromSuperview()
                completion?(isCompleted)
            })
        } else {
            print("Unable to get top view controller.")
        }
    }
    private static func getTopViewController(_ viewController: UIViewController? = UIApplication.shared.delegate?.window??.rootViewController) -> UIViewController? {
        if let tabBarViewController = viewController as? UITabBarController {
            return getTopViewController(tabBarViewController.selectedViewController)
        } else if let navigationController = viewController as? UINavigationController {
            return getTopViewController(navigationController.visibleViewController)
        } else if let presentedViewController = viewController?.presentedViewController {
            return getTopViewController(presentedViewController)
        } else {
            return viewController
        }
    }
}

Usage:

Toast.makeToast(message: "This is a test", duration: 4.0) { (isCompleted) in
    print("completed: \(isCompleted)")
}
// or
Toast.makeToast(message: "This is a test", duration: 4.0)
// or just
Toast.makeToast(message: "This is a test")

You can set the frame, backgroundColor, textColor, textAlignment, font, alpha, and cornerRadius using the static variables like so:

Toast.frame = CGRect(
    x: UIScreen.main.bounds.size.width/2 - (UIScreen.main.bounds.size.width/2 - 16),
    y: UIScreen.main.bounds.size.height - 100,
    width: UIScreen.main.bounds.size.width - 32,
    height: 35
)
Toast.backgroundColor = UIColor.blue
Toast.textColor = UIColor.green
Toast.textAlignment = .left
Toast.font = UIFont.systemFont(ofSize: 14.0)
Toast.alpha = 0.8
Toast.cornerRadius = 8.0

Another technique is to extend the UIApplication.

import UIKit
extension UIApplication {
    public func makeToast(
        message: String,
        duration: TimeInterval = 4.0,
        frame:CGRect = CGRect(
            x: UIScreen.main.bounds.size.width/2 - (UIScreen.main.bounds.size.width/2 - 16),
            y: UIScreen.main.bounds.size.height - 100,
            width: UIScreen.main.bounds.size.width - 32,
            height: 35
        ),
        backgroundColor:UIColor = UIColor.black.withAlphaComponent(0.6),
        textColor:UIColor = UIColor.white,
        textAlignment:NSTextAlignment = .center,
        font:UIFont = UIFont.systemFont(ofSize: 12.0),
        alpha:CGFloat = 1.0,
        cornerRadius:CGFloat = 10,
        completion: ((_ complete:Bool)->Void)? = nil
    ) {
        if let viewController = self.getTopViewController(self.delegate?.window??.rootViewController) {
            let toastLabel = UILabel(frame: frame)
            toastLabel.backgroundColor = backgroundColor
            toastLabel.textColor = textColor
            toastLabel.textAlignment = textAlignment;
            toastLabel.font = font
            toastLabel.alpha = alpha
            toastLabel.layer.cornerRadius = cornerRadius
            toastLabel.clipsToBounds = true

            toastLabel.text = message

            viewController.view.addSubview(toastLabel)
            UIView.animate(withDuration: 0.4, delay: duration, options: .curveEaseOut, animations: {
                toastLabel.alpha = 0.0
            }, completion: {(isCompleted) in
                toastLabel.removeFromSuperview()
                completion?(isCompleted)
            })
        } else {
            print("Unable to get top view controller.")
        }
    }

    private func getTopViewController(_ viewController: UIViewController?) -> UIViewController? {
        if let tabBarViewController = viewController as? UITabBarController {
            return getTopViewController(tabBarViewController.selectedViewController)
        } else if let navigationController = viewController as? UINavigationController {
            return getTopViewController(navigationController.visibleViewController)
        } else if let presentedViewController = viewController?.presentedViewController {
            return getTopViewController(presentedViewController)
        } else {
            return viewController
        }
    }
}

Usage:

UIApplication.shared.makeToast(message: "This is another test", duration: 4.0) { (isCompleted) in
    print("completed: \(isCompleted)")
}

You can set the frame, backgroundColor, textColor, textAlignment, font, alpha, and cornerRadius by passing them in to the function with their tags:

UIApplication.shared.makeToast(
    message: "This is another test",
    duration: 4.0,
    frame: CGRect(
        x: UIScreen.main.bounds.size.width/2 - (UIScreen.main.bounds.size.width/2 - 16),
        y: UIScreen.main.bounds.size.height - 100,
        width: UIScreen.main.bounds.size.width - 32,
        height: 35
    ),
    backgroundColor: UIColor.blue.withAlphaComponent(0.6),
    textColor: UIColor.red,
    textAlignment: NSTextAlignment.left,
    font: UIFont.systemFont(ofSize: 16.0),
    alpha: 1.0,
    cornerRadius: 10
) { (isCompleted) in
    print("completed: \(isCompleted)")
}

Download the example

Quick gist

Popmedic
  • 1,791
  • 1
  • 16
  • 21
  • Thats a hell of a useful answer! Thank you for all the examples. Haven't had a chance to try it yet, but will do over the next couple of days. – Chris Irlam Jul 03 '18 at 07:55
0

You can use NSNotification and NSNotificationCenter to send a notification when the callback is complete and register for the notification in any ViewController you may have.

Bogdan
  • 843
  • 1
  • 9
  • 26