The situation you found yourself in seems enough of a reason to reconsider what you did to get to this. I am using I guess a similar situation as I load all core data objects in separate thread so completions are used like
MyEntity.fetchAll { items,
self.entities = items
self.tableView.reloadData()
}
In this case it is pretty easy to do something like:
var entities: [Any]? {
didSet {
self.removeActivityIndicator()
}
}
You can put all the logic into some base class for your view controller so you can easily reuse it.
Sometimes though it is better to do these things statically. You can add a new window above everything that has an activity indicator. Basically like doing custom alert views. The a retain count system should work the best:
class ActivityManager {
private static var retainCount: Int = 0 {
didSet {
if(oldValue > 0 && newValue == 0) removeActivityWindow()
else if(oldValue == 0 && newValue > 0) showActivityWindow()
}
}
static func beginActivity() { retainCount += 1 }
static func endActivity() { retainCount -= 1 }
}
In this case you can use the tool anywhere in your code. The rule is that every "begin" must have an "end". So for instance:
func resolveData() {
ActivityManager.beginActivity()
doMagic {
ActivityManager.endActivity()
}
}
There are really many ways to do this and there is probably no "best solution" as it just depends on your case.
An example of using a new window to show a dialog:
As requested in comments I am adding an example on how to show a dialog in a new window. I am using a new storyboard "Dialog" that contains a view controller AlertViewController
. This might as well be a controller with some activity indicator but more important part is how a window is generated, how controller is shown and how dismissed.
class AlertViewController: UIViewController {
@IBOutlet private var blurView: UIVisualEffectView?
@IBOutlet private var dialogPanel: UIView?
@IBOutlet private var titleLabel: UILabel? // Is in vertical stack view
@IBOutlet private var messageLabel: UILabel? // Is in vertical stack view
@IBOutlet private var okButton: UIButton? // Is in horizontal stack view
@IBOutlet private var cancelButton: UIButton? // Is in horizontal stack view
var titleText: String?
var messageText: String?
var confirmButtonText: String?
var cancelButtonText: String?
override func viewDidLoad() {
super.viewDidLoad()
setHiddenState(isHidden: true, animated: false) // Initialize as not visible
titleLabel?.text = titleText
titleLabel?.isHidden = !(titleText?.isEmpty == false)
messageLabel?.text = messageText
messageLabel?.isHidden = !(messageText?.isEmpty == false)
okButton?.setTitle(confirmButtonText, for: .normal)
okButton?.isHidden = !(confirmButtonText?.isEmpty == false)
cancelButton?.setTitle(cancelButtonText, for: .normal)
cancelButton?.isHidden = !(cancelButtonText?.isEmpty == false)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
setHiddenState(isHidden: false, animated: true)
}
private func setHiddenState(isHidden: Bool, animated: Bool, completion: (() -> Void)? = nil) {
UIView.animate(withDuration: animated ? 0.3 : 0.0, animations: {
self.blurView?.effect = isHidden ? UIVisualEffect() : UIBlurEffect(style: .light)
self.dialogPanel?.alpha = isHidden ? 0.0 : 1.0
}) { _ in
completion?()
}
}
@IBAction private func okPressed() {
AlertViewController.dismissAlert()
}
@IBAction private func cancelPressed() {
AlertViewController.dismissAlert()
}
}
// MARK: - Window
extension AlertViewController {
private static var currentAlert: (window: UIWindow, controller: AlertViewController)?
static func showMessage(_ message: String) {
guard currentAlert == nil else {
print("An alert view is already shown. Dismiss this one to show another.")
return
}
let controller = UIStoryboard(name: "Dialog", bundle: nil).instantiateViewController(withIdentifier: "AlertViewController") as! AlertViewController
controller.confirmButtonText = "OK"
controller.messageText = message
let window = UIWindow(frame: UIApplication.shared.windows[0].frame)
window.windowLevel = .alert
window.rootViewController = controller
window.makeKeyAndVisible()
self.currentAlert = (window, controller)
}
static func dismissAlert() {
if let currentAlert = self.currentAlert {
currentAlert.controller.setHiddenState(isHidden: true, animated: true) {
self.currentAlert?.window.isHidden = true
self.currentAlert = nil
}
}
}
}
I added the whole class just in case but the important part is showing a new window:
let window = UIWindow(frame: UIApplication.shared.windows[0].frame) // Create a window
window.windowLevel = .alert // Define which level it should be in
window.rootViewController = controller // Give it a root view controller
window.makeKeyAndVisible() // Show the window
And removing the window:
window.isHidden = true
Simply hiding your window is enough. Assuming you don't have any strong reference to it it will be removed from application stack. To confirm this make sure that UIApplication.shared.windows.count
has an appropriate value which in most cases should be 2
when alert is shown and 1
otherwise.
My test usage of the code above was simply:
AlertViewController.showMessage("A test message. This is testing of alert view in a separate window.")