I have a CoreData app with a fairly long list of data fields. When a user edits the fields but attempts to exit the DetailViewController without saving the edits, I put up an alert asking if they really want to discard the changes. This works fine, but if the user taps the home key, the edits are lost. I've tried to present an alert before the app enters the background but have been unable to delay entry into background to allow for user input. Is it possible to delay app entry into the background while waiting for user input?
Here's what I tried:
func applicationWillResignActive(_ notification : Notification) {
//this does not work - alert is too late
cancelUnsavedEdits()
//try above
}//applicationWillResignActive
The canelUnsavedEdits method is fairly straight forward:
func cancelUnsavedEdits() {
if hasChanged {
let ac = UIAlertController(title: nil, message: nil, preferredStyle: .alert)
ac.addAction(UIAlertAction(title: "Delete Edits", style: .default, handler: { (action : UIAlertAction!) -> Void in
self.codeDismissTheKeyboard()
self.performSegue(withIdentifier: "unwindToMasterViewController", sender: self)
let editRecordButton = UIBarButtonItem(barButtonSystemItem: UIBarButtonSystemItem.edit, target: self, action: #selector(DetailViewController.editThisRecord))
self.navigationItem.rightBarButtonItem = editRecordButton
self.navigationItem.leftBarButtonItem = nil
self.navigationItem.hidesBackButton = false
//need to remove the edits - refresh the original page
self.configureView()
}))//addAction block
ac.addAction(UIAlertAction(title: "Save Edits", style: .default, handler: { (action : UIAlertAction!) -> Void in
self.codeDismissTheKeyboard()
self.saveTheEditedRecord()
self.performSegue(withIdentifier: "unwindToMasterViewController", sender: self)
}))//addAction block
//for -ipad add code in handler to reopen the fields for editing if the cancel of the cancel is chosed
ac.addAction(UIAlertAction(title: "Cancel", style: .default, handler: { (whatever) in
//print("makeEntryFieldsEnabledYES for ipad")
self.makeEntryFieldsEnabledYES()
}))
//above for ipad
self.present(ac, animated: true, completion: nil)
} else {
self.codeDismissTheKeyboard()
//add this for ipad
self.navigationItem.rightBarButtonItem = nil
//add above for ipad
self.performSegue(withIdentifier: "unwindToMasterViewController", sender: self)
}//if hasChanged
//this for ipad
navigationItem.leftBarButtonItem = nil
//above for iPad
}//cancelUnsavedEdits
Any guidance on a strategy to accomplish this idea would be appreciated. iOS 10, Xcode 8.1
No. Not possible.
iOS does give your app some time to clean up or save data, but not enough time for user interaction. The reasoning is that the user DID interact and wants to exit your app. Maybe save the data the user entered and present it when they return, but do not try to prevent the user from exiting.
Related
I'm new to Swift programming, but can't find an answer to my problem, which is...
When I present a simple UIAlertController with a UIAlertAction handler, I am expecting the alert to display until the user responds, then the handler is executed, before continuing with the remaining code.
Unexpectedly, it seems to finish off the code block before displaying the alert and executing the handler.
I've searched Stackoverflow, and re-read the Apple Developer Documentation for UIAlertController and UIAlertAction, but I can't figure out why the code doesn't pause until the user responds.
I've tried putting the UIAlertController code in its own function, but the alert still appears to be displaying out of sequence. I'm thinking maybe there needs to be a delay to allow the Alert to draw before the next line of code executes(?).
#IBAction func buttonTapped(_ sender: Any) {
let alert = UIAlertController(title: "Ouch", message: "You didn't have to press me so hard!", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "Sorry", style: .default, handler: { _ in
self.handleAlert()
}))
self.present(alert, animated: true, completion: nil)
print("Should be printed last!")
}
func handleAlert() {
print("UIAlertAction handler printed me")
}
In the code above I am expecting the debug console to display:
UIAlertAction handler printed me
Should be printed last!
But instead it displays:
Should be printed last!
UIAlertAction handler printed me
Instead of adding a seperate function, can you put it within the alert action itself like this...
let alert = UIAlertController(title: "Ouch", message: "You didn't have to press me so hard!", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "Sorry", style: .default, handler: { action in
// code for action goes here
}))
self.present(alert, animated: true)
UIAlertController is designed to run asynchronously (that is why it has you pass a block of code to execute when the action is performed instead of giving a return value)
So to fix your code, put the code you want to run after an action is chosen in another function, then call that function at the end of each UIAlertAction handler.
private var currentlyShowingAlert = false
#IBAction func buttonTapped(_ sender: Any) {
if currentlyShowingAlert {
return
}
let alert = UIAlertController(title: "Ouch", message: "You didn't have to press me so hard!", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "Sorry", style: .default, handler: { _ in
self.handleAlert()
self.alertCleanup()
}))
self.present(alert, animated: true, completion: nil)
currentlyShowingAlert = true
}
func handleAlert() {
print("UIAlertAction handler printed me")
}
func alertCleanup() {
print("Should be printed last!")
currentlyShowingAlert = false
}
Be careful when doing things like pushing view controllers (or anything where the calls will stack up) in direct response to a button press.
When the main thread is busy, the button can be pressed multiple times before the first buttonTapped call happens, in that case buttonTapped could be called many times in a row, currentlyShowingAlert will prevent that issue.
I have a universal master-detail application where I present the master
and detail views side by side on an iPad in both orientations. When a
user makes changes in the detail view on an iPhone I can easily detect
changes and present an alert asking if they want to save the changes or
lose them (CoreData). On an iPad, there is no prohibition against simply clicking
in the master list thereby losing the edits.
I have placed a function in the DetailViewController viewWillDisappear
(for iPad) that raises an alert, but the compiler tells me that it does
not like the presentation of a view on a non-connected view.
Console message: Presenting view controllers on detached view
controllers is discouraged <>.
Is there a more appropriate way to handle this? Swift 3, iOS 10, Xcode 8.2.1
var hasChanged //set to true whenever edits are made
override func viewWillDisappear(_ animated: Bool) {
if UIDevice.current.model == "iPad" {
if hasChanged {
print("hasChanged (should be true) is: \(hasChanged)")
cancelUnsavedEdits()
}//if hasChanged
}//if ipad
}//viewWillDisappear
func cancelUnsavedEdits() {
if hasChanged {
let ac = UIAlertController(title: nil, message: nil, preferredStyle: .alert)
ac.addAction(UIAlertAction(title: "Delete Edits", style: .default, handler: { (action : UIAlertAction!) -> Void in
self.codeDismissTheKeyboard()
self.performSegue(withIdentifier: "unwindToMasterViewController", sender: self)
let editRecordButton = UIBarButtonItem(barButtonSystemItem: UIBarButtonSystemItem.edit, target: self, action: #selector(DetailViewController.editThisRecord))
self.navigationItem.rightBarButtonItem = editRecordButton
self.navigationItem.leftBarButtonItem = nil
self.navigationItem.hidesBackButton = false
//need to remove the edits - refresh the original page
self.configureView()
}))//addAction block
ac.addAction(UIAlertAction(title: "Save Edits", style: .default, handler: { (action : UIAlertAction!) -> Void in
self.codeDismissTheKeyboard()
self.saveTheEditedRecord()
self.performSegue(withIdentifier: "unwindToMasterViewController", sender: self)
}))//addAction block
//ac.addAction(UIAlertAction(title: "Cancel", style: .default, handler: nil))
//try this - for -ipad add code in handler to reopen the fields for editing if the cancel of the cancel is chosen
ac.addAction(UIAlertAction(title: "Cancel", style: .default, handler: { (whatever) in
self.makeEntryFieldsEnabledYES()
let cancelItemButton = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(DetailViewController.cancelUnsavedEdits))
self.navigationItem.leftBarButtonItem = cancelItemButton
}))
//try above
self.present(ac, animated: true, completion: nil)
} else {
self.codeDismissTheKeyboard()
//for ipad
self.navigationItem.rightBarButtonItem = nil
self.performSegue(withIdentifier: "unwindToMasterViewController", sender: self)
}//if hasChanged
//for ipad
navigationItem.leftBarButtonItem = nil
}//cancelUnsavedEdits
I think the problem you try to present a new viewController on a viewController which is going to be removed from the view hierarchy.
Maybe a dirty fix can be to replace:
self.present(ac, animated: true, completion: nil)
to:
self.view.window.rootViewController?.present(ac, animated: true, completion: nil);
I have a side project app that uses a UIAlertController with a textbox to get input from the user. My problem is, when a user enters text and presses 'Add', the Keyboard stays open for a good 1-3 seconds before dismissing. However, if the user presses the return key with text entered in the textbox on the keyboard, this does not happen. I originally thought it was from using the completion handlers and having to wait for it to complete, but since using the return key works fine, I don't think that is the case.
This is where I display the alert:
func addItem(view: UIViewController, completion: (text: String?) -> Void) {
let diag = UIAlertController(title: "Add Task", message: "Enter a task name", preferredStyle: .Alert)
diag.addTextFieldWithConfigurationHandler({ (textField) -> Void in })
diag.addAction(UIAlertAction(title: "Add", style: .Default, handler: { (action) -> Void in
let textOfTask = diag.textFields![0] as UITextField
let textValue = textOfTask.text!
if textValue.characters.count > 0 {
completion(text: textValue)
}
}))
diag.addAction(UIAlertAction(title: "Cancel", style: .Cancel, handler: { (action: UIAlertAction!) in
completion(text: nil)
}))
view.presentViewController(diag, animated: true, completion: nil)
}
And how I am calling this function is:
#IBAction func didPressAdd(sender: AnyObject) {
addItem(self) {
(text) in
if let itemText = text {
// Check if has all the datas
if (self.def.objectForKey("simplest_itemlist") != nil) {
// Does have all the datas
self.tableView.reloadData()
}
// Append new data
self.itemList.append(itemText)
self.tableView.reloadData()
// Save to UserDefaults
self.def.setObject(self.itemList, forKey: "simplest_itemlist")
self.def.synchronize()
print(self.itemList)
}
}
}
I have dealt with this issue previously. Unfortunately, in all my investigations I have come to the same conclusions as you have. There is now a built-in delay when a button is pressed on the alert. No code that you change or don't run or run on another thread will prevent this delay. Others have also confirmed that this is the functionality built by Apple.
So if I had to take a bet its likely because of the fact that all of this is happening on the main thread, which makes sense if the issue doesn't happen when you hit return without entering anything in. You want your app to be as responsive as possible, so everything user-interaction based and UI based will happen on the main thread. What you probably want to do is asynchronously save the text, which means that any data saving you'll be doing with the inputted text will happen on another thread that isn't dealing with dismissing the alertView. Does that make sense?
Edit: Thank you for the replies, yes I needed an alert and not an action sheet! I have implemented this new code and it works, but is there a reason why it segues to the next view before the user can enter a ride title?
Also it throws this message in the debug console, should I be concerned?
2016-02-16 12:30:21.675 CartoBike[687:128666] Presenting view controllers on detached view controllers is discouraged .
#IBAction func stopAction(sender: UIButton) {
let alert = UIAlertController(title: "Ride Stopped", message: "Give a title to your ride", preferredStyle: .Alert)
let saveAction = UIAlertAction(title: "Save", style: .Default,
handler: { (action:UIAlertAction) -> Void in
// Allow for text to be added and appended into the RideTableViewController
let textField = alert.textFields!.first
rideContent.append(textField!.text!)
})
let cancelAction = UIAlertAction(title: "Cancel",
style: .Default) { (action: UIAlertAction) -> Void in
}
alert.addTextFieldWithConfigurationHandler {
(textField: UITextField) -> Void in
}
alert.addAction(saveAction)
alert.addAction(cancelAction)
// Save the ride
saveRide()
// Automatically segue to the Ride details page
self.performSegueWithIdentifier("ShowRideDetail", sender: nil)
presentViewController(alert, animated: true, completion: nil)
timer.invalidate()
self.stopLocation()
}
Overview:
I am working my way through my first real app. The basic logic is a home screen to start a new ride or view previous rides. Starting a ride will open a new view with a map to record a bicycle ride based on the users location, this can be stopped and save the ride and immediately switch to a new view to see a map of the ride. Alternatively from the home screen the user can select previous rides and view a list of their old rides in a table view and select one and transition to a detailed view with a map of their ride.
Problem: When adding a UIAlertAction I would like there to be a save and cancel feature. In addition, I would like the user to be able to add a custom title by typing it in via a text field. The input from the text field will be appended to a global variable called rideContent that is tied to the creation of new cells in the table view to store multiple bike rides by unique title.
Research:
I have reviewed the questions titled "How to add a TextField to UIAlertView in Swift" & "Writing handler for UIAlertAction" and still can't seem to discern what I have done wrong. Ideally the alert within this screenshot of an app from the raywenderlich site is what I would like it to look like. I am not sure if what I am trying to do is even possible since there a so many view controllers involved. Granted I am new to swift and I'm sure I am missing something obvious!
Currently getting an error of
"Type of expression is ambiguous without more context", see screenshot: here
Here is the UIAlertController code:
// The timer pauses & the location stops updating when the the stop button is pressed
#IBAction func stopAction(sender: UIButton) {
var inputTextField: UITextField?
let actionSheetController = UIAlertController (title: "Ride Stopped", message: "Add a title to your ride", preferredStyle: UIAlertControllerStyle.ActionSheet)
// Add a cancel action
actionSheetController.addAction(UIAlertAction(title: "Cancel", style: UIAlertActionStyle.Cancel, handler: nil))
// Add a save action
actionSheetController.addAction(UIAlertAction(title: "Save", style: UIAlertActionStyle.Default, handler: {
(actionSheetController) -> Void in
//Add a text field --- Getting an error on this line
actionSheetController.addTextFieldWithConfigurationHandler { textField -> Void in
// you can use this text field
inputTextField = textField
// Append the ride title to the table view
rideContent.append(textField!.text!)
// Update when user saves a new ride to store the ride in the table view for permanent storage
NSUserDefaults.standardUserDefaults().setObject(rideContent, forKey: "rideContent")
// Save the ride
self.saveRide()
// Automatically segue to the Ride details page
self.performSegueWithIdentifier("ShowRideDetail", sender: nil)
}}))
//present actionSheetController
presentViewController(actionSheetController, animated: true, completion: nil)
timer.invalidate()
self.stopLocation()
}
Thank you stack overflow for any help you may offer!
You're using an .ActionSheet while the tutorial you showed is using an .Alert
Action Sheets can have buttons but not text fields.
"Alerts can have both buttons and text fields, while action sheets only support buttons."
NSHipster
Use your tableview Array when Add Button is pressed and "Add alertview textfirld to tableview array and then reload tableview".
#IBAction func btnAdd(_ sender: Any) {
let alertController = UIAlertController(title: "Add Category", message: "", preferredStyle: .alert)
let saveAction = UIAlertAction(title: "Add", style: .default, handler: { alert -> Void in
let firstTextField = alertController.textFields![0] as UITextField
self.categories.add(firstTextField.text!)
self.tblCatetgory.reloadData()
})
let cancelAction = UIAlertAction(title: "Cancel", style: .default, handler: {
(action : UIAlertAction!) -> Void in })
alertController.addTextField { (textField : UITextField!) -> Void in
textField.placeholder = "Enter Category!!!"
}
alertController.addAction(saveAction)
alertController.addAction(cancelAction)
self.present(alertController, animated: true, completion: nil)
}
}
I have the below code which shows an alert box when user signs up. After the user interacts with the Alert Box button (i.e. let's go), I have a segue I need triggered (called someSegue for now) so that it can redirect the user to login page.
Nothing is happening after user clicks alert box button. What am I missing? is it the self there? if I remove the self it doesn't work and xcode complains.
user.signUpInBackgroundWithBlock {
(succeeded, error) -> Void in
if error == nil {
// Hooray! Let them use the app now.
let delay = 4.5 * Double(NSEC_PER_SEC)
let time = dispatch_time(DISPATCH_TIME_NOW, Int64(delay))
dispatch_after(time, dispatch_get_main_queue()) {
actInd.stopAnimating()
var alert = UIAlertController(title: "Congratulations!", message: "Your account was successfully created! You will be required to login with your credentials.", preferredStyle: UIAlertControllerStyle.Alert)
alert.addAction(UIAlertAction(title: "Let's Go!", style: UIAlertActionStyle.Default, handler: nil))
self.presentViewController(alert, animated: true, completion: nil)
self.performSegueWithIdentifier("someSegue", sender: self)
}
} else {
// Show the errorString somewhere and let the user try again.
}
}
If you want to execute a block of code after alert button is pressed you need to pass that block in handler parameter of addAction method
alert.addAction(UIAlertAction(title: "Let's Go!", style: UIAlertActionStyle.Default, handler: { (action: UIAlertAction!) in
self.performSegueWithIdentifier("someSegue", sender: self)
}))