Attempt to present VC2 on VC1 which is already presenting - ios

Presently at a lost for why I am receiving the message of Attempt to present VC2 on VC1 which is already presenting when the VC1 does both the presenting and dismissing of the VC2. There are a few other questions and a lot of examples and I've not been able to resolve it from looking through them.
VC2 is called RoadwaysViewController and is opened as a modal. The only catch I see is that VC2 is used by 2 other VC. Things work great for awhile and then the problem arises when I've moved back and forth from VC to VC after awhile and in each I've used VC2. VC2 was intended to be used in multiple places since the data is the same for all.
Presenting and dismissing are both done from VC1.
class level variable
var roadwaysViewController = RoadwaysViewController()
Here is how it is presented
func roadwayTapped(){
roadwaysViewController = storyboard?.instantiateViewController(withIdentifier: "RoadwayModal") as! RoadwaysViewController
roadwaysViewController.delegate = self
self.present(self.roadwaysViewController, animated: true)
}
Here is how it is dismissed on VC1 the callback from the delegate.
func sendValue(value: Int, name: String) {
roadwaysViewController.dismiss(animated: true, completion: {
if value == 999 {
return
}
self.groupId = value
self.roadName.text = name
self.setupTableData()
})
}
I've tried checking to see if VC2 is presenting and then did dismiss. Tried this in the function that does the presenting, viewWillAppear and viewDidAppear.
I understand you can't have 2 VC presented at the same time but why it isn't getting properly dismissed I'm struggling to understand why. I don't believe I want to dismiss the parent and then show the child since the child is a modal.
Any help, ideas or suggestions is appreciated.

Related

UINavigationController popToViewController not popping

Been working with UIKit for years now. It's amazing how issues like this seem to pop-up out of the blue.
I have a simple navigation setup:
UINavigationController
HomeViewController [push]
DetailViewController [push]
ModalViewController [modal]
A root navigation controller with 2 children pushed onto the stack. Then a modal presented from the root nav controller.
For some reason, the following snippet of code isn't working as expected:
extension UINavigationController {
func popToViewController(_ vc: UIViewController, animated: Bool, completion: #escaping ([UIViewController]?)->()) {
let popped = popToViewController(viewController, animated: animated)
if let coordinator = self.transitionCoordinator {
coordinator.animate(alongsideTransition: nil) { _ in
completion(popped)
}
}
else {
completion(popped)
}
}
}
using the extension:
navigationController.popToViewController(
homeViewController,
animated: true
)
No errors, warnings or crashes occur. UI is still fully responsive. But the DetailViewController in the stack is never popped. Inspecting the extension's popped variable, results in an empty array - which makes sense as the DetailViewController is clearly not removed from the stack.
What could prevent a navigation controller from popping a valid vc off of it's stack?
Things I've checked:
homeViewController is in the stack already, and I'm asking it to pop to the same instance. i.e. navigationController.viewControllers.contains(homeViewController) == true
I'm on the main thread. i.e. Thread.isMainThread == true
navigationController.viewControllers returns the same array before & after calling popToViewController(_vc:animated:)
Manually figuring out what vc's need to be popped, and calling setViewControllers(_ vcs:animated:) with the vcs I want to keep (in this case, just the HomeViewController instance). This still has the same issue.
I want to say this has something to do with popping view controllers off the stack from behind a modal presentation. But, as far as I know this is an okay thing to do. Plus, I've done it before and have had no issues in the past.

How to 'pop' a navigation controller and all of its view controllers

I have inherited a navigation controller issue in an existing app that I'm trying to solve cleanly.
This app has multiple storyboards and multiple UINavigationControllers. At one point in the app, a series of view controllers is presented modally, using a separate storyboard and a separate nav controller. When the modal process is complete, the navigation hierarchy looks something like this:
NavController1 -> VC1 ['Present Modally' segue] -> NavController2 -> VC2 -> VC3 -> VC4
When the user completes the modal activity in VC4, dismiss() is called programmatically on VC4 and the user can then navigate back to VC1 using the back button.
However, what we really need to do is to 'pop off' all of the modally presented set of view controllers (and their nav controller) when the user finishes the modal activity. The problem is that from VC3 or VC4 I can't call popToRootViewController(). I also can't traverse down the VC stack to find VC1, since the current Nav controller doesn't manage it.
A couple solutions come to mind:
1) use the notification manager and have VC1 listen for the message to pop everything off back to itself
2) pass a reference to VC1 as a delegate all the way up the chain so that VC3 or 4 can have it pop everything off
Both of these solutions follow the general maxim that the presenting VC should be the one that dismisses, but neither are what I would consider clean.
I would welcome any thoughts or alternative solutions.
Assuming that these are the way the view controllers were laid out:
NavController1 --['Root View Controller' segue]--> VC1 --['Present Modally' segue]--
--> NavController2 --['Root View Controller' segue]--> VC2 --['Push' segue]--> VC3 --['Push' segue]--> VC4
You should be able to go back to VC1 by dismissing either VC2, VC3 or VC4.
// example for vc4
vc4.navigationController?.dismiss(animated: true, completion: nil)
However, if each of the viewControllers were presented modally, you should be able to traverse through the presentingViewController to reach VC1.
var currentVC: UIViewController? = self
var presentingVC: UIViewController? = currentVC?.presentingViewController
while presentingVC != nil && !(presentingVC is VC1) {
currentVC?.dismiss(animated: true, completion: nil)
currentVC = presentingVC
presentingVC = currentVC?.presentingViewController
}
Hope that helps.
When popping, you may kick out the viewControllers from your navigation Controller, would solve your problem
extension UINavigationController {
public func removeViewController(classes : [String]) {
var vcs = [UIViewControllers]()
for viewController in self.viewControllers {
let name = viewController.className
if !classes.contains(name) {
vcs.append(viewController)
}
}
if classes.count < vcs.count {
self.viewControllers = vcs
}
}
}
now think you have 4 viewControllers , A, B, C, D, you want to remove B and C and Move Back To A
In D's View Controller
override func viewDidLoad() {
super.viewDidLoad()
//your works
let viewControllersToRemove = [String(describing: type(of:B)), String(describing: type(of:C))]
navigationController.removeViewControoler(classes : viewControllersToRemove)
}

How do I delegate to view controller more than 1 view controller up the stack?

Scenario: I have a view controller (vc1) that presents another view controller (vc2). And then vc2 presents vc3. How can I set vc1 as a delegate for vc3?
Situation: While in vc3, I intend to pop back to vc1 and have it execute some code. But, since the vc3 instance is created in vc2, vc1 has no direct link to vc3 from vc1.
Here is a simplified picture of what I'm trying to achieve:
vc1
let vc2Instance = vc2()
navigationController?.pushViewController(vc2Instance)
class vc1: UIViewController, tellVC1ToDoSomethingDelegate {
func vc1DoSomething(withThis: String) {
// for instance: present another VC not currently in stack
let randomVC = RandomVC()
}
}
vc2
let vc3Instance = vc3()
navigationController?.pushViewController(vc3Instance)
vc3
protocol tellVc1ToDoSomethingDelegate() {
func vc1DoSomething(withThis: String)
}
class vc3: UIViewController {
weak var vc1Delegate: tellVC1ToDoSomethingDelegate?
func pushRandomVCWithString(myString: String) {
// code to dismiss view controllers up to vc1 in stack
if let vcStack = self.navigationController?.viewControllers{
self.navigationController?.popToViewController(vcStack[vcStack.count - 2], animated: true)
vc1Delegate.vc1DoSomething(withThis: myString)
}
}
Here's my issue:
If I was marking vc1 as a delegate for vc2 (Just 1 VC up the stack), I'll simply type
let vc2Instance = vc2()
vc2Instance.vc1Delegate = self
How do I access self (of vc1) when no instance of vc3 exists in vc1? Is chaining delegates from vc3 to vc2 then to vc1 the only way out? I imagine this will be ugly when there are several vc's in-between.
There are two way to do this, one is delegation and another one is Notification.
Delegation:
As you asked, you are trying this one. Just:
create a delegate variable in vc2 and vc3 both.
while creating the object of vc2 in vc1 pass the self in the delegate variable of vc2.
while creating the object of vc3 in vc2 pass the self.delegate in the delegate variable of vc3.
Now as you call the method in vc3 like self.delegate?.anyMethod() will get the call in both vc2 and vc3.
Notification:
As of the notification you can broadcast a notification of custom type with some info for vc3. and can add observer to observe the notification in either in vc1 or any other vc, you will receive the call with passed data.
Learn more about Notification here
As my personal suggestion first one is better in your case.
In many ways you can achieve this. Some of them listed below.
If VC1 is your root VC for the navigation stack you can use the
below API in VC3
popToRootViewController(animated:)
Other way you can get the viewControllers array property from the
navigationController and get the VC1 from the array and use the
below API in VC3
popToViewController(_:animated:)
Hope this help. Please comment any doubt.

Pass data to VC that is not the previous VC

When I go from VC1 to VC2, if VC2 gets dismissed, I could easily pass data back to VC1 by setting protocal in VC2 and have VC1 conform to it. I want to do something similar, however, with the following difference
When I go from VC1-> NavVC->VC2-> VC3. When VC3 gets dismissed, VC1 is shown. I want to be able to pass data back to VC1 and initiate some function in VC1. For example, I have an image to upload in VC3. As soon as VC3 gets dismissed, I am hoping to have a function in VC1 such as the following function where the image was the data from VC3
func uploadInitiate(image: UIImage) {}
Relationship of the three VC
VC1 is normal VC. It presents VC2 via
let navController = UINavigationController.init(rootViewController: VC2)
self.navigationController?.presentViewController(navController, animated: true, completion: nil)
VC2 is a custom FusumaCamera photo picker from cocoapods.
Once image is selected, I go to VC3 with the selected Image via
self.navigationController!.pushViewController(postVC, animated: true)
At VC3, I allow the user to interact with the image and make comments and press a button to upload to the server. I thought it would be nice to dismiss the VC straight away after button press and allow VC1 to initiate the upload with the data the came from VC3 (That way I can have a progress bar under navigation bar or display any warnings there)
You have multiple patterns that you can apply in similar situations. i will go with the delegation (recomended) example. Imagine you have VC1->
that presents modally a UINavigationController as a root controller and from there VC2 pushes VC3 to the stack. You have to define multiple protocols that will be called on the way you dismiss the VC3
protocol DismissVCDelegate : class {
func dismissViewAndStartUpload(shouldUpload: Bool)
}
Then store the property:
weak var delegate: DismissVCDelegate!
Set the delegate when to the VC3 when you push it from VC2 and conform the the protocol you defined in VC2.
Apply the same pattern all the way back to VC1 where you have passed multiple times the protocol back and you can start you upload task and you should dismiss the modally presented navigation controller like that:
func dismissViewAndStartUpload(shouldUpload: Bool) {
self.presentedViewController.dismissViewControllerAnimated(true, completion: nil)}

Remove current ViewController after instantiate and presented the next VC

In iOS project, XCode, I have two VCs, VC1 and VC2. What I want to do is in VC1, have a button to go to VC2 using
let VC2 = self.storyboard?.instantiateViewControllerWithIdentifier("VC2") as! VC2
self.presentViewController(loginVC, animated: true)
After VC2 appears, I will never need to go back to VC1 so I want to remove it completely as VC1 has some downloading function that may still run in background. So I want to remove VC1. I have tried the following:
let VC2 = self.storyboard?.instantiateViewControllerWithIdentifier("VC2") as! VC2
self.presentViewController(loginVC, animated: true) {
self.removeFromParentViewController()
}
However, that does not seem to do the job as I still see the background download in progress. Is there a simple way of doing it? I do not really want to include navigation controller in this case.

Resources