13
votes

I have sideViewController with a button and Action, which present new view controller by clicking this button.

class sideViewController: UIViewController {
    @IBOutlet var buttonVC1 : UIButton!
    @IBAction func goToVC1 () {
        var VC1 = self.storyboard.instantiateViewControllerWithIdentifier("ViewController") as ViewController
        presentViewController(VC1, animated:true, completion: nil)
    }
}

I use this in main view controller:

class ViewController: UIViewController {
    var menu : sideViewController!
    override func viewDidLoad() {
        super.viewDidLoad()
        menu = self.storyboard.instantiateViewControllerWithIdentifier("menu") as      sideViewController
        menu.view.frame = CGRect(x: 0, y: 0, width: 160, height: 480)
        view.addSubview(menu.view)
}

when I click this button, the problem is: "Presenting view controllers on detached view controllers is discouraged"

What should I do to fix this?

5
try this in your sideViewController.goToVC1 : self.view.window.rootViewController.presentViewController(VC1, animated:true, completion: nil)Ali Abbas
@AliAB. it works well at the first attempt, but then it crushes (other view controller has this menu too)Yury Alexandrov
A side effect of this warning in iOS 13 is that the detached view controllers do not receive the traitCollectionDidChange notification. This means they always stay in the color theme that was active when the view controller was instantiated.navigator48

5 Answers

9
votes

I just ran into this same warning myself, and realized that I'm getting it because when I was calling

self.presentViewController

I was calling it on a view controller that wasn't attached to the UIWindow through the view hierarchy. You need to change what your doing to delay calling presentViewController until you know the view is on the view stack. This would be done in ViewDidLoad or ViewDidAppear, or if your coming from a background state, waiting until your app is in the active state

3
votes

Use this to make sure you are on the main thread

dispatch_async(dispatch_get_main_queue(), { () -> Void in
        self.presentViewController(VC1, animated: true, completion: nil)
    })
3
votes

Problem

iOS is complaining that some other view(the detached view) which came after the main view is presenting something. It can present it, which it does apparently, but it's discouraged as it's not a good practice to do so.

Solution

Delegate/protocol pattern is suitable to solve this issue. By using this pattern, the action will be triggered inside the SideVC although this trigger will be sent to the MainVC and be performed there.

Therefore, since the action will be triggered by the MainVC, from iOS's perspective, it will all be safe and sound.

Code

SideVC:

protocol SideVCDelegate: class {
  func sideVCGoToVC1()
}

class sideVC: UIViewController {
  weak var delegate: SideVCDelegate?

  @IBOutlet var buttonVC1: UIButton!

  @IBAction func goToVC1 () {
    delegate.sideVCGoToVC1()
  }

MainVC

class MainVC: UIViewController, SideVCDelegate {
  var menu: sideVC!

  override func viewDidLoad() {
    super.viewDidLoad()
    menu = self.storyboard?.instantiateViewControllerWithIdentifier("menu") as sideViewController
    menu.delegate = self
    menu.view.frame = CGRect(x: 0, y: 0, width: 160, height: 480)
    view.addSubview(menu.view)
  }

  // MARK: - SideViewControllerDelegate
  func sideViewControllerGoToVC1() {
    menu.view.removeFromSuperview()
    var VC1 = self.storyboard?.instantiateViewControllerWithIdentifier("ViewController") as ViewController
    presentViewController(VC1, animated:true, completion: nil)
  }
}

Note

Apart from the question you've asked, the below lines seems somewhat vague.

var VC1 = self.storyboard?.instantiateViewControllerWithIdentifier("ViewController") as ViewController
menu.view.frame = CGRect(x: 0, y: 0, width: 160, height: 480)

You're obtaining a view controller from your storyboard which has a frame when you designed it inside Interface Builder but you're changing it afterwards. It's not a good practice to play with the frames of views once they're created.

Maybe you've intended to do something else but most likely, it's a problematic piece of code.

0
votes

Swift 5

In the UIKit view hierarchy, view controllers can either be "attached" or "detached", which I put in quotes because they're never explained in documentation. From what I've observed, attached view controllers are simply view controllers that are directly chained to the key window.

Therefore, the nearest attached view controller would obviously be the root view controller itself, since it's directly owned by the key window. This is why presenting from the root view controller remedies warnings about presenting on detached view controllers.

To present a subsequent view controller (a second one), you must find the next nearest and available attached view controller (I say available because the root view controller is currently occupied presenting the current view controller; it cannot present any more view controllers). If the root is presenting a plain view controller (meaning, not a container view controller like a navigation controller), then the next nearest attached view controller is that view controller. You can present from self without any warnings, since it's directly chained to the root, which is directly chained to the key window. However, if the root presented a container view controller, like a navigation controller, then you could not present from any of its children, because they are not directly chained to the root—the parent/container is. Therefore, you would have to present from the parent/container.

To make this easier, you can subclass UIViewController and add a convenience method for finding the nearest available attached view controller.

class XViewController: UIViewController {
    var rootViewController: UIViewController? {
        return UIApplication.shared.keyWindow?.rootViewController
    }
    
    /* Returns the nearest available attached view controller
       (for objects that seek to present view controllers). */
    var nearestAvailablePresenter: UIViewController? {
        guard let root = rootViewController else {
            return nil
        }
        if root.presentedViewController == nil {
            return root // the root is not presenting anything, use the root
        } else if let parent = parent {
            return parent // the root is currently presenting, find nearest parent
        } else {
            return self // no parent found, present from self
        }
    }
}

Usage

class SomeViewController: XViewController {
    let modal = AnotherViewController()
    nearestAvailablePresenter?.present(modal, animated: true, completion: nil)
}
-1
votes

Here this might help you. I got my error fixed with this

let time = dispatch_time(DISPATCH_TIME_NOW, Int64(0.001 * Double(NSEC_PER_SEC)))

dispatch_after(time, dispatch_get_main_queue(), { () -> Void in
   self.performSegueWithIdentifier("SegueName", sender: self)
})

Good luck..