NSAlert beginSheetModalForWindow Not Showing Alert - swift

I have a settings view controller that is presented as a sheet. It has a button that saves the settings if they are valid. If the settings are valid the view controller is dismissed. If they are not valid the user gets an alert saying the settings are not valid. My code is as follows:
var settingsValidated = false
#IBAction func dismissSettings(sender: AnyObject) {
if settingsValidated == true {
dismissViewController(self)
} else {
let alert = NSAlert()
alert.messageText = "Warning"
alert.addButtonWithTitle("OK")
alert.informativeText = "Your settings did not validate!"
let window = NSApplication.sharedApplication().mainWindow
let res = alert.beginSheetModalForWindow(window!, completionHandler: nil)
}
}
If settingsValidated is set to true everything works as expected but when I set settingsValidated to false nothing happens. The alert never shows. What am I missing? I do not receive any errors in Xcode.
Please note this question is about OS X NOT iOS.

It's not showing up because you aren't doing anything with the res object! — so remove it:
alert.beginSheetModalForWindow(window!, completionHandler: nil)
↳ NSAlert Class Reference

Related

Presenting a series of alert view controllers sequentially, then performing a Present Modally segue - simultaneous presentation errors sometimes occur

In a certain app I'm developing, there are occasions where the user may be shown multiple popups (UIAlertViewControllers) in a row. Sometimes, this coincides with a Present Modally segue directly afterwards (i.e. the user presses a button, which is supposed to display all alerts in order, then perform a segue to a different screen). The user has the option of dismissing the alerts manually with the 'OK' button, but if they do nothing, each alert will automatically disappear after a set time.
It isn't absolutely essential that the user sees these popups, as they are just to notify the user that they have gained a new achievement. Users can always check their achievements by going directly to that page.
After a lot of experimentation, I got to the setup I have now. This mostly works as intended, except for the specific case where the user attempts to dismiss an alert manually right before that alert was set to disappear. This causes an error to be logged to the console that it's attempting to present an alert when one is already being presented. Other than this message -- which of course the user won't see :) --, the only issues caused by this error are some alert popups getting skipped, and the possibility that the user must press the button a second time to trigger the Present Modally segue to the next screen.
Although the issue is minor, I'm still looking to eliminate it completely if possible. I'm open to completely redoing the timing mechanism if the current implementation is flawed and someone proposes an alternative. (In other words, I realize this may be an instance of the "XY problem" that's often discussed on Meta!)
Below is an MCVE which reproduces the timing issue.
The desired behavior is to see the 4 alerts pop up in order, followed by the segue from the first view controller to the second. There are two View Controller scenes in Main.storyboard, with segues connecting them in each direction. Each View Controller has a UIButton which is connected to an IBAction to perform the segue to the other VC.
Note that allowing each alert to time out causes no errors. Similarly, dismissing each alert manually as soon as it appears (or shortly afterwards) also causes no errors. The only situation I've encountered where an error may occur is when an alert appears and you attempt to dismiss it very close to when it should auto-dismiss (3 seconds after it appears).
FIRST VIEW CONTROLLER
import UIKit
class ViewController: UIViewController {
// A failsafe in case the event queue timing gets messed up. This kind of error could otherwise cause an infinite cycle of alert view controllers being shown whenever the button is pressed, preventing the main segue from ever being performed. Having this boolean ensures that alert VCs can only be shown the first time the button is pressed, and that regardless of event queue 'success' or 'failure', a subsequent button press will always trigger the main segue (which is what is wanted).
var showedAlerts = false
var alertMessages = ["1st Item", "2nd Item", "3rd Item", "4th Item"]
override func viewDidLoad() {
super.viewDidLoad()
}
#IBAction func goForward(_ sender: UIButton) {
if !showedAlerts {
for i in 0..<alertMessages.count {
// This function is defined in Constants.swift
K.alerts.showAlertPopup(alertMessages[i], counter: K.alerts.alertCounter, VC: self)
}
showedAlerts = true
}
// Refer to Constants.swift for documentation of these variables
if K.alerts.canPresentNextSegue {
self.performSegue(withIdentifier: K.segues.toSecond, sender: self)
} else {
K.alerts.suspendedSegueParameters = (identifier: K.segues.toSecond, VC: self)
}
}
}
SECOND VIEW CONTROLLER
import UIKit
class SecondViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
}
#IBAction func goBack(_ sender: UIButton) {
self.performSegue(withIdentifier: K.segues.toFirst, sender: self)
}
}
CONSTANTS FILE
import UIKit
struct K {
struct segues {
static let toSecond = "toSecondVC"
static let toFirst = "toFirstVC"
}
struct alerts {
// Avoids segue conflicts by showing alert VC popups sequentially, and delaying any 'present modally' segue until all popups have been shown
static var canPresentNextSegue = true {
didSet {
if canPresentNextSegue == true {
if !suspendedAlertParameters.isEmpty {
// Take the first element out of the array each time, not the last, otherwise the order of all popups after the first will be reversed (i.e. will show popups in order of 1st, 4th, 3rd, 2nd)
let p = suspendedAlertParameters.removeFirst()
showAlertPopup(p.alertItem, counter: alertCounter, VC: p.VC)
}
// Don't perform the main segue until all alert popups have been shown! This should be true when the suspendedAlertParameters array is empty.
else if let p = suspendedSegueParameters {
p.VC.performSegue(withIdentifier: p.identifier, sender: p.VC)
suspendedSegueParameters = nil
}
}
}
}
// The purpose of this counter is to ensure that each Alert View Controller has an associated unique ID which can be used to look it up in the alertDismissals dictionary.
static var alertCounter = 0
// Keeps track of which alert VCs have been dismissed manually by the user using the 'OK' button, to avoid the DispatchQueue block in 'showAlertPopup' from setting the status of canPresentNextSegue to 'true' erroneously when the already-dismissed alert 'times out' and attempts to dismiss itself again
static var alertDismissals: [Int: Bool] = [:]
// Tuple representations of any alert view controllers which were not able to be immediately presented due to an earlier alert VC still being active. This allows them to be retrieved and presented once there is an opening.
static var suspendedAlertParameters: [(alertItem: String, counter: Int, VC: UIViewController)] = []
// Analogous to the preceding variable, but for the main 'Present Modally' segue
static var suspendedSegueParameters: (identifier: String, VC: UIViewController)? = nil
static func showAlertPopup(_ alertItem: String, counter: Int, VC: UIViewController) {
alertDismissals[counter] = false
alertCounter += 1 // Automatially increment this, so that the next alert has a different ID
// Present the alert if there isn't one already being presented
if canPresentNextSegue {
let alert = UIAlertController(title: "Test Alert", message: alertItem, preferredStyle: .alert)
let OKButton = UIAlertAction(title: "OK", style: .cancel) { (action) in
alertDismissals[counter] = true
canPresentNextSegue = true
return
}
alert.addAction(OKButton)
VC.present(alert, animated: true, completion: nil)
// Automatically dismiss alert after 3 seconds
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
if alertDismissals.keys.contains(counter) && !alertDismissals[counter]! {
alert.dismiss(animated: true)
canPresentNextSegue = true
}
}
// Because this current alert is now being shown, nothing else can be presented until it is dismissed, resetting this boolean to 'true' (either with the OK button or the DispatchQueue block)
canPresentNextSegue = false
}
// If there is another alert being presented, store this one in tuple representation for later retrieval
else {
suspendedAlertParameters.append((alertItem: alertItem, counter: counter, VC: VC))
}
}
}
}
Since you are not familiar with RxSwift I present below the entirety of the solution. This solution doesn't use segues. The CLE library takes over all view controller routing for you. It does this with generic coordinators that it creates and destroys for you so you never have to worry about them.
Create a new project.
Import the RxSwift, RxCocoa and Cause_Logic_Effect libraries.
Remove all the swift code in it and the storyboard. Remove the manifest from the info.plist and remove "main" from the "Main interface" entry in the target.
Add a new .swift file to the project and paste all the code below into it.
import Cause_Logic_Effect
import RxCocoa
import RxSwift
import UIKit
// the app delegate for the app.
#main
final class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
window = {
let result = UIWindow(frame: UIScreen.main.bounds)
result.rootViewController = ViewController().configure { $0.connect() }
result.makeKeyAndVisible()
return result
}()
return true
}
}
// the main view controller, notice that the only thing in the VC itself is setting up the views. If you use a
// storyboard or xib file, you can delete the viewDidLoad() completely.
final class ViewController: UIViewController {
#IBOutlet var goForwardButton: UIButton!
let disposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
// create the view. This could be set up in a xib or storyboard instead.
view.backgroundColor = .white
let button = UIButton(frame: CGRect(x: 100, y: 100, width: 50, height: 50)).setup {
$0.backgroundColor = .green
}
view.addSubview(button)
goForwardButton = button
}
}
// The second view controller, same as the first but the button name and color is different.
final class SecondViewController: UIViewController {
#IBOutlet var goBackButton: UIButton!
override func viewDidLoad() {
super.viewDidLoad()
// create the view. This could be set up in a xib or storyboard instead.
view.backgroundColor = .white
let button = UIButton(frame: CGRect(x: 100, y: 100, width: 50, height: 50)).setup {
$0.backgroundColor = .red
}
view.addSubview(button)
goBackButton = button
}
}
// here's where the magic happens.
extension ViewController {
func connect() {
let alertMessages = ["1st Item", "2nd Item", "3rd Item", "4th Item"]
goForwardButton.rx.tap
.flatMap {
displayWarnings(messages: alertMessages, interval: .seconds(3))
}
.subscribe(onNext: presentScene(animated: true) {
SecondViewController().scene { $0.connect() }
})
.disposed(by: disposeBag)
/*
When the goForwardButton is tapped, the closure in the flatMap will present the alerts, then the closure in the
subscribe will present the second view controller.
*/
}
}
func displayWarnings(messages: [String], interval: RxTimeInterval) -> Observable<Void> {
Observable.from(messages)
.concatMap(presentScene(animated: true) { message in
UIAlertController(title: nil, message: message, preferredStyle: .alert)
.scene { $0.dismissAfter(interval: interval) }
})
.takeLast(1)
/*
This displays the alerts in succession, one for each message in the array and sets up the dismissal.
When the last alert has been dismissed, it will emit a next event and complete. Note that this function is reusable
for any number of alerts with any duration in any view controller.
*/
}
extension UIAlertController {
func dismissAfter(interval: RxTimeInterval) -> Observable<Void> {
let action = PublishSubject<Void>()
addAction(UIAlertAction(title: "OK", style: .default, handler: { _ in action.onSuccess(()) }))
return Observable.amb([action, .just(()).delay(interval, scheduler: MainScheduler.instance)])
/*
This will close the alert after `interval` time or when the user taps the OK button.
*/
}
}
extension SecondViewController {
func connect() -> Observable<Void> {
return goBackButton.rx.tap.take(1)
/*
When the user taps the goBackButton, this will notify the CLE library that it's complete and the library will dismiss it.
*/
}
}
You almost could do this with standard callback closures, except implementing the concatMap with callback closures would be a huge PITA.
If you look inside the CLE library, you will see that it sets up a serial queue to present/dismiss view controllers and waits for their completion before allowing the next one to present dismiss. Also, it finds the top view controller itself so you don't ever have to worry about presenting from a VC that is already presenting something.

is this firestore closure causing a memory leak?

So my goal is to fix this condition issue when it comes to instantiating the right viewController. I have a function that I basically use to navigate a user to the right viewController depending on the type of user and if they're logged in or not.
Here is this function :
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let windowScene = (scene as? UIWindowScene) else { return }
let window = UIWindow(windowScene: windowScene)
self.window = window
let auth = Auth.auth()
let actualuser = Auth.auth().currentUser
auth.addStateDidChangeListener { (_, user) in
if user != nil {
db.document("student_users/\(actualuser?.uid)").getDocument { (docSnapshot, error) in
if error != nil {
print("\(error)")
} else {
let docSnap = docSnapshot?.exists
guard docSnap! else {
let alreadyLoggedInAsASchoolViewController = self.storyboard.instantiateViewController(withIdentifier: Constants.StoryboardIDs.SchoolEventDashboardStoryboardID) as! SchoolTableViewController
let navigationizedSchoolVC = UINavigationController(rootViewController: alreadyLoggedInAsASchoolViewController)
self.window!.rootViewController = navigationizedSchoolVC
self.window!.makeKeyAndVisible()
return
}
let alreadyLoggedInAsAStudentViewController = self.storyboard.instantiateViewController(withIdentifier: Constants.StoryboardIDs.StudentEventDashboardStoryboardID) as! StudentSegmentedTableViewController
let navigationizedVC = UINavigationController(rootViewController: alreadyLoggedInAsAStudentViewController)
self.window!.rootViewController = navigationizedVC
self.window!.makeKeyAndVisible()
}
}
} else {
let notLoggedInAtAll = self.storyboard.instantiateViewController(withIdentifier: Constants.StoryboardIDs.GothereMainMenuStoryboardID) as! GothereMainMenuViewController
let navMainMenu = UINavigationController(rootViewController: notLoggedInAtAll)
self.window!.rootViewController = navMainMenu
self.window!.makeKeyAndVisible()
}
}
}
I also have the exact block of code like this for the sceneDidEnterForeground for push notification purposes. Now the issue is when I run the simulator and launch the app for the first time, the correct viewController will show up, but when I logout as a school user and login as a school user in that same simulator session, the wrong viewController (aka the viewController of the other type of user) shows up.
Not that it would be a real situation in production where a student user would just have access to a school user's account and log in like that in the same scene session, but still, better to be safe than sorry. So this leads to me ask, is this a memory leak or a completely different issue?
I also get this error :
Your query is based on the variable actualuser, which looks like it is only set once, when the scene is first set up. Inside the state change callback, it's never updated.
So, when you log out, then log back in as a different user, that initial value of actualuser will be used, explaining why you see the wrong view controller. Then, when you run the app again and the scene is set up, actualuser gets set to the auth().currentUser again, showing you the correct view controller.
The solution here is to base your query on the current (and current) user.
Something like:
db.document("student_users/\(user.uid)")
(Instead of checking user != nil, do an optional binding with let user = user and then you can avoid the ? unwrapping)
This is not, by the way, a memory leak, which is a different type of issue: https://en.wikipedia.org/wiki/Memory_leak

Run Modal after async call made

I am new to Swift Mac App development, I am having troubling going from a login window to showing the main window after a login URLRequest, and making another URLRequest in the new main window. If I just go from one window without making the login URLRequest, it works fine.
func loadMainView() {
self.view.window?.close()
let storyboard = NSStoryboard(name: "Main", bundle: nil)
let mainWindowController = storyboard.instantiateController(withIdentifier: "MainViewController") as! NSWindowController
if let mainWindow = mainWindowController.window {
let application1 = NSApplication.shared()
application1.runModal(for: mainWindow)
}
}
func tryLogin(_ username: String, password: String ) {
Staff.login(username: self.username.stringValue, password: self.password.stringValue) { (completed, result, staff) in
// DispatchQueue.main.async {
if completed {
if result == .Success && staff != nil {
DispatchQueue.main.async(execute: {
self.loadMainView()
})
} else {
self.dialogOKCancel(message: "Error", text: "Your credentials are incorrect")
}
} else {
print("error")
}
}
}
HTTPSConnection.httpGetRequestURL(token: token, url: digialOceanURL, mainKey: mainKeyVal) { ( complete, results) in
DispatchQueue.main.async {
if complete {
}
}
}
I have tried calling the self.loadMainView() without the execute, but still not luck.
Any help appreciated. Thanks
Don't run your main view in modal, modal should be used for dialogs. So you can run login view in modal (and finish it by calling stopModal on the application). In that case you could use smth like loadLoginView which will have similar implementation to your current loadMainView (but without this self.view.window?.close() call. And main view would be loaded from nib on application launch. But you have to post some more code (how your app initalization looks like?) to get help on that.

how to segue to 2nd page from successful login - "warning attempt to present on while a presentation is in progress" error

How do I segue to my 2nd page after successfully verifying login?
I have pulled a segue from the login page view controller (not the login button) to the next page and named the segue 'nextPage'. (If I segue from the login button then the button click allows all logins to segue through without testing them). When I segue from the login page it correctly checks details but does not segue to the next page on successful login, and instead I get the console error "Warning: Attempt to present on while a presentation is in progress!"
the code is
#IBAction func loginButtonTapped(sender: AnyObject) {
let userEmail = userEmailTextField.text;
let userPassword = userPasswordTextField.text;
let userEmailStored = NSUserDefaults.standardUserDefaults().stringForKey("userEmail");
let userPasswordStored = NSUserDefaults.standardUserDefaults().stringForKey("userPassword");
if userEmailStored == userEmail && userPasswordStored == userPassword {
// Login successful
// Display an alert message
displayMyAlertMessage("Login successful. Thank you");
NSUserDefaults.standardUserDefaults().setBool(true,forKey:"isUserLoggedIn");
NSUserDefaults.standardUserDefaults().synchronize();
print("login success!")
self.dismissViewControllerAnimated(true, completion:nil);
self.performSegueWithIdentifier("nextPage", sender: self);
} else if userEmailStored != userEmail {
// Login unsuccessful (email incorrect)
NSUserDefaults.standardUserDefaults().setBool(false,forKey:"isUserLoggedIn");
print("login unsuccessful. Incorrect email.")
// Display an alert message
displayMyAlertMessage("Incorrect login details.");
return;
} else if userPasswordStored != userPassword {
// Login unsuccessful (password incorrect)
// Display an alert message
displayMyAlertMessage("Incorrect login details");
//return;
NSUserDefaults.standardUserDefaults().setBool(false,forKey:"isUserLoggedIn");
print("login unsuccessful. Incorrect password.")
}
The login page comes after an initial 'protected' login/logout screen as ViewController.swift with this code
override func viewDidAppear(animated: Bool)
{
let isUserLoggedIn = NSUserDefaults.standardUserDefaults().boolForKey("isUserLoggedIn");
if(!isUserLoggedIn)
{
self.performSegueWithIdentifier("loginView", sender: self);
}
}
#IBAction func logoutButtonTapped(sender: AnyObject) {
NSUserDefaults.standardUserDefaults().setBool(false,forKey:"isUserLoggedIn");
NSUserDefaults.standardUserDefaults().synchronize();
self.performSegueWithIdentifier("loginView", sender: self);
}
}
I do suggest to have a different approach on this.
If you set a storyboardID to LoginViewController you can directly manage to override the Protected page checking directly in AppDelegate.
For example you can try to do this
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
[...]
let isUserLoggedIn = NSUserDefaults.standardUserDefaults().boolForKey("isUserLoggedIn")
if isUserLoggedIn {
let storyboard = UIStoryboard(name: "Main", bundle: NSBundle.mainBundle())
let vc = storyboard.instantiateViewControllerWithIdentifier("IDYOUSETBEFORE")
window.rootViewController = vc
return
}
I recently managed to release a pod in order to easily handle this situations, have a look at StoryboardEnum lib
I solved this by removing the alert controller function, i.e. the code
displayMyAlertMessage("Login successful. Thank you");
as this was segueing to the 'login successful' popup view controller, instead of the segue that I needed, and in effect blocking the next page, while also not really necessary, as successful login means moving to the next page. I was able to still keep the alert/ popups for 'incorrect login details' which were the only essential alerts.

Is there anyway to display UIAlertController ONLY when app launches?

so I have the following code in the viewDidAppear section
let theAlert = UIAlertController(title: "SUP", message: "DAWG", preferredStyle: UIAlertControllerStyle.Alert)
theAlert.addAction(UIAlertAction(title: "sup!", style: UIAlertActionStyle.Default, handler: nil))
self.presentViewController(theAlert, animated: true, completion: nil)
Don't mind the messages, I just came up with them randomly :3
Okay, so is there anyway for me to ONLY display this message when the app launches? Because when I come back from another controller, this message pops up again.
Set a flag to indicate if the message has shown or not.
// first check to see if the flag is set
if alertShown == false {
// show the alert
alertShown = true
}
For this behavior to persist through launches, and show only on FIRST launch, save to NSUserDefaults.
// when your app loads, check the NSUserDefaults for your saved value
let userDefaults = NSUserDefaults.standardUserDefaults()
let alertShown = userDefaults.valueForKey("alertShown")
if alertShown == nil {
// if the alertShown key is not found, no key has been set.
// show the alert.
userDefaults.setValue(true, forKey: "alertShown")
}
You can handle both of these in the root view controller viewDidLoad.