Instruments shows a memory leak from simply opening and closing the alert controller.
#IBAction func delBtnAc(sender: AnyObject) {
let deleteAlert = UIAlertController(title: "Delete Image?", message: "", preferredStyle: .Alert)
let cancelIt = UIAlertAction(title: "Cancel", style: .Cancel, handler: nil)
deleteAlert.addAction(cancelIt)
presentViewController(deleteAlert, animated: true, completion: nil)
}
I have reduced the alert to only a cancel button for testing.
Edited: Removed deleteAlert.dismissViewController in closure. Fixed retain cycle, but still shows a memory leak. Perhaps a bug.
Your alert action's completion handler has a strong reference to your alert controller.
Your alert action has a strong reference to its completion handler.
Your alert controller has a strong reference to the alert action.
So here we have a classic retain cycle.
The problem is the strong reference from the completion handler to the alert controller itself, which in this case, happens to be completely unnecessary. The alert controller dismisses itself after running the appropriate completion handler.
We can completely eliminate the line.
If we were doing something non-redundant in the completion handler, we would need to create a weak reference to the completion handler so that we could use that in the completion handler.
I found the same problem.
I solved it by setting the alert to null after button action:
deleteAlert = null inside your cancel button action
Related
I'm showing alert in a function which I call from VC. I don't want main VC to be blocked. I'm calling this alert function in async. The function in turn has another async. Is this good practice or I'm doing it wrong?
Can anyone suggest good practice for following code?
class MyVC: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Don't block main thread
DispatchQueue.main.async {
self.showAlert(title: "Title", message: "Message")
}
// Do other stuff ...
}
}
func showAlert(title: String = "", message: String) {
alert = UIAlertController(title: title,message: message, preferredStyle: .alert)
let cancelAction = UIAlertAction(title: "Ok", style: .cancel, handler: nil)
alert.addAction(cancelAction)
DispatchQueue.main.async {
UIApplication.shared.keyWindow?.rootViewController!.present(alert, animated: true, completion: nil)
}
}
Showing an alert doesn't block the thread. present(_:animated:completion:) is not a blocking operation, so there's no reason to add any of these .async calls.
That said, you wouldn't want to try to present an alert inside viewDidLoad. That's far too early. Your view controller isn't on the screen yet. You should put showAlert() in viewDidAppear, and remove all the .async calls.
As a general rule, these kinds of modal alerts should be a last resort in any case, especially when a view controller is coming on screen. Generally, you should integrate whatever message you want to present to the user into the view itself rather than blocking the entire UI. But if an alert is appropriate (and sometimes they are), then you can just present them directly as long as you're on the main queue.
It seems you have a problem with DispatchQueue :)
Your program uses operation queues while running. This queues can be system-defined(like main) or user defined. When you use DispatchQueue.main.async(_:) you enqueue a code block to main queue. When their time comes, main queue execute them.
But in viewDidLoad(_:), you are already in main queue. Also, cause of calling an AlertController is a UI operation and UI operations can't be done on any queue except main, you don't need to send your code block to any queue and you shouldn't.
And also, Like #SeanRobinson159 said, AlertController does not block main thread when it is on screen. It works like your orther ViewControllers.
So, Which cases you should use DispatchQueue to call an AlertController
You should use DispatchQueue.main.async(_:) to send code blocks that performs UI operations (like calling AlertController or changing UILabel's text), to main queue from different queue. For example, maybe you work on an network operation. You make your operation in different thread and when result comes, You can send your code block that makes UI operations, to main queue.
You can google GCD(Grand Central Dispatch) for deatiled information.
You should only put what you have to on to the main thread. So you shouldn't wrap it in the main.async block in the viewDidLoad function.
You should wrap it in a DispatchQueue with a different priority.
i.e. DispatchQueue.global(qos: .userInitiated).async { }
Swift 4, XCode 9.4
I cannot pop back to the root of the stack in the completion of (or even near) a UIAlertController.
The requirement is to show the user a confirmation and then directly transition back to the 'home' page. When I try I always get something like :
popToViewController:transition: called on <UINavigationController 0x7fc4fe85f000> while an existing transition or presentation is occurring; the navigation stack will not be updated.
I understand the concept I suppose. The UIAlertController is in control, or the nav controller is not sufficiently in control to pop back. Ok makes some sense since I am in the completion. But I don't see how to trigger my transition without being in there.
#IBAction func doneAction(_ sender: Any) {
if verifyDoneProperly() {
let alert = UIAlertController(title: "Complete", message: "Good Job", preferredStyle: UIAlertControllerStyle.alert)
alert.addAction(UIAlertAction(title: "Ok", style: UIAlertActionStyle.default, handler: nil))
self.present(alert, animated: true, completion: {
if let nav = self.navigationController {
nav.popToViewController(select, animated: true)
}
})
}
}
FWIW, It doesn't work when I specify the view controller either.
I have read carefully through this answer but nothing there is really going to help me (AFAIK).
The only thing I can really think to do is to send a message to the root controller and have it display the alert after having transitioned back (Perhaps checking for a flag in viewWillAppear)
There must surely be a correct way to do this. Can anyone suggest something?
Important Note: I cannot use segues here although I do have a storyboard. The transition to the 'worker' view controller is done programmatically and chosen based on the current state.
I'm not sure it's the best idea to pop a view controller while it's still presenting something else. And the completion of present is not executed after the view controller is dismissed, only after it has finished presentation (i.e., become visible on the screen).
You can pop your view controller in the handler of your OK action — which will be called after your view controller is no longer presenting the alert.
alert.addAction(UIAlertAction(title: "OK", style: .default, handler: { (_) in
self.navigationController?.popToViewController(select, animated: true)
}))
Make sure to perform the pop animation in the next run loop by embedding it in a dispatch block:
DispatchQueue.main.async {
self.navigationController?.popToViewController(select, animated: true)
}
This is i have written in viewDidLoad.
if DBSession.shared().isLinked() {
print("already linked")
initDropboxRestClient()
}
else
{
print("connecting2")
DBSession.shared().link(from: self)
initDropboxRestClient()
}
and function initDropboxRestClient() is written below.
func initDropboxRestClient() {
dbRestClient = DBRestClient(session: DBSession.shared())
dbRestClient.delegate = self
dbRestClient.loadMetadata("/")
}
The problem is i have two view controllers for displaying dropbox file names, the first view controller is calling the delegate methods and displaying filename and folder names perfectly. But the second one isn't.
In the second view controller,
I observed that if i scroll my tableview in second view controller up and down then the delegate methods get called immediately and once it is linked next time the methods are called immediately.
So for the first time delegate methods are not getting called in my second dropbox view controller thats my problem here. Thanks in advance.
The restClient delegate methods are as follows.
func restClient(_ client: DBRestClient!, loadedMetadata metadata: DBMetadata!) {
for file in metadata.contents
{
dbMetadataArray.append(file as! DBMetadata)
fileNamesArray.append((file as AnyObject).filename)
}
tableView.reloadData()
self.myActivityIndicator.stopAnimating()
self.myActivityIndicator.hidesWhenStopped = true
}
func restClient(_ client: DBRestClient!, loadMetadataFailedWithError error: Error!) {
print("in loadMetadataFailedWithError method in dropbox email view controller")
print("Error dscription = %#",[error.localizedDescription])
let alert = UIAlertController(title: "Go Back.", message: "Try Once Again", preferredStyle: UIAlertControllerStyle.alert)
alert.addAction(UIAlertAction(title: "OK", style: UIAlertActionStyle.default, handler: nil))
self.myActivityIndicator.stopAnimating()
self.myActivityIndicator.hidesWhenStopped = true
tableView.reloadData()
}
There are a few things that might cause your delegate methods to not be called:
Your rest client is nil or is being released (e.g., by ARC) prematurely.
You're making the call in a background thread that doesn't have a run loop.
Your delegate method that should be called back has a typo in it. Unfortunately the SDK doesn't warn you if it can't find a delegate method to call; it just completes without telling anyone.
Also, note that the SDK you're using uses API v1, which is deprecated and being retired soon anyway:
https://blogs.dropbox.com/developers/2016/06/api-v1-deprecated/
You should switch to API v2:
https://www.dropbox.com/developers/documentation
So the first thing my app does is get a list of movies from an API. I'm trying to handle if there's a network issue. Currently, in my viewDidLoad method, I call "updateApiInfo", which contains the following code:
if self.movies == nil {
print("stuff went wrong")
let alert = UIAlertController(title: "Network Error", message: "There was a nework error.\nYou must be connected to the internet to use Flicks.", preferredStyle: UIAlertControllerStyle.Alert)
alert.addAction(UIAlertAction(title: "Exit", style: UIAlertActionStyle.Default, handler: {(UIAlertAction) in
UIControl().sendAction(Selector("suspend"), to: UIApplication.sharedApplication(), forEvent: nil)
}))
self.presentViewController(alert, animated: true, completion: nil)
}
When viewDidLoad calls updateApiInfo, I get this message:
Warning: Attempt to present <UIAlertController: 0x7fad10e3ad80> on <Flicks.MoviesViewController: 0x7fad10e35cc0> whose view is not in the window hierarchy!
When I call updateApiInfo just from the user refreshing, the error pops up as expected.
I'm assuming that viewDidLoad only gets called before the view gets displayed to the user, which I guess is causing this error. Is there a method I can stick this code in for after the view is displayed to the user, so the problem can presumably get fixed?
You need to use viewDidAppear: to make sure the view is already in the window hierarchy.
There's a pretty good explanation of the order in which the methods are called here:
What is the process of a UIViewController birth (which method follows which)?
I was under the impression that if the normal action is a destructive action and the other is a cancel action in their UIAlertController that the destructive one should be on the left and the cancel should be on the right.
If the normal action is not destructive, then the normal action should be on the right and the cancel should be on the left.
That said, I have the following:
var confirmLeaveAlert = UIAlertController(title: "Leave", message: "Are you sure you want to leave?", preferredStyle: .Alert)
let leaveAction = UIAlertAction(title: "Leave", style: .Destructive, handler: {
(alert: UIAlertAction!) in
//Handle leave
}
)
let cancelAction = UIAlertAction(title: "Cancel", style: .Cancel, handler: nil)
confirmLeaveAlert.addAction(leaveAction)
confirmLeaveAlert.addAction(cancelAction)
self.presentViewController(confirmLeaveAlert, animated: true, completion: nil)
I was under the impression that if I add the leaveAction first, then the cancelAction that the leaveAction would be the button on the left. This was not the case. I tried adding the buttons in the opposite order as well and it also resulted in the buttons being added in the same order.
Am I wrong? Is there no way to achieve this?
My solution to this was to use the .Default style instead of .Cancel for the cancelAction.
Since iOS9 there is a preferredAction property on UIAlertController. It places action on right side. From docs:
When you specify a preferred action, the alert controller highlights the text of that action to give it emphasis. (If the alert also contains a cancel button, the preferred action receives the highlighting instead of the cancel button.)
The action object you assign to this property must have already been added to the alert controller’s list of actions. Assigning an object to this property before adding it with the addAction: method is a programmer error.