popToRootViewControllerAnimated and Switch TabBar Index - ios

I am in the 3rd UIViewController of a UINavigationController and would like to dismiss all UIViewControllers before switching tabs on the UITabBarController that contains my UINavigationController.
self.navigationController?.popToRootViewControllerAnimated(true)
self.tabBarController?.selectedIndex = 1
The code above does not switch tab bar indexes. It seems that code after popToRootViewControllerAnimated never runs.
What are my options?

AFAIK, there is no API to provide a completion block to popToRootViewControllerAnimated and I'm guessing that once you pop the VC, it's gone and no more code executes. It's like you want to provide a completion block to be performed once the animation completes.
The UINavigationController API itself doesn't offer any options for this.
However by using a combination of CoreAnimation framework and NSNotifications it's possible to add a completion block that posts a notification, which the root view controller can listen for.
You might even be able to get away without the CoreAnimation trickery, and just post the notification after you popToRootViewControllerAnimated but I haven't tried that yet.
This would be the code for the bottom view controller in the stack:
class DetailViewController: UIViewController {
#IBAction func popAndSwitchTabs(sender: AnyObject) {
CATransaction.begin()
CATransaction.setCompletionBlock { () -> Void in
NSNotificationCenter.defaultCenter().postNotificationName("switchTabsNotification", object: nil)
}
self.navigationController?.popToRootViewControllerAnimated(true)
CATransaction.commit()
}
}
And then use code like this in the top view controller in the stack:
class FirstViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
NSNotificationCenter.defaultCenter().addObserver(self, selector: "switchTabs", name: "switchTabsNotification", object: nil)
}
func switchTabs() {
self.tabBarController?.selectedIndex = 1
}
}
Here's a quick example project I threw together that you can try out on Github:
https://github.com/obuseme/PopAndSwitchExample

Related

View Controller must refresh itself when coming to front

I have an app that I am developing using Swift 4.0. I have a View Controller on which I am showing some useful information and user can interact with them. Lets call this as BaseViewController.
What I am doing:
The BaseViewController is starting different other ViewControllers, and than user can dismiss those viewControllers and come back to BaseViewController.
What I want:
Now I want that whenever user comes back to BaseViewController it gets itself updated. I know it can be done using Protocols, but I just want a simple way. Like in Android there is onResume method to perform updates whenever Activity comes into active state.
I think there is no need to share code as starting other viewController from one viewcontroller is pretty simple. I just wanted to know the better approach. Thanks in advance
UPDATE: THIS IS HOW I AM CALLING NEXT CONTROLLER OVER BASE CONTROLLER
let dialogRegisterForEventVC = UIStoryboard(name: "Main",bundle: nil).instantiateViewController(withIdentifier: "idDialogRegisterForEventVC") as! DialogRegisterForEventVC
dialogRegisterForEventVC.modalPresentationStyle = .overCurrentContext
dialogRegisterForEventVC.modalTransitionStyle = .crossDissolve
dialogRegisterForEventVC.isUserLogin = isLogin
self.present(dialogRegisterForEventVC, animated: true) {
}
You can always go with viewWillAppear Method
override func viewWillAppear(_ animated: Bool) {
print("This is called when coming back to Base View Controller")
}
Use your refresh code here. This is called when you pop your viewController
There different cases and you need to handle them for example
If you presenting another viewController on top of this you should use
override func viewWillAppear(_ animated: Bool)
If you need to refresh the view if the app comes in foreground state you need such code :
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(self, selector: #selector(willResignActive), name: UIApplication.willEnterForegroundNotification, object: nil)
}
#objc func willResignActive(_ notification: Notification) {
refreshMyView()
}
Also, you can create delegate that your viewController can call to update this view controller. And one more option is to make your view controller observer of custom notification in Notification center and to push this notification once when you need to refresh the view controller.
If you using the notification center just be sure that your UI changes are on the main thread.

Get a notification with viewWillAppear - in another VC?

Say you have
var someVC: UIViewController
is it possible to essentially do the following, somehow?
get a notification when {
someVC has a viewWillAppear
self.#selector(wow)
}
#objc func wow() {
print("we spied on that view controller, and it just willAppeared"
}
Is that possible ?
(Or maybe on didLayoutSubviews ?)
(I realize, obviously, you can do this by adding a line of code to the UIViewController in question. That's obvious. I'm asking if we can "add on" to it from elsewhere.)
If I understand your question correctly, you want ViewController B to receive a notification when viewWillAppear is called in ViewController A? You could do this through the Notifications framework. Keep in mind that both VC's have to be loaded for one to receive a notification.
Alternatively, if the two VC's are on the screen at the same time, then I'd recommend a delegate pattern - have VC A tell an overarcing controller class that it's viewWillAppear has been called, and this overarcing controller will then inform ViewController B.
To do this using Notifications:
(This is from memory, so please excuse typos)
class TestClassA: UIViewController {
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// To improve this code, you'd pull out the Notification name and perhaps put it into an extension, instead of hardcoding it here and elsewhere.
NotificationCenter.default.post(Notification.init(name: Notification.Name.init(rawValue: "viewControllerAppeared")))
}
}
class TestClassB: UIViewController {
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
NotificationCenter.default.addObserver(self, selector: #selector(viewControllerAppeared(notification:)), name: Notification.Name.init(rawValue: "viewControllerAppeared"), object: nil)
}
#objc func viewControllerAppeared(notification: NSNotification) {
print("other viewcontroller appeared")
}
}
Documentation

where does back event is handel in swift 3.0

I want to refresh the whole page controller on back press.
I am navigating the viewcontroller using code.
My Code
let GTC = self.storyboard?.instantiateViewController(withIdentifier: "GoToCart")as! GoToCart
self.navigationController?.pushViewController(GTC, animated: true)
Using viewWillAppear to reload your UI. As you use navigationController?.pushViewController, the view will be retained and stored in stack.
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
// Reload the UI
}
viewWillAppear(_:)
viewWillAppear is called the first time the view is displayed as well as when the view is displayed again, so it can be called multiple times during the life of the view controller object. It’s called when the view is about to appear as a result of the user tapping the back button, when the view controller’s tab is selected in a tab bar controller etc. Make sure to call super.viewWillAppear() at some point in the implementation. You can refresh your UI in this method
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
// Reload the UI
}
A better approach is to use protocol
Create protocol from where you want to pop back(GoToCart)
Create delegate variable in GoToCart
Extend GoToCart protocol in MainViewController
Give reference to GoToCart of MainViewController when
navigate
Define delegate Method in MainViewController
Then you can call delegate method from GoToCart
Example
In GoToCart: Write code below..
protocol GoCartControllerDelegate
{
func childViewControllerResponse(parameter)
}
class GoToCart:UIViewController
{
var delegate: ChildViewControllerDelegate?
....
}
Then in mainViewController implement the protocol function end extend to the protocol
class MainViewController:UIViewController,GoCartControllerDelegate
{
// Define Delegate Method
func childViewControllerResponse(parameter)
{
//...here update what you want to update according to the situation
}
}
2 Important thing
when navigating to the gocart controller code like this
let GTC = self.storyboard?.instantiateViewController(withIdentifier: "GoToCart")as! GoToCart
GTC.delegate = self
self.navigationController?.pushViewController(GTC, animated: true)
and when popping from gocartViewController
code like this
self.navigationController?.popViewController(animated:true)
self.delegate?.childViewControllerResponse(parameter)

Detect when a presented view controller is dismissed

Let's say, I have an instance of a view controller class called VC2. In VC2, there is a "cancel" button that will dismiss itself. But I can't detect or receive any callback when the "cancel" button got trigger. VC2 is a black box.
A view controller (called VC1) will present VC2 using presentViewController:animated:completion: method.
What options does VC1 have to detect when VC2 was dismissed?
Edit: From the comment of #rory mckinnel and answer of #NicolasMiari, I tried the following:
In VC2:
-(void)cancelButton:(id)sender
{
[self dismissViewControllerAnimated:YES completion:^{
}];
// [super dismissViewControllerAnimated:YES completion:^{
//
// }];
}
In VC1:
//-(void)dismissViewControllerAnimated:(BOOL)flag completion:(void (^)(void))completion
- (void)dismissViewControllerAnimated:(BOOL)flag
completion:(void (^ _Nullable)(void))completion
{
NSLog(#"%s ", __PRETTY_FUNCTION__);
[super dismissViewControllerAnimated:flag completion:completion];
// [self dismissViewControllerAnimated:YES completion:^{
//
// }];
}
But the dismissViewControllerAnimated in the VC1 was not getting called.
There is a special Boolean property inside UIViewController called isBeingDismissed that you can use for this purpose:
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
if isBeingDismissed {
// TODO: Do your stuff here.
}
}
According to the docs, the presenting controller is responsible for the actual dismiss. When the presented controller dismisses itself, it will ask the presenter to do it for it. So if you override dismissViewControllerAnimated in your VC1 controller I believe it will get called when you hit cancel on VC2. Detect the dismiss and then call the super classes version which will do the actual dismiss.
As found from discussion this does not seem to work. Rather than rely on the underlying mechanism, instead of calling dismissViewControllerAnimated:completion on VC2 itself, call dismissViewControllerAnimated:completion on self.presentingViewController in VC2. This will then call your override directly.
A better approach altogether would be to have VC2 provide a block which is called when the modal controller has completed.
So in VC2, provide a block property say with the name onDoneBlock.
In VC1 you present as follows:
In VC1, create VC2
Set the done handler for VC2 as: VC2.onDoneBlock={[VC2 dismissViewControllerAnimated:YES completion:nil]};
Present the VC2 controller as normal using [self presentViewController:VC2 animated:YES completion:nil];
In VC2, in the cancel target action call self.onDoneBlock();
The result is VC2 tells whoever raises it that it is done. You can extend the onDoneBlock to have arguments which indicate if the modal comleted, cancelled, succeeded etc....
Use a Block Property
Declare in VC2
var onDoneBlock : ((Bool) -> Void)?
Setup in VC1
VC2.onDoneBlock = { result in
// Do something
}
Call in VC2 when you're about to dismiss
onDoneBlock!(true)
You can use UIViewControllerTransitioningDelegate on the parent view controller that you want to observe the dismissal of another presented view controller:
anotherViewControllerYouWantToObserve.transitioningDelegate = self
And observe the dismissal on:
func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
print("anotherViewControllerYouWantToObserve was dismissed")
return nil
}
Both the presenting and presented view controller can call dismissViewController:animated: in order to dismiss the presented view controller.
The former option is (arguably) the "correct" one, design-wise: The same "parent" view controller is responsible for both presenting and dismissing the modal ("child") view controller.
However, the latter is more convenient: typically, the "dismiss" button is attached to the presented view controller's view, and it has said view controller set as its action target.
If you are adopting the former approach, you already know the line of code in your presenting view controller where the dismissal occurs: either run your code just after dismissViewControllerAnimated:completion:, or within the completion block.
If you are adopting the latter approach (presented view controller dismisses itself), keep in mind that calling dismissViewControllerAnimated:completion: from the presented view controller causes UIKit to in turn call that method on the presenting view controller:
Discussion
The presenting view controller is responsible for
dismissing the view controller it presented. If you call this method
on the presented view controller itself, UIKit asks the presenting
view controller to handle the dismissal.
(source: UIViewController Class Reference)
So, in order to intercept such event, you could override that method in the presenting view controller:
override func dismiss(animated flag: Bool,
completion: (() -> Void)?) {
super.dismiss(animated: flag, completion: completion)
// Your custom code here...
}
extension Foo: UIAdaptivePresentationControllerDelegate {
func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
//call whatever you want
}
}
vc.presentationController?.delegate = foo
Using the willMove(toParent: UIViewController?) in the following way seemed to work for me. (Tested on iOS12).
override func willMove(toParent parent: UIViewController?) {
super.willMove(toParent: parent);
if parent == nil
{
// View controller is being removed.
// Perform onDismiss action
}
}
This works well if you have a modal presentation that can be dismissed like a page sheet by swipe.
override func endAppearanceTransition() {
if isBeingDismissed{
print("dismissal logic here")
}
}
I have used deinit for the ViewController
deinit {
dataSource.stopUpdates()
}
A deinitializer is called immediately before a class instance is deallocated.
You can use unwind segue to do this task, no need to use the dismissModalViewController. Define an unwind segue method in your VC1.
See this link on how to create the unwind segue, https://stackoverflow.com/a/15839298/5647055.
Assuming your unwind segue is set up, in the action method defined for your "Cancel" button, you can perform the segue as -
[self performSegueWithIdentifier:#"YourUnwindSegueName" sender:nil];
Now, whenever you press the "Cancel" button in the VC2, it will be dismissed and VC1 will appear. It will also call the unwind method, you defined in VC1. Now, you know when the presented view controller is dismissed.
#user523234 - "But the dismissViewControllerAnimated in the VC1 was not getting called."
You can't assume that VC1 actually does the presenting - it could be the root view controller, VC0, say. There are 3 view controllers involved:
sourceViewController
presentingViewController
presentedViewController
In your example, VC1 = sourceViewController, VC2 = presentedViewController, ?? = presentingViewController - maybe VC1, maybe not.
However, you can always rely on VC1.animationControllerForDismissedController being called (if you have implemented the delegate methods) when dismissing VC2 and in that method you can do what you want with VC1
I use the following to signal to a coordinator that the view controller is "done". This is used in a AVPlayerViewController subclass in a tvOS application and will be called after the playerVC dismissal transition has completed:
class PlayerViewController: AVPlayerViewController {
var onDismissal: (() -> Void)?
override func beginAppearanceTransition(_ isAppearing: Bool, animated: Bool) {
super.beginAppearanceTransition(isAppearing, animated: animated)
transitionCoordinator?.animate(alongsideTransition: nil,
completion: { [weak self] _ in
if !isAppearing {
self?.onDismissal?()
}
})
}
}
I've seen this post so many times when dealing with this issue, I thought I might finally shed some light on a possible answer.
If what you need is to know whether user-initiated actions (like gestures on screen) engaged dismissal for an UIActionController, and don't want to invest time in creating subclasses or extensions or whatever in your code, there is an alternative.
As it turns out, the popoverPresentationController property of an UIActionController (or, rather, any UIViewController to that effect), has a delegate you can set anytime in your code, which is of type UIPopoverPresentationControllerDelegate, and has the following methods:
popoverPresentationControllerShouldDismissPopover
popoverPresentationControllerDidDismissPopover
Assign the delegate from your action controller, implement your method(s) of choice in the delegate class (view, view controller or whatever), and voila!
Hope this helps.
Another option is to listen to dismissalTransitionDidEnd() of your custom UIPresentationController
Create one class file (.h/.m) and name it : DismissSegue
Select Subclass of : UIStoryboardSegue
Go to DismissSegue.m file & write down following code:
- (void)perform {
UIViewController *sourceViewController = self.sourceViewController;
[sourceViewController.presentingViewController dismissViewControllerAnimated:YES completion:nil];
}
Open storyboard & then Ctrl+drag from cancel button to VC1 & select Action Segue as Dismiss and you are done.
If you override on the view controller being dimissed:
override func removeFromParentViewController() {
super.removeFromParentViewController()
// your code here
}
At least this worked for me.
You can handle uiviewcontroller closed using with Unwind Segues.
https://developer.apple.com/library/content/technotes/tn2298/_index.html
https://spin.atomicobject.com/2014/12/01/program-ios-unwind-segue/
overrideing viewDidAppear did the trick for me. I used a Singleton in my modal and am now able to set and get from that within the calling VC, the modal, and everywhere else.
As has been mentioned, the solution is to use override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil).
For those wondering why override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) does not always seem to work, you may find that the call is being intercepted by a UINavigationControllerif it's being managed. I wrote a subclass that should help:
class DismissingNavigationController: UINavigationController {
override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) {
super.dismiss(animated: flag, completion: completion)
topViewController?.dismiss(animated: flag, completion: completion)
}
}
If you want to handle view controller dismissing, you should use code below.
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
if (self.isBeingDismissed && self.completion != NULL) {
self.completion();
}
}
Unfortunately we can't call completion in overridden method - (void)dismissViewControllerAnimated:(BOOL)flag completion:(void (^ _Nullable)(void))completion; because this method is been called only if you call dismiss method of this view controller.
I didn't see what seems to be an easy answer. Pardon me if this is a repeat...
Since VC1 is in charge of dismissing VC2, then you need to have called vc1.dismiss() at some point. So you can just override dismiss() in VC1 and put your action code in there:
class VC1 : UIViewController {
override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) {
super.dismiss(animated: flag, completion: completion)
// PLACE YOUR ACTION CODE HERE
}
}
EDIT:
You probably want to trigger your code when the dismiss completes, not when it starts. So in that case, you should use:
override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) {
super.dismiss(animated: flag) {
if let unwrapCompletion = completion { unwrapCompletion() }
// PLACE YOUR ACTION HERE
}
}
A more productive approach would be to create a protocol for presentingControllers and then call in childControllers
protocol DismissListener {
func childControllerWillDismiss(_ controller : UIViewController, animated : Bool)
func childControllerDidDismiss(_ controller : UIViewController, animated : Bool)
}
extension UIViewController {
func dismissWithListener(animated flag: Bool, completion: (() -> Void)? = nil){
self.viewWillDismiss(flag)
self.dismiss(animated: flag, completion: {
completion?()
self.viewDidDismiss(true)
})
}
func viewWillDismiss(_ animate : Bool) {
(presentingViewController as? DismissListener)?.childControllerWillDismiss(self, animated: animate)
}
func viewDidDismiss(_ animate : Bool) {
(presentingViewController as? DismissListener)?.childControllerDidDismiss(self, animated: animate)
}
}
and then when the view is about to dismiss :
self.dismissWithListener(animated: true, completion: nil)
and finally just add protocol to any viewController that you wish to listen!
class ViewController: UIViewController, DismissListener {
func childControllerWillDismiss(_ controller: UIViewController, animated: Bool) {
}
func childControllerDidDismiss(_ controller: UIViewController, animated: Bool) {
}
}

Dismissing a Modal ViewController back to a Navigation Controller Stack imbedded in a TabBarController causing crash

Have been working for a long time trying to find the fix to my crash. Not sure if it's a fundamental design flaw on my part or something that can be fixed easily.
My app has a NavigationController embedded in a TabBarController. When I modally present a view from the root view controller of the NavigationController, it is presented and dismissed without any problem.
However, after i navigate forward in the navigation stack from the root ViewController of the NavigationController pushing other Controllers and then back to the root ViewController, I try the modally presentation again. This time it presents fine but when I dismiss it my app crashes with a EXC_BAD_ACCESS error.
After sometime debugging I found that when the modally presented ViewController is dismissed it is trying to access a ViewController in the navigation Heiarchy that has already been dismissed and thus the crash. Despite the fact that it was presented from the Root ViewController of the Navigation Stack.
It is causing me a lot of grief and wondering if anybody has come across something similar. Any help will be greatly appreciated. Thanks in advance.
Here is the presentation which occurs inside an AlertController Block:
let createNewMemoAction = UIAlertAction(title: "Create New Memo", style: .Default) { (action: UIAlertAction!) -> Void in
self.performSegueWithIdentifier("Segue To Create New Memo", sender: nil)
}
I am using an unwind to dismiss the viewController. I have tried dismissing with the following code with the same error:
#IBAction func cancelButtonPressed(sender: UIBarButtonItem) {
dismissViewControllerAnimated(true, completion: nil)
}
Thanks
did you implement the prepareForUnwind function in the presenting view controller? you need that in order for the unwind to work: here is an example in objective C but it's the same concept for swift!
This code goes in the presenting view controller:
#IBAction func backToHome(segue:UIStoryboardSegue) {
// code is not necessary here!
}
instead of writing code for dismissing the viewcontroller you just need to ctrl + drag from the button you use for dismissing to the exit at the top of the scene and then choose the function "backToHome" from the popup
you should check the connections:
https://www.dropbox.com/s/jwpaz1nxvjkautv/Screen%20Shot%202015-07-12%20at%206.16.23%20PM.png?dl=0
I recommend you do push segue and make your own protocol, that way you can navigate back to any viewcontroller. For your example, the pushed viewcontroller code I wrote is :
import UIKit
protocol BoxMessageDelegate {
func userDidType(controller: TextAddition, sendsmessage: String) -> Void;
}
class TextAddition: UIViewController {
var delegate: BoxMessageDelegate?;
var message: String?;
#IBOutlet weak var textMsgField: UITextField!
#IBAction func `return`(sender: UIButton) {
self.message = self.textMsgField.text;
[self.delegate!.userDidType(self, sendsmessage: message!)];
}
}
in the presenting view controller code is:
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if (segue.identifier == "MessageView") {
var controller = segue.destinationViewController as! TextAddition
controller.delegate = self
}
}
/*
conforming to protocol here
*/
func userDidType(controller: TextAddition, sendsmessage: String) {
mySquare.text = sendsmessage;
self.navigationController?.popToViewController(self, animated: true)
}
I found the problem. I had declared the second view in the NavigationController Hierarchy as the delegate for the NavigationController. That's why it was being referenced. When I removed that line of code the problem went away. Thanks very much for all who commented and helped out.

Resources