Keyboard lags when using UIAlertController - ios

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?

Related

UIAlertController not displaying immediately

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.

RPScreenRecorder How recording screen audio and video of an app Action Extension?

I would like to record the screen, audio, and video of my app with target Action Extension.
If I put this code in a normal app, it works, but in an Action Extension doesn't.
#IBAction func recButton(_ sender: Any) {
if recButton.currentTitle == "stop" {
stopRecording()
recButton.setTitle("rec", for: .normal)
}
else {
recButton.setTitle("stop", for: .normal)
RPScreenRecorder.shared().isMicrophoneEnabled = true
RPScreenRecorder.shared().startRecording(handler: {[unowned self] (error) in
//Handler - never called
if let unwrappedError = error {
print(unwrappedError.localizedDescription)
}
})
}
}
func stopRecording() {
RPScreenRecorder.shared().stopRecording(handler: {(previewController, error) -> Void in
//Handler - never called
if previewController != nil {
let alertController = UIAlertController(title: "Recording", message: "Do you want to discard or view your recording?", preferredStyle: .alert)
let discardAction = UIAlertAction(title: "Discard", style: .default) { (action: UIAlertAction) in
RPScreenRecorder.shared().discardRecording(handler: { () -> Void in
// Executed once recording has successfully been discarded
})
}
let viewAction = UIAlertAction(title: "View", style: .default, handler: { (action: UIAlertAction) -> Void in
self.present(previewController!, animated: true, completion: nil)
})
alertController.addAction(discardAction)
alertController.addAction(viewAction)
self.present(alertController, animated: true, completion: nil)
} else {
// Handle error
}
})
}
Is there another method to achieve this goal using AVCaptureSession, or do I need to use something else to achieve this? Thanks.
I'm pretty sure Apple won't let you do that by design. When it comes to extensions, they are generally very strict both in terms of what is allowed api-wise and what will pass app review.
Even if you figure out a hacky solution to overcome the issues with ReplayKit, I guess it would get rejected by app review.
In the general App Review Guidelines, the app extension programming guide is referenced as a defining guideline where for action extension it specifically says:
In iOS, an Action extension:
Helps users view the current document in a different way
Always appears in an action sheet or full-screen modal view
Receives selected content only if explicitly provided by the host app
Not quite sure how a screen recording would fit into that pattern in a way that convinces Apple...
I don't think that this is not possible, as I have seen some apps on App Store, which are recording video and Audio within iMessage extension app. Like : SuperMoji App This app records face expression and audio
and sends as a video message within iPhone's Message app only.
However, I am not sure how to do that in Extension apps. I am working on it and will let you know very soon.

Allow User Input Before App Enters Background

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.

Why is my app resetting due to a function that has nothing to do with resetting it?

I am writing a swift/Xcode app and have a particular page where a user can upload a picture, which is saved to a database. Upon success of the upload, I give the user a pop up telling them that the save was successful. Here is the relevant code for that bit:
func displayAlert(title: String, message: String){ //all possible alerts go into here
if #available(iOS 8.0, *) {
let alert = UIAlertController(title: title, message: message, preferredStyle: UIAlertControllerStyle.Alert)
alert.addAction(UIAlertAction(title: "Ok", style: .Default, handler: { (action) in
self.dismissViewControllerAnimated(true, completion: nil)
}))
self.presentViewController(alert, animated: true, completion: nil)
} else {
// Fallback on earlier versions
}
}
Thats the function that is called. Here is where and how it is called:
if error == nil{
self.imageToPost.image = UIImage(named: "blah.jpg" )
self.message.text = "" //input fields are set back to original
self.displayAlert("Success", message: "Your image has been uploaded")
} else{}
If the image is uploaded successfully, which it is, the error returns nil and the code runs. The user gets the alert, clicks ok, and then... the app resets. It doesn't close out, but immediately goes to the main page viewcontroller (which is not this page's viewcontroller)
I have deduced that the displayAlert function must be at fault, because commenting out the self.displayAlert("Success", .....) fixes the issue, and the app does not reset. But I see nothing in the syntax that would cause this issue. (There is the exact same function in the main page's swift file - I copied and pasted it over - could that be the issue?). I am stumped
Your call to self.dismissViewControllerAnimated(true, completion: nil) dismisses your view and shows your previous ViewController (Main) in the stack.

iOS dismiss UIAlertController in response to event

I have a situation where I want to present a UIAlertController in order to wait for an event (asynchronous request for data from third party) to finish before showing my main ViewController to the user to interact with.
Once the asynchronous code finishes, then I want to dismiss the UIAlertController. I know that normally UIAlertControllers are setup with a button to dismiss it, which is input from the user. I am wondering if what I want to do (dismiss with an event instead of user input) is possible?
So far, I tried to show the UIAlertController, and then wait in a while loop checking a boolean for when the event occurs:
var alert = UIAlertController(title: "Please wait", message: "Retrieving data", preferredStyle: UIAlertControllerStyle.Alert)
self.presentViewController(alert, animated: true, completion: nil)
// dataLoadingDone is the boolean to check
while (!dataLoadingDone) {
}
self.dismissViewControllerAnimated(true, completion: nil)
This gives a warning
Warning: Attempt to dismiss from view controller while a presentation or dismiss is in progress!
and does not dismiss the UIAlertController. I also tried alert.dismissViewControllerAnimated(true, completion: nil) instead of self.dismissViewControllerAnimated(true, completion: nil), but this doesn't get rid of the UIAlertController either.
I wouldn't use a while loop but a didSet observer for your dataLoadingDone property. Thereby, you may try something similar to the following code:
class ViewController: UIViewController {
var dismissAlertClosure: (() -> Void)?
var dataLoadingDone = false {
didSet {
if dataLoadingDone == true {
dismissAlertClosure?()
}
}
}
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
let alert = UIAlertController(title: "Please wait", message: "Retrieving data", preferredStyle: .Alert)
presentViewController(alert, animated: true, completion: nil)
// Set the dismiss closure to perform later with a reference to alert
dismissAlertClosure = {
alert.dismissViewControllerAnimated(true, completion: nil)
}
// Set boolValue to true in 5 seconds in order to simulate your asynchronous request completion
var dispatchTime: dispatch_time_t = dispatch_time(DISPATCH_TIME_NOW, Int64(5.0 * Double(NSEC_PER_SEC)))
dispatch_after(dispatchTime, dispatch_get_main_queue(), { self.dataLoadingDone = true })
}
}
Another user (matt) gave what I'm pretty sure is the correct answer, but he removed it, so I'm going to answer it. Just wanted to give credit (though he seems pretty reputable anyways).
What he wrote is that my UIAlertController presentation was not finished before I tried to dismiss it, so I got that error. I changed my code to the following:
// check if I need to wait at all
if (!dataLoadingDone) {
var alert = UIAlertController(title: "Please wait", message: "Retrieving data", preferredStyle: UIAlertControllerStyle.Alert)
self.presentViewController(alert, animated: true, completion: { () -> Void in
// moved waiting and dismissal of UIAlertController to inside completion handler
while (!self.dataLoadingDone) {
}
self.dismissViewControllerAnimated(true, completion: nil)
})
}
I moved the waiting and dismissal inside the completion handler of the presentViewController call, so that I know the presenting is done before dismissing. Also, I check if I need to wait at all with the first if statement, because otherwise another issue occurs if the while loop never does anything.
I'm not 100% sure yet, because my boolean is actually never false at the moment (the data retrieval happens really quickly). However, I have reason to believe it will take longer later on in my app development, so I will update the answer here once I have that.
EDIT: another user who previously posted an answer (Aaron Golden) was also correct about while loop blocking. I had thought that the while loop shares processing time with other events, but apparently not (or at least not enough time). Therefore, the above while loop DOES NOT work

Resources