Does this view controller leak in a “willSet/didSe

2019-07-18 20:26发布

问题:

You have a vc (green) and it has a panel (yellow) "holder"

Say you have ten different view controllers...Prices, Sales, Stock, Trucks, Drivers, Palettes, which you are going to put in the yellow area, one at a time. It will dynamically load each VC from storyboard

 instantiateViewController(withIdentifier: "PricesID") as! Prices

We will hold the current VC one in current. Here's code that will allow you to "swap between" them...

>>NOTE, THIS IS WRONG. DON'T USE THIS CODE<<

One has to do what Sulthan explains below.

var current: UIViewController? = nil {
    willSet {
        // recall that the property name ("current") means the "old" one in willSet
        if (current != nil) {
            current!.willMove(toParentViewController: nil)
            current!.view.removeFromSuperview()
            current!.removeFromParentViewController()
            // "!! point X !!"
        }
    }
    didSet {
        // recall that the property name ("current") means the "new" one in didSet
        if (current != nil) {
            current!.willMove(toParentViewController: self)
            holder.addSubview(current!.view)
            current!.view.bindEdgesToSuperview()
            current!.didMove(toParentViewController: self)
        }
    }
}

>>>>>>>>IMPORTANT!<<<<<<<<<

Also note, if you do something like this, it is ESSENTIAL to get rid of the yellow view controller when the green page is done. Otherwise current will retain it and the green page will never be released:

override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) {
    current = nil
    super.dismiss(animated: flag, completion: completion)
}

Continuing, you'd use the current property like this:

func showPrices() {
    current = s.instantiateViewController(withIdentifier: "PricesID") as! Prices
}
func showSales() {
    current = s.instantiateViewController(withIdentifier: "SalesID") as! Sales
}

But consider this, notice "point X". Normally there you'd be sure to set the view controller you are getting rid of to nil.

blah this, blah that
blah.removeFromParentViewController()
blah = nil

However I (don't think) you can really set current to nil inside the "willSet" code block. And I appreciate it's just about to be set to something (in didSet). But it seems a bit strange. What's missing? Can you even do this sort of thing in a computed property?


Final usable version.....

Using Sulthan's approach, this then works perfectly after considerable testing.

So calling like this

// change yellow area to "Prices"
current = s.instantiateViewController(withIdentifier: "PricesID") as! Prices

// change yellow area to "Stock"
current = s.instantiateViewController(withIdentifier: "StickID") as! Stock

this works well...

var current: UIViewController? = nil { // ESSENTIAL to nil on dismiss
    didSet {
        guard current != oldValue else { return }

        oldValue?.willMove(toParentViewController: nil)
        if (current != nil) {
            addChildViewController(current!)

            holder.addSubview(current!.view)
            current!.view.bindEdgesToSuperview()
        }
        oldValue?.view.removeFromSuperview()

        oldValue?.removeFromParentViewController()
        if (current != nil) {
            current!.didMove(toParentViewController: self)
        }
    }
    // courtesy http://stackoverflow.com/a/41900263/294884
}
override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) {
   // ESSENTIAL to nil on dismiss
    current = nil
    super.dismiss(animated: flag, completion: completion)
}

回答1:

I don't think that using didSet is actually wrong. However, the biggest problem is that you are trying to split the code between willSet and didSet because that's not needed at all. You can always use oldValue in didSet:

var current: UIViewController? = nil {
    didSet {
        guard current != oldValue else {
           return
        }

        oldValue?.willMove(toParentViewController: nil)            
        if let current = current {
            self.addChildViewController(current)
        }

        //... add current.view to the view hierarchy here...
        oldValue?.view.removeFromSuperview()

        oldValue?.removeFromParentViewController()
        current?.didMove(toParentViewController: self)
    }
}

By the way, the order in which the functions are called is important. Therefore I don't advise to split the functionality into remove and add. Otherwise the order of viewDidDisappear and viewDidAppear for both controllers can be surprising.



回答2:

Let's divide the question into two: (1) Is there a "leak"? and (2) Is this a good idea?

First the "leak". Short answer: no. Even if you don't set current to nil, the view controller it holds obviously doesn't "leak"; when the containing view controller goes out of existence, so does the view controller pointed to by current.

The current view controller does, however, live longer than it needs to. For that reason, this seems a silly thing to do. There is no need for a strong reference current to the child view controller, because it is, after all, your childViewControllers[0] (if you do the child view controller "dance" correctly). You are thus merely duplicating, with your property, what the childViewControllers property already does.

So that brings us to the second question: is what you are doing a good idea? No. I see where you're coming from — you'd like to encapsulate the "dance" for child view controllers. But you are doing the dance incorrectly in any case; you're thus subverting the view controller hierarchy. To encapsulate the "dance", I would say you are much better off doing the dance correctly and supplying functions that perform it, along with a computed read-only property that refers to childViewController[0] if it exists.

Here, I assume we will only ever have one child view controller at a time; I think this does much better the thing you are trying to do:

var current : UIViewController? {
    if self.childViewControllers.count > 0 {
        return self.childViewControllers[0]
    }
    return nil
}

func removeChild() {
    if let vc = self.current {
        vc.willMove(toParentViewController: nil)
        vc.view.removeFromSuperview()
        vc.removeFromParentViewController()
    }
}

func createChild(_ vc:UIViewController) {
    self.removeChild() // There Can Be Only One
    self.addChildViewController(vc) // *
    // ... get vc's view into the interface ...
    vc.didMove(toParentViewController: self)
}