'How do you present a sequence of UIAlertControllers?

I have an app that can download many publications from a server at once. For each publication that already exists in the app, I want to prompt the user if he wants to overwrite the existing version.

Is there any clean way to present UIAlertControllers so that when the user has answered one, the app presents the next one?



Solution 1:[1]

Here is the output

enter image description here

Though two alert actions were called in a subsequent statements, second alert will be shown only after user interacts with alert on screen I mean only after tapping ok or cancel.

If this is what you want, as I mentioned in my comment you can make use of Asynchronous Operation and Operation Queue with maximum concurrent operation as 1

Here is the code.

First declare your own Asynchronous Operation

struct AlertObject {
    var title : String! = nil
    var message : String! = nil
    var successAction : ((Any?) -> ())! = nil
    var cancelAction : ((Any?) -> ())! = nil
    
    init(with title : String, message : String, successAction : @escaping ((Any?) -> ()), cancelAction : @escaping ((Any?) -> ())) {
        self.title = title
        self.message = message
        self.successAction = successAction
        self.cancelAction = cancelAction
    }
}


class MyAsyncOperation : Operation {
    var alertToShow : AlertObject! = nil
    var finishedStatus : Bool = false
    
    override init() {
        super.init()
    }
    
    override var isFinished: Bool {
        get {
            return self.finishedStatus
        }
        set {
            self.willChangeValue(forKey: "isFinished")
            self.finishedStatus = newValue
            self.didChangeValue(forKey: "isFinished")
        }
    }
    
    override var isAsynchronous: Bool{
        get{
            return true
        }
        set{
            self.willChangeValue(forKey: "isAsynchronous")
            self.isAsynchronous = true
            self.didChangeValue(forKey: "isAsynchronous")
        }
    }
    
    required convenience init(with alertObject : AlertObject) {
        self.init()
        self.alertToShow = alertObject
    }
    
    override func start() {
        if self.isCancelled {
            self.isFinished = true
            return
        }
        DispatchQueue.main.async {
            let alertController = UIAlertController(title: self.alertToShow.title, message: self.alertToShow.message, preferredStyle: .alert)
            alertController.addAction(UIAlertAction(title: "OK", style: .default, handler: { (action) in
                self.alertToShow.successAction(nil) //pass data if you have any
                self.operationCompleted()
            }))
            alertController.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: { (action) in
                self.alertToShow.cancelAction(nil) //pass data if you have any
                self.operationCompleted()
            }))
            UIApplication.shared.keyWindow?.rootViewController?.present(alertController, animated: true, completion: nil)
        }
    }
    
    func operationCompleted() {
        self.isFinished = true
    }
}

Though code looks very complicated in essence its very simple. All that you are doing is you are overriding the isFinished and isAsynchronous properties of Operation.

If you know how Operation queues works with Operation it should be very clear as to why am I overriding these properties. If in case u dont know! OperationQueue makes use of KVO on isFinished property of Operation to start the execution of next dependent operation in Operation queue.

When OperationQueue has maximum concurrent operation count as 1, isFinished flag of Operation decides when will next operation be executed :)

Because user might take action at some different time frame on alert, making operation Asynchronous (By default Operations are synchronous) and overriding isFinised property is important.

AlertObject is a convenience object to hold alert's meta data. You can modify it to match your need :)

Thats it. Now whenever whichever viewController wants to show alert it can simply use MyAsyncOperation make sure you have only one instance of Queue though :)

This is how I use it

    let operationQueue = OperationQueue() //make sure all VCs use the same operation Queue instance :)
    operationQueue.maxConcurrentOperationCount = 1
    
    let alertObject = AlertObject(with: "First Alert", message: "Success", successAction: { (anything) in
        debugPrint("Success action tapped")
    }) { (anything) in
        debugPrint("Cancel action tapped")
    }
    
    let secondAlertObject = AlertObject(with: "Second Alert", message: "Success", successAction: { (anything) in
        debugPrint("Success action tapped")
    }) { (anything) in
        debugPrint("Cancel action tapped")
    }

    let alertOperation = MyAsyncOperation(with: alertObject)
    let secondAlertOperation = MyAsyncOperation(with: secondAlertObject)
    operationQueue.addOperation(alertOperation)
    operationQueue.addOperation(secondAlertOperation)

As you can see I have two alert operations added in subsequent statement. Even after that alert will be shown only after user dismisses the currently displayed alert :)

Hope this helps

Solution 2:[2]

Althought answer with Queue is very good, you can achieve te same as easy as:

var messages: [String] = ["first", "second"]

func showAllerts() {
    guard let message = messages.first else { return }
    messages = messages.filter({$0 != message})
    let alert = UIAlertController(title: "title", message: message, preferredStyle: .alert)
    alert.addAction(UIAlertAction(title: "OK", style: .default, handler: { [weak self] (action) in
        // do something
        self?.showAllerts()
    }))
    alert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: { [weak self] (action) in
        self?.showAllerts()
    }))
    present(alert, animated: true, completion: nil)
}

(replace array of messages with whatever you want)

Solution 3:[3]

I would recommend creating a Queue data structure (https://github.com/raywenderlich/swift-algorithm-club/tree/master/Queue).

Where alert objects are queued in the order that the alerts are initialized. When a user selects an action on one of the alerts, dequeue the next possible alert and present it.

Solution 4:[4]

I had the same problem in my app and tried several solutions, but all of them were messy. Then I thought of a very simple and effective way: use a delay to retry presentation until it can be shown. This approach is much cleaner in that you don't need coordinated code in multiple places and you don't have to hack your action handlers.

Depending on your use case, you might care that this approach doesn't necassarily preserve the order of the alerts, in which case you can easily adapt it to store the alerts in an array to preserve order, showing and removing only the first on in the array each time.

This code overrides the standard presentation method in UIViewController, use it in your subclass which is presenting the alerts. This could also be adapted to an app level method if needed that descends from the rootViewController to find the top most presented VC and shows from there, etc.

- (void)presentViewController:(UIViewController *)viewControllerToPresent animated:(BOOL)flag completion:(void (^)(void))completion {
  // cannot present if already presenting.
  if (self.presentedViewController) {
    // cannot present now, try again in 100ms.
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
      // make sure we ourselve are still presented and able to present.
      if (self.presentingViewController && !self.isBeingDismissed) {
        // retry on self
        [self presentViewController:viewControllerToPresent animated:flag completion:completion];
      }
    });
  } else {
    // call super to really do it
    [super presentViewController:viewControllerToPresent animated:flag completion:completion];
  }
}

Solution 5:[5]

Few years ago I wrote a kind of presentation service, that process queue of items to present. When there isn't any presented view in current moment it take another one from array of items to present. Maybe it will help someone: https://github.com/Flawion/KOControls/blob/master/Sources/KOPresentationQueuesService.swift

Usage is very simple:

let itemIdInQueue = present(viewControllerToPresent, inQueueWithIndex: messageQueueIndex)

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Community
Solution 2 Michal Gorzalczany
Solution 3 cnbecom
Solution 4 stonemonk
Solution 5 Flawion