Search code examples
iosswiftexplain

Why is UIView removed from heirarchy after presenting view controller?


Ok I just ran into something bizarre. I've got my app controller dependency injecting a view (header) into a view controller. That view controller presents another view controller modally and dependency injects it's own header for the presenting view controller to use. But when its presented the header from the first controller disappears.

The property is still set but it's been removed from the view hierarchy.

I've reproduced this issue in fresh singleview project:

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()

        self.view.backgroundColor = .white

        let button = UIButton(frame: CGRect(x: 0, y: 20, width: 100, height: 50))
        button.setTitle("Click Me!", for: .normal)
        button.addTarget(self, action: #selector(self.segue), for: .touchUpInside)
        button.backgroundColor = .black
        button.setTitleColor(.lightGray, for: .normal)

        self.view.addSubview(button)
    }

    func segue() {
        let view = UIView(frame: CGRect(x: 0, y: 0, width: 200, height: 200))
        view.backgroundColor = .lightGray

        let firstVC = FirstViewController()
        firstVC.sharedView = view

        present(firstVC, animated: false)
    }
}

class FirstViewController: UIViewController {
    var sharedView: UIView!

    override func viewDidLoad() {
        super.viewDidLoad()
        self.view.backgroundColor = .white

        self.view.addSubview(self.sharedView)

        let button = UIButton(frame: CGRect(x: 0, y: 200, width: 100, height: 50))
        button.setTitle("Click Me!", for: .normal)
        button.addTarget(self, action: #selector(self.segue), for: .touchUpInside)
        button.backgroundColor = .black
        button.setTitleColor(.lightGray, for: .normal)

        self.view.addSubview(button)
    }

    func segue() {
        let secondVC = SecondViewController()
        secondVC.sharedView = self.sharedView

        present(secondVC, animated: true)
    }
}

class SecondViewController: UIViewController {
    var sharedView: UIView!

    override func viewDidLoad() {
        super.viewDidLoad()
        self.view.backgroundColor = .white

        self.view.addSubview(self.sharedView)

        let button = UIButton(frame: CGRect(x: 0, y: 200, width: 100, height: 50))
        button.setTitle("Click Me!", for: .normal)
        button.addTarget(self, action: #selector(self.segue), for: .touchUpInside)
        button.backgroundColor = .black
        button.setTitleColor(.lightGray, for: .normal)

        self.view.addSubview(button)
    }

    func segue() {
        self.dismiss(animated: true)
    }
}

Can someone explain what's going on here? Why does the sharedView disappear from the FirstViewController?


Solution

  • At the doc of -addSubview(_:):

    Views can have only one superview. If view already has a superview and that view is not the receiver, this method removes the previous superview before making the receiver its new superview.

    That should explain your issue.

    I'd suggest instead that you create a method that generate a headerView (a new one each time) according to your customization style.

    If you really want to "copy" the view, you can check that answer. Since UIView is not NSCopying Compliant, their trick is to "archive/encode" it since it's NSCoding compliant, copy that archive, and "unarchive/decode" the copy of it.