Since UIAlertView is deprecated I want to replace it by UIAlertController in my old libraries.
But it is not always obvious to do. For example I have those two functions, doing a very similar task.
showAlertViewMessageBox uses UIAlertView and showMessageBox uses UIAlertController:
func showAlertViewMessageBox(_ msg:String, title:String, caller:UIViewController) {
let userPopUp = UIAlertView()
userPopUp.delegate = caller
userPopUp.title = title
userPopUp.message = msg
userPopUp.addButton(withTitle: "OK")
userPopUp.show()
}
func showMessageBox(_ msg:String, title:String, caller:UIViewController) {
let attribMsg = NSAttributedString(string: msg,
attributes: [NSAttributedString.Key.font:UIFont.systemFont(ofSize: 23.0)])
let userPopUp = UIAlertController(title:title,
message:nil, preferredStyle:UIAlertController.Style.alert)
userPopUp.setValue(attribMsg, forKey: "attributedMessage")
let alertAction = UIAlertAction(title:"OK", style:UIAlertAction.Style.default,
handler:{action in})
alertAction.setValue(UIColor.darkGray, forKey: "titleTextColor")
userPopUp.addAction(alertAction)
caller.present(userPopUp, animated: true, completion: nil)
}
I want to use showMessageBox as much as possible. But I have this unhappy situation:
In the following code:
//showMessageBox(usrMsg, title: popTitle, caller: self)
showAlertViewMessageBox(usrMsg, title: popTitle, caller: self)
quitViewController()
When using showAlertViewMessageBox, the message will pop up and stay there until I click the OK button.
(This is the behavior I want)
When using showMessageBox, the message will pop up as a blink and disappear without waiting for me to click the OK button.
(This is NOT the behavior I want)
How should I modify showMessageBox to get the behavior I want?
Assuming you are dismissing the viewController inside quitViewController() method, it is very natural that the message will pop up as a blink and disappear without waiting for me to click the OK button. As your quitViewController() method is executed without waiting for the OK button clicked.
One way, is adding a parameter to handle OK button clicking:
func showMessageBox(_ msg:String, title:String, caller:UIViewController, onOk: #escaping ()->Void) {
let attribMsg = NSAttributedString(string: msg,
attributes: [NSAttributedString.Key.font:UIFont.systemFont(ofSize: 23.0)])
let userPopUp = UIAlertController(title:title,
message:nil, preferredStyle:UIAlertController.Style.alert)
userPopUp.setValue(attribMsg, forKey: "attributedMessage")
let alertAction = UIAlertAction(title:"OK", style:UIAlertAction.Style.default,
handler:{action in onOk()}) //<- Call the Ok handler
alertAction.setValue(UIColor.darkGray, forKey: "titleTextColor")
userPopUp.addAction(alertAction)
caller.present(userPopUp, animated: true, completion: nil)
}
Use it as:
showMessageBox(usrMsg, title: popTitle, caller: self) {
self.quitViewController()
}
By the way, attributedMessage of UIAlertController and titleTextColor of UIAlertAction are private properties, so using this code may risk your app to be rejected by using private APIs.
Related
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?
Here is the output
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
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)
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.
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];
}
}
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)
I have two UIAlertControllers that can potentially be displayed – one for registering an account, and one for logging into it. They can be presented with an optional message.
func present(alertController: UIAlertController? = nil, message: String? = nil) {
self.currentAlertController = alertController ?? self.currentAlertController
self.currentAlertController.message = message
(UIApplication.shared.delegate as! AppDelegate).window?.rootViewController?.present(self.currentAlertController, animated: true, completion: nil)
}
And it look like the message is carried through the function.
But then it doesn't display.
I can't tell if I'm missing something basic, or there's just something about UIAlertController I'm not aware of.
Switching the present action and the setting of the message did the trick.
func present(alertController: UIAlertController? = nil, message: String? = nil) {
self.currentAlertController = alertController ?? self.currentAlertController
(UIApplication.shared.delegate as! AppDelegate).window?.rootViewController?.present(self.currentAlertController, animated: true, completion: nil)
self.currentAlertController.message = message
}
Another thing is, when initializing the UIAlertController to begin with, if you want the message to start blank but allow it to be updated later, the message should be "" as opposed to nil.
I am fairly new to Swift but I am creating a app that requires after a certain time to enter either Touch-ID or a PIN. I am checking a timer from AppDelegate.swift to see if it has expired and if it has expired I am making a call to my "BaseTableViewController" which holds my function authenticateUser. Again I am calling this from my AppDelegate.swift file by creating an instance of BaseTableViewController var baseTableVC = BaseTableViewController() and making a call if timer expired to self.baseTableVC.authenticateUser().
Anyways I am getting: Warning: Attempt to present <UIAlertController: 0x7fed5ae1dcf0> on <admin.BaseViewController: 0x7fed5ad279d0> whose view is not in the window hierarchy!
Thank you in advance for you help!
func showPasswordAlert(){
let alertController = UIAlertController(title: "Touch ID Password", message: "Please enter your password", preferredStyle: .Alert)
let defaultAction = UIAlertAction(title: "OK", style: .Cancel) {(action) -> Void in
if let textField = alertController.textFields?.first as UITextField?{
if textField.text == "hello" {
print("Authentication successfull!")
}
else{
self.showPasswordAlert()
}
}
}
alertController.addAction(defaultAction)
alertController.addTextFieldWithConfigurationHandler{(textField) -> Void in
textField.placeholder = "Password"
textField.secureTextEntry = true
}
presentViewController(alertController, animated: true, completion: nil)
}
func authenticateUser(){
let context = LAContext()
var error: NSError?
let reasonString = "Authentication is required for Admin!"
context.localizedFallbackTitle = "Enter your PIN Code"
if context.canEvaluatePolicy(LAPolicy.DeviceOwnerAuthenticationWithBiometrics, error: &error){
context.evaluatePolicy(LAPolicy.DeviceOwnerAuthenticationWithBiometrics, localizedReason: reasonString, reply: {(success, policyError) ->Void in
if success{
print("Authentication successful!")
}
else{
switch policyError!.code{
case LAError.SystemCancel.rawValue:
print("Authentication was cancelled by the system!")
case LAError.UserCancel.rawValue:
NSOperationQueue.mainQueue().addOperationWithBlock({ () -> Void in
self.showPasswordAlert()
})
print("Authentication was cancelled by the user!")
case LAError.UserFallback.rawValue:
print("User selected to enter password.")
NSOperationQueue.mainQueue().addOperationWithBlock({() -> Void in
self.showPasswordAlert()
})
default:
print("Authentication failed!")
NSOperationQueue.mainQueue().addOperationWithBlock({ () -> Void in
self.showPasswordAlert()
})
}
}
})
}
else{
print(error?.localizedDescription)
NSOperationQueue.mainQueue().addOperationWithBlock({ () -> Void in
self.showPasswordAlert()
})
}
var baseTableVC = BaseTableViewController()
func applicationWillEnterForeground(application: UIApplication) {
// Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background.
let logInStatus = NSUserDefaults.standardUserDefaults()
let currentTime = NSDate().timeIntervalSince1970
let roundCurrentTime = (round(currentTime))
// Pin expire limit
let pinExpLimit: Double = 30
// Set the exact time of expire for pin
let pinExpDate = (currentTime + pinExpLimit)
let newPinExpDate = (round(pinExpDate))
if (logInStatus.doubleForKey("expPinTime") <= roundCurrentTime) {
self.baseTableVC.authenticateUser()
print("AppDelegate Pin Exp Time")
print(logInStatus.doubleForKey("expPinTime"))
//print(newPinExpDate)
print("AppDelegate Current Time")
print(roundCurrentTime)
logInStatus.setDouble(newPinExpDate, forKey: "expPinTime")
NSUserDefaults.standardUserDefaults().synchronize()
}
}
I suspect you simply create an instance of BaseTableViewController but you don't add its view to the view hierarchy before presenting the UIAlertController's instance.
If self.baseTableVC is the root view controller of your app, then a call like this
baseTableVC.presentViewController(instanceOfUIAlertController, animated: true, completion: yourCompletionBlock)
should work from within the AppDelegate.
If self.baseTableVC is not the root view controller, then or you make sure to invoke the previous command on the root VC of your app
window.rootViewController.presentViewController(instanceOfUIAlertController, animated: true, completion: yourCompletionBlock)
or make sure you embed the view of self.baseTableVC in the view hierarchy and then call
baseTableVC.presentViewController(instanceOfUIAlertController, animated: true, completion: yourCompletionBlock)
As a side note, if your alert must be displayed from anywhere in the app, then your approach is ok. If instead your alert must be displayed only from a specific screen, I would remove the timer logic from the app delegate and move it inside the presenting view controller. This would keep your app delegate clean from unnecessary code and would confine the control logic in the right place: the presenting view controller
You can't create instance of view controller just by calling default constructor, use storyboard. Correct me if I'm wrong
I present a UIAlertController, appearing for about half a second and disappearing, before I need to touch the OK button.
Any idea about what could cause this?
For reference, the code is below, I have been using it many times, in the past. Apart that this time I pass a caller parameter.
func showMessageBox(msg:String, title:String, caller:UIViewController) {
let attribMsg = NSAttributedString(string: msg,
attributes: [NSFontAttributeName : UIFont.systemFontOfSize(23.0)])
let userPopUp = UIAlertController(title:title,
message:nil, preferredStyle:UIAlertControllerStyle.Alert)
userPopUp.setValue(attribMsg, forKey: "attributedMessage")
userPopUp.addAction(UIAlertAction(title:"OK", style:UIAlertActionStyle.Default,
handler:{action in}))
caller.presentViewController(userPopUp, animated: true, completion: nil)
}
I just tried the code here and it worked fine - the alert controller waits for me to click the button before going away. Is it possible that whatever is presenting it is also dismissing it by accident? Or perhaps that the view controller inside caller is itself being dismissed?
I have the following function working as I expect, in iOS 8:
func showConfirmBox(msg:String, title:String,
firstBtnStr:String,
secondBtnStr:String,
caller:UIViewController) {
let userPopUp = UIAlertController(title:title,
message:msg, preferredStyle:UIAlertControllerStyle.Alert)
userPopUp.addAction(UIAlertAction(title:firstBtnStr, style:UIAlertActionStyle.Default,
handler:{action in}))
userPopUp.addAction(UIAlertAction(title:secondBtnStr, style:UIAlertActionStyle.Default,
handler:{action in}))
caller.presentViewController(userPopUp, animated: true, completion: nil)
}
I would like to make something like the following, in order to pass as arguments the methods to be executed when one or the other of the buttons are going to be touched:
func showConfirmBox(msg:String, title:String,
firstBtnStr:String, firstSelector:Selector,
secondBtnStr:String, secondSelector:Selector,
caller:UIViewController) {
let userPopUp = UIAlertController(title:title,
message:msg, preferredStyle:UIAlertControllerStyle.Alert)
userPopUp.addAction(UIAlertAction(title:firstBtnStr, style:UIAlertActionStyle.Default,
handler:{action in caller.firstSelector()}))
userPopUp.addAction(UIAlertAction(title:secondBtnStr, style:UIAlertActionStyle.Default,
handler:{action in caller.secondSelector()}))
caller.presentViewController(userPopUp, animated: true, completion: nil)
}
Obviously I am not doing the right thing with firstSelector and secondSelector, because what I have tried up to now did not work. I suppose I am not using the right syntax for what I want, but I am sure it is possible to do what I would like to do. Any idea of the way to do it properly?
Oneword answer for your question is Closures
The Default Syntax for closures is () -> ()
Instead of Selector you could directly mention the method definition
func showConfirmBox(msg:String, title:String,
firstBtnStr:String, firstSelector:(sampleParameter: String) -> returntype,
secondBtnStr:String, secondSelector:() -> returntype,
caller:UIViewController) {
//Your Code
}
But using this will create readability problems so i suggest you to use typeAlias
typealias MethodHandler1 = (sampleParameter : String) -> Void
typealias MethodHandler2 = () -> Void
func showConfirmBox(msg:String, title:String,
firstBtnStr:String, firstSelector:MethodHandler1,
secondBtnStr:String, secondSelector:MethodHandler2) {
// After any asynchronous call
// Call any of your closures based on your logic like this
firstSelector("FirstButtonString")
secondSelector()
}
You can call your method like this
func anyMethod() {
//Some other logic
showConfirmBox(msg: "msg", title: "title", firstBtnStr: "btnString",
firstSelector: { (firstSelectorString) in
print(firstSelectorString) //this prints FirstButtonString
},
secondBtnStr: "btnstring") {
//Invocation comes here after secondSelector is called
}
}
Just in case anyone else stumbles upon this. I worked out an updated simple solution for Swift 5.1 while I was working through this for while building a global alert utility for a project.
Swift 5.1
Function with Closure:
func showSheetAlertWithOneAction(messageText: String, dismissButtonText: String, actionButtonText : String, presentingView : NSWindow, actionButtonClosure: #escaping () -> Void) {
let alert = NSAlert()
alert.messageText = messageText
alert.addButton(withTitle: actionButtonText)
alert.addButton(withTitle: dismissButtonText)
alert.beginSheetModal(for: presentingView) { (response) in
if response == .alertFirstButtonReturn {
actionButtonClosure()
}
}
}
Function Called:
showSheetAlertWithOneAction(messageText: "Here's a message", dismissButtonText: "Nope", actionButtonText: "Okay", presentingView: self.view.window!) {
someFunction()
}
Adding to got2jam's answer...
If you're working with UIAlertController
The generic function to show an alert with closure:
func showAlertAction(title: String, message: String, actionClosure: #escaping () -> Void){
let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert)
alertController.addAction(UIAlertAction(title: "Ok", style: UIAlertAction.Style.default, handler: {(action: UIAlertAction!) in actionClosure()}))
self.present(alertController, animated: true, completion: nil)
}
Now you can call it like that:
showAlertAction(title: "This is the title", message: "This is the message") {
self.close()
}
in this case, close is the particular UIAlertAction to execute
func close(){
dismiss(animated: true, completion: nil)
}
I wrote this routine based on various site examples. Here is how I call the routine...
#IBAction func buttonClick(_ sender: Any) {
SS_Alert.createAlert(parmTitle: "Choose", parmMessage: "Please select Yes or No", parmOptions: ["Yes","No","Cancel"], parmFunctions: [testYes, testNo, nil])
}
func testYes() {
print("yes")
}
func testNo() {
print("no")
}
You can pass in button options and the functions to performed when the buttons are selected. Took a little time to figure out how to pass functions as parameters, but appears to work fine now. I did encounter a strange problem trying to use loops to dynamically add buttons, and finally gave up and used a switch/case. I included the loop code I tried to use, if someone can figure out what I was doing wrong let me know. Thanks.
https://github.com/blakeguitar/iOS/blob/0e243d13cb2decd6e1dbe134a8a046c2caed3876/SS_Alert.swift