When does the completion block of dismiss view get executed?
Is it after or before the user sees the view dismissed?
I have this code to make a toast with a message inside completion block but never see the toast after this view dismissed.
self.dismiss(animated: true, completion: {
self.view.makeToast(message: "Employee has been assigned successfully.", duration: 2.0, position: HRToastPositionCenter as AnyObject, title: "Succeeded!")
})
What I want is the user can see the toast when the view gets completely dismissed?
How do I do this ?
You can delegate the event from presented controller to the parent and handle it there.
In EmployeePickerViewController (or whatever your modal controller is called):
#protocol EmployeePickerDelegate {
func employeeAssigned()
}
class EmployeePickerViewController {
weak delegate: EmployeePickerDelegate!
}
When employee assignment is finished just call delegate's method:
delegate?.employeeAssigned()
In MainViewController when you present modally:
employeePicker.delegate = self
present(employeePicker, animated: true, completion: nil)
In MainViewController below:
extension MainViewController: EmployeePickerDelegate {
func employeeAssigned {
dismiss(animated: true, completion: {
self.view.makeToast(message: "Employee has been assigned successfully.", duration: 2.0, position: HRToastPositionCenter as AnyObject, title: "Succeeded!")
})
}
}
For UIViewController.dismiss(animated:completion:)
The completion handler is called after the viewDidDisappear(_:) method
is called on the presented view controller.
Source
For UIViewController.present(_:animated:completion:)
The completion handler is called after the viewDidAppear(_:) method is
called on the presented view controller.
Source.
If you don't know when that is, this is the order of the UIViewController load, appear and dissapear methods
viewDidLoad
normally we initialize data objects and controls. It will create all
the necessary memory for all controls/data objects for this view. i.e.
In the above case, anotherView and btnView, they will keep the same
memory addresses for the whole life cycle.
viewWillAppear
Called before the view is added to the windows’ view hierarchy. So it
is ideal for updating the viewcontroller’s data.
viewDidAppear
Called after the view is added to the windows’ view hierarchy.
viewWillDisappear
Called before the view is removed from the windows’ view hierarchy.
viewDidDisappear
Called after the view is removed from the windows’ view hierarchy.
Source
You are calling a dismiss on self, so every reference of that will be dealloc. your self.view doesn't exist anymore I guess.
The completion block is executed after the View Controller has been dismissed. This means your view is no longer on screen. I think you want to render the toast inside that view, which is not possible because it is off screen.
The toast will be shown inside your dismissed view. Since the view has disappeared you won't see its toast. You might want to show the toast in the following screen that appears after the dismissed view.
Maybe could be easier to show the toast on the window instead of a particular view?
UIApplication.shared.keyWindow?.makeToast(message: "Employee has been assigned successfully.", duration: 2.0, position: HRToastPositionCenter as AnyObject, title: "Succeeded!")
Related
I'm using material design dialog for my iOS app written with swift. Here is the brief documentation of material design dialogs: https://material.io/develop/ios/components/dialogs/
I have a dialog which has 1 action and in the completion block of the action, I want to dismiss the view controller and go back to the previous view controller. The problem is that dismissing the view controller doesn't work. All instructions which are written in the completion block, such as printing something, execute except for dismissing view controller.
Here is my code :
DispatchQueue.main.async {
let alertStr = "Alert"
let alertController = MDCAlertController(title: "Error", message: alertStr)
let action = MDCAlertAction(title:"GoBack") { (action) in
self.dismiss(animated: false, completion: nil)
}
alertController.addAction(action)
self.present(alertController, animated:true, completion:nil)
}
I'd appreciate if you could help me figure out the problem.
Thanks in advance !
A couple of thoughts:
The dismiss(animated:completion:) “Dismisses the view controller that was presented modally by the view controller.” It’s not intended to dismiss the the view controller referenced by self.
Admittedly, dismiss will, “If you call this method on the presented view controller itself, UIKit asks the presenting view controller to handle the dismissal.” But you can’t rely upon that within the UIAlertAction for the button, because you don’t know when the dismissal of the MDCAlertController and when the action of the button is performed.
Are you sure you presented view controller and that it’s not, for example, having instead pushed on a navigation controller?
A good way of getting back to a prior view controller is an unwind segue (or see TN2298). That eliminates all ambiguity about “push” v “present” and whether dismiss will dismiss a presented view controller and instead pass the message to the presenting view controller.
have you tried to
performSegue(withIdentifier: "ViewControllerSegue", sender: nil)
you need to select your viewController on the top-bar the yellow square(name is what you predefine)
right-click and drag to the next view controller ---> Present Modally
then select the arrow and go to attributes inspector and name the identifier.
I am trying to display a child view controller over the top of all elements on screen (including navigation bars), and the only way I've found that works is to add it as a child view controller to my window's rootViewController:
guard let window = UIApplication.shared.keyWindow,
let view = window.rootViewController?.view
else { return }
window.rootViewController?.addChildViewController(attachmentViewController)
view.addSubview(attachmentViewController.view)
attachmentViewController.view.snp.makeConstraints { make in
make.left.equalTo(view)
make.right.equalTo(view)
make.top.equalTo(view)
make.bottom.equalTo(view)
}
attachmentViewController.didMove(toParentViewController: window.rootViewController)
However, this doesn't call the viewDidAppear or viewWillDisappear methods... Why is that? I really need it to.
Instead of doing all that, simply present the view controller (don't push it as suggested).
let destination = SomeViewController.instantiateFromStoryboard(self.storyboard!)
present(destination, animated: true, completion: nil)
Focusing on the "why is that?" of your question.
When you call addChildViewController to a view you're not changing the "stack" of view controllers at all or the state of the host view controller; you're just adding a view controller as a child controller of the main view.
Usually when you work with child view controllers you orchestrate calls like willMove and didMove to trigger the view controller lifecycle behaviour.
In your case, you may be better off with a push or present. Present will give you the capability of overlaying a view controller.
As a note, I have used an approach similar to what you describe for managing sign in/out states adding either a signed in child view controller or a signed out view controller. In which case, when they change I usually call methods like:
// To add the child
addChildViewController(child)
view.addSubview(child.view)
child.didMove(toParentViewController: self)
// To remove the child.
child.willMove(toParentViewController: nil)
child.removeFromParentViewController()
child.view.removeFromSuperview()
I have 3 ViewController.
The first ViewController is checking if the user is logged in.
If yes performSegue to the mainVC and if no performSegue to loginVC.
When I am in loginVC, I log in and performSegue to mainVC.
What I want now is, I want to have all ViewControllers which are unused being "deleted", to save memory.
How is that going to work?
I found here in StackOverflow this piece of code:
class ManualSegue: UIStoryboardSegue {
override func perform() {
sourceViewController.presentViewController(destinationViewController, animated: true) {
self.sourceViewController.navigationController?.popToRootViewControllerAnimated(false)
UIApplication.sharedApplication().delegate?.window??.rootViewController = self.destinationViewController
}
}
}
Is that going to do what I want? It seems like yes because this method is popping the ViewController.
I am using "Show Detail" - segues only, except when using this method I created a custom Segue Segue.
Deletion should be handled by Apple, you (theoretically) shouldn't have to worry about it, so long as you don't create any retain cycles. As a rule, just don't have any strong references to self in blocks. Funny enough, the code you have above, that should dismiss the ViewController (and therefore delete it) also has a retain cycle. Adding [weak self] and strongSelf casts as needed should help:
override func perform() {
sourceViewController.presentViewController(destinationViewController, animated: true) { [weak self] in
guard let strongSelf = self else { return }
strongSelf.sourceViewController.navigationController?.popToRootViewControllerAnimated(false)
UIApplication.sharedApplication().delegate?.window??.rootViewController = strongSelf.destinationViewController
}
}
Memory question
Yes, that is how it works. You do not need to take care of freeing view controllers.
The system will keep track of references to view controller objects. When you do not have references to these anymore then the memory is deallocated. You can read about this more in swift language documentation:
https://developer.apple.com/library/content/documentation/Swift/Conceptual/Swift_Programming_Language/AutomaticReferenceCounting.html
What the code is doing
presentViewController method is showing a view controller modally. The completion closure is performed after presenting the new view controller finishes. Inside closer 2 things happen
popToRootViewControllerRemoves all view controllers inside the sourceViewController object.
rootViewController of the window is set to new value.
This practically changes the root view controller to another one. This seems like a valid action after successful login.
I do not know if step 1 is necessary. That navigation view controller is going to go away anyway so why to pop view controllers inside it?
More about view controllers
You might be also interested in view controller life cycle. UIKit developer documentation contains in-depth details about view controllers:
https://developer.apple.com/documentation/uikit/uiviewcontroller
I have this code in one of my IBAction (when a button is pressed), which is supposed to bring up a new view controller.
let addAlertVC = self.storyboard?.instantiateViewController(withIdentifier: "addAlert")
self.present(addAlertVC!, animated: false, completion: nil)
However, when I run the app and press the button that's supposed to take me to the new viewcontroller, but then I'm stuck with the original viewcontroller. I have put a print statement in the viewDidAppear function in the new view controller, and it is printing out whenever I press the button, so the new controller is definitely appearing. I have not dismissed the new controller anywhere in my app.
I have used the same code in other parts of my app, so I'm extremely confused as to why it's not working this time.
Any help would be greatly appreciated.
EDIT: I fixed my code. It turns out it wasn't how I was calling the view controller that was wrong, it was my button. Once I deleted and re-added the button, my code now works.
There is a good chance that your view controller is deallocated after you present it.
Try to declare your view controller outside of the function, in your controller. Something like:
class ViewController{
var addAlertVC:UIViewController?
...
func someFunction(){
addAlertVC = self.storyboard?.instantiateViewController(withIdentifier: "addAlert")
self.present(addAlertVC!, animated: false, completion: nil)
}
}
I'm having some trouble playing around with two viewcontrollers that interact in a straightforward manner:
The homeViewController shows a to-do list, with an addTask button.
The addTask button will launch an additional viewController that acts as a "form" for the user to fill.
However, upon calling
self.dismissViewControllerAnimated(true, completion: nil);
inside the presented view controller I return to my home page, but it's blank white and it seems nothing can be seen except the highest-level view on the storyboard can be seen (i.e. the one that covers the entire screen).
All of my views, scenes, etc. were set up with autolayout in storyboard. I've looked around on Stack Overflow, which lead to me playing around with the auto-resizing subview parameter i.e.:
self.view.autoresizesSubviews = false;
to no avail. I'm either fixing the auto-resizing parameter wrong (in the wrong view of interest, or simply setting it wrong), or having some other problem.
Thanks in advance
edit:
I present the VC as follows:
func initAddNewTaskController(){
let addNewTaskVC = self.storyboard?.instantiateViewControllerWithIdentifier("AddNewTaskViewController") as! AddNewTaskViewController;
self.presentViewController(addNewTaskVC, animated: true, completion: nil);
}
edit2:
While I accept that using delegates or unwinding segue can indeed circumvent the problem I'm encountering (as campbell_souped suggests), I still don't understand what's fundamentally happening when I dismiss my view controller that causes a blank screen.
I understand that calling dismissViewControllerAnimated is passed onto the presenting view controller (in this case my homeViewController). Since I don't need to do any pre or post-dismissal configurations, the use of a delegate is (in my opinion) unnecessary here.
My current thought is that for some reason, when I invoke
dismissViewControllerAnimated(true, completion:nil);
in my addNewTaskViewController, it is actually releasing my homeViewController. I'm hoping someone can enlighten me regarding what it is exactly that I'm not understanding about how view controllers are presented/dismissed.
In a situation like this, I usually take one of two routes. Either set up a delegate on AddNewTaskViewController, or use an unwind segue.
With the delegate approach, set up a protocol:
protocol AddNewTaskViewControllerDelegate {
func didDismissNewTaskViewControllerWithSuccess(success: Bool)
}
Add an optional property that represents the delegate in your AddNewTaskViewController
var delegate: AddNewTaskViewControllerDelegate?
Then invoke the didDismissNewTaskViewControllerWithSuccess whenever you are about to dismiss AddNewTaskViewController:
If the record was added successfully:
self.delegate?.didDismissNewTaskViewControllerWithSuccess(true)
self.dismissViewControllerAnimated(true, completion: nil);
Or if there was a cancelation/ failure:
self.delegate?.didDismissNewTaskViewControllerWithSuccess(false)
self.dismissViewControllerAnimated(true, completion: nil);
Finally, set yourself as the delegate, modifying your previous snippet:
func initAddNewTaskController(){
let addNewTaskVC = self.storyboard?.instantiateViewControllerWithIdentifier("AddNewTaskViewController") as! AddNewTaskViewController;
self.presentViewController(addNewTaskVC, animated: true, completion: nil);
}
to this:
func initAddNewTaskController() {
guard let addNewTaskVC = self.storyboard?.instantiateViewControllerWithIdentifier("AddNewTaskViewController") as AddNewTaskViewController else { return }
addNewTaskVC.delegate = self
self.presentViewController(addNewTaskVC, animated: true, completion: nil);
}
...
}
// MARK: AddNewTaskViewControllerDelegate
extension homeViewController: AddNewTaskViewControllerDelegate {
func didDismissNewTaskViewControllerWithSuccess(success: Bool) {
if success {
self.tableView.reloadData()
}
}
}
[ Where the extension is outside of your homeViewController class ]
With the unwind segue approach, take a look at this Ray Wenderlich example:
http://www.raywenderlich.com/113394/storyboards-tutorial-in-ios-9-part-2
This approach involves Ctrl-dragging from your IBAction to the exit object above the view controller and then picking the correct action name from the popup menu