How to detect which ViewController is being popped on popViewController func override? - ios

I have sublassed UINavigationController to conform my needs and use case. There in order to detect back action I have overriden method that is properly called on desired action:
var popViewController: ((UIViewController) -> Void)?
override func popViewController(animated: Bool) -> UIViewController? {
return super.popViewController(animated: animated)
}
I would like to check which VC is being currently popped in order to compare it further to evaluate some properties.
First thing I tried is to add var that will be changed inside this override method.
var popViewController: ((UIViewController) -> Void)?
But I have no further clue what should be done.
Is that even possible to do here?

I don't understand why you have that variable with a closure type, you can check the view controller that is being popped right in the override method without needing any stored properties:
override func popViewController(animated: Bool) -> UIViewController? {
let popVC = super.popViewController(animated: animated) // this is the view controller that will be popped
// Do what ever check you want to do here
return popVC
}

According to the documentation, the result of calling func popViewController(animated: Bool) on a UINavigationController returns the popped controller if any was popped.
https://developer.apple.com/documentation/uikit/uinavigationcontroller/1621886-popviewcontroller

Related

UINavigationController has to call method when updates view with a new ViewController

I have a UINavigationControllerSubclass. When view controller is popped to some new view controller (by navigationController.popViewController, navigationController.popToRootViewController or even by manually sliding from left to right)
I need to call inside my navigation controller:
viewController.newTopViewController.updateBackButtonTitle()
What is the best approach to accomplish that?
One way of doing it would be the following:
class CustomNavigationController: UINavigationController {
override func popToRootViewController(animated: Bool) -> [UIViewController]? {
shouldUpdateBackButtonTitle()
return super.popToRootViewController(animated: animated)
}
override func popViewController(animated: Bool) -> UIViewController? {
shouldUpdateBackButtonTitle()
return super.popViewController(animated: animated)
}
private func shouldUpdateBackButtonTitle() {
viewController.newTopViewController.updateBackButtonTitle()
}
}
When you return to viewController call this viewWillAppear method. Inside that function you can check your rootviewController then you can call your
updateBackbuttonTitle()<
function.
You can use viewWillAppear method, and easily update UI Controls
super.viewWillAppear(animated)

XLPagerTabStrip select what view controller will be shown first

I'm using XLPagerTabStrip to switch among a collection of view controllers. I have three view controllers and I would like that middle view controller is shown by default as first.
I could use
let parentViewController = self.parent! as! ParentViewController
parentViewController.moveToViewControllerAtIndex(1)
inside my first view controller, but that first view controller loads some data from the server and if I switch to another view controller while it is loading data, that first view controller will freeze and it won't load data.
Is there a way to show middle view controller as first by default?
jump to the defenition of 'currentIndex' and change it to public from private. then you can select your current controller by this code:
currentIndex = 1
In function:
override func viewControllers(for pagerTabStripController:
PagerTabStripViewController) -> [UIViewController] {
// This line will help you achieve the requirement
pagerTabStripController.currentIndex = /* required index */
}
It will work smoothly after you make currentIndex in PagerTabStripViewController as public.
To prevent loading the first tab, moveToViewControllerAtIndex() must be called before viewDidLoad() is called in your PagerTabStripViewController subclass.
override func viewControllers(for pagerTabStripController: PagerTabStripViewController) -> [UIViewController] {
pagerTabStripController.moveToViewController(at: 0) // required index
}
For Move Specific Tab XLPagerTabStrip in swift 5
override func viewDidAppear(_ animated: Bool) {
if nowFrom == "sendvc"
{
self.moveToViewController(at: 3,animated: false)
}
}
You have to use the following lines:
override func viewDidAppear(_ animated: Bool) {
self.moveToViewController(at: 2)
reloadPagerTabStripView()
}

Perform segue to another Navigation Controller without showing Tab Bar

I have a root Tab Host Controller with two Navigation Controller tab siblings: (1) Nearby Stops and (2) Saved Stops. Each of these has a View Controller respectively.
I would like to perform a segue from one of the sibling View Controllers to another Navigation Controller with Stop Schedule View Controller embedded in it, with the following requirements:
The root Tab Bar should not show at the bottom of this View Controller
I need to pass a Stop object to this View Controller before performing the segue
Storyboard:
Currently, I am performing a segue this way, though the Tab Bar remains on the Stop Schedule View Controller when it shouldn't.
func showStopSchedule(stop: Stop) {
let stopScheduleController = self.storyboard?.instantiateViewControllerWithIdentifier("StopScheduleViewController") as! StopScheduleViewController
stopScheduleController.stop = stop // pass data object
self.navigationController?.pushViewController(stopScheduleController, animated: true)
}
You can simply set the hidden property of your tab bar when the stop schedule view controller is displayed and unhide the tab bar before that view controller disappears
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
self.tabBarController?.tabBar.hidden=true
}
override func viewWillDisappear(animated: Bool) {
super.viewWillDisappear(animated)
self.tabBarController?.tabBar.hidden=false
}
Update: To animate the transition you can use this:
class StopViewController: UIViewController {
var barFrame:CGRect?
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
// self.tabBarController?.tabBar.hidden=true
if let tabBar=self.tabBarController?.tabBar {
self.barFrame=tabBar.frame
UIView.animateWithDuration(0.3, animations: { () -> Void in
let newBarFrame=CGRectMake(self.barFrame!.origin.x, self.view.frame.size.height, self.barFrame!.size.width, self.barFrame!.size.height)
tabBar.frame=newBarFrame
}, completion: { (Bool) -> Void in
tabBar.hidden=true
})
}
}
override func viewWillDisappear(animated: Bool) {
super.viewWillDisappear(animated)
self.tabBarController?.tabBar.hidden=false;
if self.barFrame != nil {
UIView.animateWithDuration(0.3, animations: { () -> Void in
let newBarFrame=CGRectMake(self.barFrame!.origin.x, self.view.frame.size.height-self.barFrame!.size.height, self.view.frame.size.width, self.barFrame!.size.height)
self.tabBarController?.tabBar.frame=newBarFrame
})
}
}
}
You are not using the segue you just defined in your Storyboard. Instead, you are currently reloading your StopScheduleViewController manually, whereas you should only perform the segue you already have defined.
Add an Identifier to each of the Storyboard Segue you want to invoke programmatically,
then load them in this manner:
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
performSegueWithIdentifier("showStopSchedule", sender: self)
}
If you want to only hide the navigationController the below code works.
self.navigationController?.navigationBar.hidden = true

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) {
}
}

How can you reload a ViewController after dismissing a modally presented view controller in Swift?

I have a first tableViewController which opens up a second tableViewcontroller upon clicking a cell. The second view controller is presented modally (Show Detail segue) and is dismissed with:
self.dismissViewControllerAnimated(true, completion: {})
At this point, the second view controller slides away and reveals the first view controller underneath it. I would then like to reload the first view controller. I understand that this may require use of delegate functions, but not sure exactly how to implement it
Swift 5:
You can access the presenting ViewController (presentingViewController) property and use it to reload the table view when the view will disappear.
class: FirstViewController {
var tableView: UITableView
present(SecondViewController(), animated: true, completion: nil)
}
In your second view controller, you can in the viewWillDisappear method, add the following code:
class SecondViewController {
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
if let firstVC = presentingViewController as? FirstViewController {
DispatchQueue.main.async {
firstVC.tableView.reloadData()
}
}
}
}
When you dismiss the SecondViewController, the tableview of the FirstViewController will reload.
I solved it a bit differently since I don't want that dependancy.
And this approach is intended when you present a controller modally, since the presenting controller wont reload when you dismiss the presented.
Anyway solution!
Instead you make a Singleton (mediator)
protocol ModalTransitionListener {
func popoverDismissed()
}
class ModalTransitionMediator {
/* Singleton */
class var instance: ModalTransitionMediator {
struct Static {
static let instance: ModalTransitionMediator = ModalTransitionMediator()
}
return Static.instance
}
private var listener: ModalTransitionListener?
private init() {
}
func setListener(listener: ModalTransitionListener) {
self.listener = listener
}
func sendPopoverDismissed(modelChanged: Bool) {
listener?.popoverDismissed()
}
}
Have you Presenting controller implement the protocol like this:
class PresentingController: ModalTransitionListener {
//other code
func viewDidLoad() {
ModalTransitionMediator.instance.setListener(self)
}
//required delegate func
func popoverDismissed() {
self.navigationController?.dismissViewControllerAnimated(true, completion: nil)
yourTableViev.reloadData() (if you use tableview)
}
}
and finally in your PresentedViewController in your viewDid/WillDisappear func or custom func add:
ModalTransitionMediator.instance.sendPopoverDismissed(true)
You can simply reaload your data in viewDidAppear:, but that might cause the table to be refreshed unnecessarily in some cases.
A more flexible solution is to use protocols as you have correctly guessed.
Let's say the class name of your first tableViewController is Table1VC and the second one is Table2VC. You should define a protocol called Table2Delegate that will contain a single method such as table2WillDismissed.
protocol Table2Delegate {
func table2WillDismissed()
}
Then you should make your Table1VC instance conform to this protocol and reload your table within your implementation of the delegate method.
Of course in order for this to work, you should add a property to Table2VC that will hold the delegate:
weak var del: Table2Delegate?
and set its value to your Table1VC instance.
After you have set your delegate, just add a call to the delegate method right before calling the dismissViewControllerAnimated in your Table2VC instance.
del?.table2WillDismissed()
self.dismissViewControllerAnimated(true, completion: {})
This will give you precise control over when the table will get reloaded.

Resources