Pushing View Controller Twice - ios

Within my app I'm having an issue with the following error:
Pushing the same view controller instance more than once is not supported
It's a bug report that's comeback from a few users. We've tried to replicate it but can't (double tapping buttons etc). This is the line we use to open the view controller:
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let editView = storyboard.instantiateViewControllerWithIdentifier("EditViewController") as! EditViewController
editView.passedImage = image
editView.navigationController?.setNavigationBarHidden(false, animated: false)
if !(self.navigationController!.topViewController! is EditViewController) {
self.navigationController?.pushViewController(editView, animated: true)
}
Anybody have any ideas? I've done a bit of research and most answers on Stack we've covered so are at a bit of a loss for how to investigate.

Try this to avoid pushing the same VC twice:
if !(self.navigationController!.viewControllers.contains(editView)){
self.navigationController?.pushViewController(editView, animated:true)
}

As the pushViewController is asynchronous since iOS7, if you tap the button that push view controller too fast, it will be pushed twice.
I have met such issue, the only way I tried is to set a flag when push is invoked (i.e - navigationController:willShowViewController:animated:) and unset the flag when the delegate of UINavigationController is called - navigationController:didShowViewController:animated:
It's ugly, but it can avoid the twice-push issue.

In the function that does the push:
guard navigationController?.topViewController == self else { return }

The completion block of CATransaction to the rescue :)
The animation of pushViewController(:animated:) is actually pushed onto the CATransaction stack, which is created by each iteration of the run loop. So the completion block of the CATransaction will be called once the push animation is finished.
We use a boolean variable isPushing to make sure new view controller can't be pushed while already pushing one.
class MyNavigationController: UINavigationController {
var isPushing = false
override func pushViewController(_ viewController: UIViewController, animated: Bool) {
if !isPushing {
isPushing = true
CATransaction.begin()
CATransaction.setCompletionBlock {
self.isPushing = false
}
super.pushViewController(viewController, animated: animated)
CATransaction.commit()
}
}
}

Related

Correct way of presenting a ViewController B from ViewController A before A is visible

My iOS App starts with UIViewController A which is embedded as first element in a UINavigationController. When the app is started or when returning to it after some time in background I would like to show a password prompt. In this case UIViewController A should present UIViewController B which shows the password prompt.
The user should immediately see UIViewController B, not A and then B sliding in, etc. Thus, I have presented UIViewController B in viewWillAppear in UIViewController A:
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
if needPassword {
let passwordVC = PasswordViewController()
passwordVC.modalPresentationStyle = .fullScreen
present(passwordVC, animated: false, completion: nil)
}
}
This works fine, but an error message is logged:
Unbalanced calls to begin/end appearance transitions for <UINavigationController: 0x7fe9af01c200>.
It is obvious that presenting UIViewController B from UIViewController A before it became visible causes this problem. Moving from viewWillAppear to viewDidAppear would solve the error message. However, than the user would first see A then B...
Is it even possible to overlay a ViewControler A with ViewController B without A becoming visible first?
I know that there might be other solutions like adding the view of the password ViewController manually to the view hierachy, etc. However, I would prefer a clean way where A is in complete control. Is this possible?
Or is it save to simple ignore the warning?
It sounds a bit tricky, might do the job though.
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
if needPassword {
self.view.alpha = 0
// Maybe (or not?)
self.navigationController?.view.backgroundColor = .white
}
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if needPassword {
let passwordVC = PasswordViewController()
passwordVC.modalPresentationStyle = .fullScreen
present(passwordVC, animated: false, completion: { [weak self] in
self?.view.alpha = 1
})
}
}

popToRootViewController not returning popped UIViewControllers

I found some strange behaviour with popToRootViewController method.
According to it's declaration it should return popped viewControllers, but it's not if you call it during the pushViewController
class HomeTabNavigationController: UINavigationController {
override func popToRootViewController(animated: Bool) -> [UIViewController]? {
let viewControllers = super.popToRootViewController(animated: animated)
if let vcs = viewControllers, vcs.count > 0 {
print("POPPED SOMETHING")
}
return viewControllers
}
}
This is working as expected, if animation did finish before I call this.
But if I call this method during the animation still happening, it will return nil but it still pops to root viewcontroller after a slight delay.
I prepared a quick demo of that problem:
https://github.com/grzegorzkrukowski/UINavigationControllerBug
Of course I have different use case in my real app, but problem is the same.
Am I missing something or it's just a bug ?

Swift 3: popToViewController not working

In my app I have three table view controllers and then potentially many UIViewControllers each of which has to lead back to the first table view controller if the user presses back at any point. I don't want the user to have to back through potentially hundreds of pages. This is what I amusing to determine if the user pressed the back button and it works the message is printed
override func viewWillDisappear(_ animated: Bool) {
if !movingForward {
print("moving back")
let startvc = self.storyboard!.instantiateViewController(withIdentifier: "FirstTableViewController")
_ = self.navigationController!.popToViewController(startvc, animated: true)
}
}
I have searched and none of the solutions have worked so far.
popToViewController not work in a way you are trying you are passing a complete new reference of FirstTableViewController instead of the one that is in the navigation stack. So you need to loop through the navigationController?.viewControllers and find the FirstTableViewController and then call popToViewController with that instance of FirstTableViewController.
for vc in (self.navigationController?.viewControllers ?? []) {
if vc is FirstTableViewController {
_ = self.navigationController?.popToViewController(vc, animated: true)
break
}
}
If you want to move to First Screen then you probably looking for popToRootViewController instead of popToViewController.
_ = self.navigationController?.popToRootViewController(animated: true)
Try this :
let allViewController: [UIViewController] = self.navigationController!.viewControllers as [UIViewController];
for aviewcontroller : UIViewController in allViewController
{
if aviewcontroller .isKindOfClass(YourDestinationViewControllerName)// change with your class
{
self.navigationController?.popToViewController(aviewcontroller, animated: true)
}
}
If you are in a callback, particularly an async network callback, you may not be on the main thread. If that's you're problem, the solution is:
DispatchQueue.main.async {
self.navigationController?.popToViewController(startvc, animated: true)
}
The system call viewWillDisappear() is always called on the main thread.

Trigger events when ViewController covered by a presented ViewController

I would like to process code when a ViewController is no longer visible due to presenting a new ViewController.
I cannot use ViewWillDisappear etc since the controller is not technically ever dismissed from the stack - you just can't see it.
What process can I use so that code runs when the controller is no longer visible (i.e. topmost) and when it becomes visible again?
EDIT:
Seems some confusion here - not sure why.
I have a viewcontroller.
I use the following code to present another controller
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let navController = storyboard.instantiateViewControllerWithIdentifier("NavController") as! UINavigationController
let thisController = navController.viewControllers[0] as! MyController
self.presentViewController(navController, animated: true, completion: nil)
This controller does not trigger a viewWillDisappear on the previous controller since the previous view is not removed - just hidden.
I need to process code when this view is hidden (i.e. not visible) and, more importantly, process code when it becomes visible again.
When presenting a UIViewController if the presentation style has been set to UIModalPresentationOverCurrentContext it doesn't call the viewWillDisappear and related methods as the view never disappears or gets hidden.
A simple test to check if thats the case would be to set the NavController that you are using to have a clear background color. If you do this and present the NavController and you can still view the first UIViewController below your NavController content. Then you are using UIModalPresentationOverCurrentContext and that is why the viewDidDisappear isn't called.
Have a look at the answer referenced by Serghei Catraniuc (https://stackoverflow.com/a/30787112/4539192).
EDIT: This is in Swift 3, you can adjust your method accordingly if you're using an older version of Swift
If you won't be able to figure out why viewDidAppear and viewDidDisappear are not called, here's a workaround
protocol MyControllerDelegate {
func myControllerWillDismiss()
}
class MyController: UIViewController {
var delegate: MyControllerDelegate?
// your controller logic here
func dismiss() { // call this method when you want to dismiss your view controller
// inform delegate on dismiss that you're about to dismiss
delegate?.myControllerWillDismiss()
dismiss(animated: true, completion: nil)
}
}
class PresentingController: UIViewController, MyControllerDelegate {
func functionInWhichYouPresentMyController() {
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let navController = storyboard.instantiateViewController(withIdentifier: "NavController") as! UINavigationController
let thisController = navController.viewControllers[0] as! MyController
thisController.delegate = self // assign self as delegate
present(navController, animated: true, completion: {
// place your code that you want executed when it disappears here
})
}
func myControllerWillDismiss() {
// this method will be called now when MyController will dismiss
// place your code that you want executed when it re-appears here
}
}
Firstly, thanks to Serghei for his time in helping work through this.
To clarify, both my potential presented controllers were set to Full Screen presentation style in the storyboard, however one was being set to Custom via a piece of pasted code dealing with the presentation. I can't find the error with the other.
However, if I force a presentation style of Full Screen as part of the presenting process then all is ok.
Hopefully my frustrating afternoon can help to save someone else's - always try to understand the implications and processes involved in pasted snippets.

Attempt to present ViewController whose view is not in the windows hierarchy

I meet a strange problem: I made 2 view controllers for wich I can switch the view with code:
var currentViewController:UIViewController=UIApplication.shared.keyWindow!.rootViewController!
func showController()
{
let ViewControllernew1 = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "viewController2")
currentViewController.present(ViewControllernew1, animated: true, completion: nil)
}
My app open correctly to the first view controller, then, when I click on the button created on a sprite kit scene, I can switch the view to my new view controller successfully (I get my second scene successfully showed) but then, I can not change anymore my view controller after this switch. If I click again on the button, I get this message:
Attempt to present on Test_Vuforia.GameViewController: 0x12f549610 whose view is not in the window hierarchy!
Do you know what is the problem ? I understand I'm in the root position so that I can not change anymore my view controller after having switched it, but how to change that ?
Thanks !
Edit:
My code is used inside a SKScene and not from a UIVewController and I get this error when I use the suffix self. : Value of type View (SKScene) has no member 'present'.
I'm creating an augmented reality game with Vuforia and I need to switch AR view with SKScene.
Issue
Current viewController is not the rootViewController from UIApplication. So you should find the current viewController which is visible and then present it from there.
Solution
Simply find the topViewController on your UIApplication Stack, and from there present your controller.
let newViewController = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "viewController2")
UIApplication.topViewController()?.present(newViewController, animated: true, completion: nil)
This extension of UIApplication comes in handy for your case
extension UIApplication {
class func topViewController(base: UIViewController? = UIApplication.shared.keyWindow?.rootViewController) -> UIViewController? {
if let nav = base as? UINavigationController {
return topViewController(base: nav.visibleViewController)
}
if let tab = base as? UITabBarController {
if let selected = tab.selectedViewController {
return topViewController(base: selected)
}
}
if let presented = base?.presentedViewController {
return topViewController(base: presented)
}
return base
}
}
References: Gist
Calling function in viewDidAppear helps in my case. Solution for Swift 3:
In your Main Controller:
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
showTutorialModally()
}
func showTutorialModally() {
let tutorialViewController = TutorialViewController()
tutorialViewController.modalPresentationStyle = .overCurrentContext
present(tutorialViewController, animated: true, completion: nil)
}
In your Tutorial Controller:
view.backgroundColor = UIColor.clear
view.isOpaque = false
Use the extension below to retrieve the next available controller in the stack.
Swift 3
extension UIResponder {
func next<T: UIResponder>(_ type: T.Type) -> T? {
return next as? T ?? next?.next(type)
}
}
Swift 2.3
extension UIResponder {
func nextResponder<T: UIResponder>(_ type: T.Type) -> T? {
return nextResponder() as? T ?? nextResponder()?.nextResponder(type)
}
}
Inside your SKScene, view?.next(UIViewController.self) gives you the next available UIViewController in the hierarchy.
Add this extension to a group in your project called Categories, if this group does not exist already create it, then create a new file called UIResponder+NextOfType.swift and paste the extension.
Xcode error significance for roughly: this view is not in the Window of the view hierarchy.
What I don't think the above answer questions, but maybe you might have wondered why this would happen.
But I find that you are the reasons for this problem is likely to be in the ViewController life cycle at ViewDidLoading switch view Code execution inside.
Reason is probably that, when the ViewController implementation allco init during initialization, it will be executed asynchronously viewWillLoad - > viewDidLoad... -- -- -- -- > viewDidApper. Then may be in code execution to the viewDidLoad. The ViewController may not assign values to the Window. The rootViewController. So we directly use [self presentViewController:] will appear this error.
It is recommended that you move the code of the switch to ViewDidApper.
I hope it will help you.
Probably your rootViewController is not the current ViewController. Either you presented or pushed a new UIViewController on top of it.
The viewController's view is not in the
window's view hierarchy at the point that it has been loaded (when
the viewDidLoad message is sent), but it is in the window
hierarchy after it has been presented (when the viewDidAppear:
message is sent). if you calling showController method from
viewDidLoad just call it from viewDidAppear method
Do something like:
let vc: UIViewController = (self.storyboard?.instantiateViewControllerWithIdentifier("viewController2"))!
self.presentViewController(vc, animated: true, completion: nil)
// OR
self.navigationController?.pushViewController(vc, animated: true)
Use like this
let vc = self.view?.window?.rootViewController
func showController()
{
let ViewControllernew1 = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "viewController2")
vc.present(ViewControllernew1, animated: true, completion: nil)
}
Maybe the issue is with the currentViewController.

Resources