Present Alert on ViewController only once - ios

I am having various ViewControllers in my app. On one of them I want a alert to be displayed on load of the VC once to the user.
I have followed the instructions to set a glob var under the import section:
var disalert:Bool = true
and in the function I got:
if disalert {
let actionSheetController: UIAlertController = UIAlertController(title: "How-to use Holiday List", message: "message here", preferredStyle: .Alert)
//Create and add the Cancel action
//Create and an option action
let nextAction: UIAlertAction = UIAlertAction(title: "OK", style: .Default) { action -> Void in
}
actionSheetController.addAction(nextAction)
//Add a text field
//Present the AlertController
self.presentViewController(actionSheetController, animated: true, completion: nil)
disalert = false
}
The alert is not presented whilst the app is open. When I restart the phone or quit the app completely its again there.
Thank you!

If I am reading your question properly, my suggestion would be to user NSUserDefaults to save a key when the user first opens the view. Then just use an IF statement to decide whether an alertView should be displayed.

Before showing the alert, wherever you want to show it, check the value against the "disalert" key in your userDefaults with this statement:
var disalert: Bool = NSUserDefaults.standardUserDefaults.boolForKey("disalert");
if disalert {
// The alert has already been shown so no need to show it again
}
else
{
// The alert hasn't been shown yet. Show it now and save in the userDefaults
// After showing the alert write this line of code
NSUserDefaults.standardUserDefaults.setBool(true, forKey: "disalert")
}

Adeel's code worked for me, with a slight improvement:
var disalert: Bool =
NSUserDefaults.standardUserDefaults().boolForKey("disalert");
if disalert {
// The alert has already been shown so no need to show it again
}
else
{
// The alert hasn't been shown yet. Show it now and save in the userDefaults
// After showing the alert write this line of code
NSUserDefaults.standardUserDefaults.setBool(true, forKey: "disalert")
}
NSUserDefaults cried for the following: NSUserDefaults.standardUserDefaults()

Related

iOS Swift UI Test tap alert button

A really small UI test fails when trying to tap an alert button, what step am I missing?
I'm trying to tap the "Continue" button in the alert displayed below with the following code (that I get from recording my steps).
let app = XCUIApplication()
let salesforceloginbuttonButton = app.buttons["salesforceLoginButton"]
salesforceloginbuttonButton.tap()
let posWantsToUseSalesforceComToSignInAlert = app.alerts["“POS” Wants to Use “salesforce.com” to Sign In"]
let continueButton = posWantsToUseSalesforceComToSignInAlert.buttons["Continue"]
continueButton.tap()
When I run the test it fails at the last line (i.e: continueButton.tap()) with the error No matches found for Find: Descendants matching type Alert from input.
Notes:
I already tried waiting a few seconds before tapping the continue button with the same result.
When the test is ran, the app launches and the alert gets displayed after tapping the salesforceloginbuttonButton
I think your alert is not recognized, maybe because of the double quotation marks
You can try and explicitly set the identifier of your alert like this:
let alert = UIAlertController(title: "Alert", message: "msg", preferredStyle: .alert)
alert.view.accessibilityIdentifier = "myAlert"
Then in your tests:
let alert = app.alerts["myAlert"]
let button = alert.buttons["Continue"]
button.tap()
Had a similar issue but I could solve it by using addUIInterruptionMonitor. In your XCTestCase add an override setUp like this. One caveat is that it's a little flakey... Will update the answer if I find a better solution.
class ExamplelUITests: XCTestCase {
var app: XCUIApplication!
override func setUp() {
super.setUp()
app = XCUIApplication()
continueAfterFailure = false
addUIInterruptionMonitor(withDescription: "Login Dialog") {
(alert) -> Bool in
let okButton = alert.buttons["Continue"]
okButton.tap()
return true
}
app.launch()
}
func testExample() throws {
app.buttons["Login"].tap()
app.tap() // This one is needed in order for the InteruptionMonitor to work
}
}

Replacing UIAlertView by UIAlertController

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.

IOS share extension show alert if user is not logged in?

I had added share extension to upload file.
But i want to stop open share extension when user is not logged in application and show alert similar like Messenger application from facebook.
Facebook messenger app
How can i do this
Note : I know how to do check is User logged in or not using app groups. But I want to check before open the share extension and show alert. in my case its first open the share extension and then i am showing alert. I want to check before open the share extension
Rather than directly open your custom view for share extension you could use alert first to check if user logged in or not, then if user has logged in, you can proceed to present your custom view with animation.
you can do this by adding below method on ShareViewController: SLComposeServiceViewController.
override func viewDidLoad() {
// check if user logged in or not here and if not below code will be executed.
self.loginAlertController = UIAlertController(title: "Please launch application from the home screen before continuing.", message: nil, preferredStyle: .alert)
let onOk = UIAlertAction(title: "OK", style: .destructive) { alert in
self.extensionContext?.cancelRequest(withError: NSError(domain: "loging", code: 0, userInfo: nil))
}
loginAlertController!.addAction(onOk)
present(loginAlertController!, animated: true, completion: nil)
}
You should make use of App groups to communicate between the main app and extension app.(Sharing Data: NSUserDefaults and App Groups
)
You can add data in main app like this :-
let mySharedDefaults = UserDefaults(suiteName: "group.yourValue")
mySharedDefaults?.set(false, forKey: "isLoggedIn")
Then you can get data like this in your extension
let mySharedDefaults = UserDefaults(suiteName: "group.yourValue")
if let isLoggedIn = mySharedDefaults?.value(forKey: "isLoggedIn") as? Bool {
if !isLoggedIn {
showAlert()
}
}
Refer this for implementing app groups

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?
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)

Warning: Attempt to present <UIAlertController: x> on <x.x:x> whose view is not in the window hierarchy

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

Resources