3

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

enter image description here

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)
}
Fattie
  • 27,874
  • 70
  • 431
  • 719
  • 1
    Well the fact that you're using setter observers means that the property is being set to something else – be it `nil` or some other view controller. So what difference would being able to assign `nil` in the intermediate step between `willSet` and `didSet` make? Also, unless you have a retain cycle, you don't *need* to explicitly set a reference to `nil` – it's retain count will be decremented when it goes out of scope, and if no strong references are left to it, it will be deallocated. – Hamish Jan 27 '17 at 16:39
  • @Hamish ..."So what difference..." well, it's always good to check with smarter people. :) – Fattie Jan 27 '17 at 17:37
  • @Hamish - regarding your second point. Hmm, current *is* a strong reference to it. Setting aside the property ... if at some point in a class you go "current = instantiateViewController ..." you do indeed eventually have to nil that. Merely removeFromParentViewController()'ing does not release it, I believe???? – Fattie Jan 27 '17 at 17:39
  • 1
    "*Merely removeFromParentViewController()'ing does not release it*" – correct, it will still be kept alive by your `current` property. The point I was making is that not setting it to `nil` won't leak it, unless you have a retain cycle. As soon as `current` goes out of scope (as it's a property, when the view controller instance that it's a member of is deallocated), the instance that it pointed to will have its reference count decremented, and if 0, will be deallocated :) – Hamish Jan 27 '17 at 18:18
  • heh .. I *think* I know what you mean! :) – Fattie Jan 27 '17 at 18:29

2 Answers2

3

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)
}
matt
  • 515,959
  • 87
  • 875
  • 1,141
  • facepalm! I totally forgot about `childViewControllers` :O thanks, man – Fattie Jan 27 '17 at 17:14
  • Right, but I don't think that makes your question silly. Encapsulating the "dance" is a _great_ idea. Otherwise I wouldn't have taken the time to write that code! – matt Jan 27 '17 at 17:15
  • Right, this will take some thought. Say one has a screen. And it has say five child VC. then there is one area ("displayX") where you want to cycle between say ten different displays (prices/sales etc as in my example.) do you'd have to "find" the one in displayX at that moment. It might be say #3 in the `childViewControllers` array. when you `removeFromParentViewController` item #3 in the `childViewControllers` array .. does iOS .. know to remove? it from the array? or what? this might take some investigation! :O thanks again! – Fattie Jan 27 '17 at 17:20
  • I don't see how that changes anything. If there is just _one_ area, you only need _one_ child view controller at a time. You should _not_ have five child VCs if only one is showing. Think about how a UIPageViewController works. (In fact, from what you have said so far, I would think you should just _use_ a UIPageViewController!) – matt Jan 27 '17 at 17:27
  • All I mean is: imagine you happen to have (say) four small container views (imagine some sort of custom dial displays). So that's fine. So four slots in `childViewControllers` are used up and have nothing to do with this QA. Then on the same screen, you have the thing in this QA. So, it's a display (picture it being, oh, 1/4 of the screen somewhere) which shows Prices/Sales/Stock/Orders. Call it ProductArea. So, your call .current would in fact have to find the vc in ProductArea (it could be any random index 0 through 4)....... – Fattie Jan 27 '17 at 17:30
  • OK but I think you have now strayed into a completely different question. You didn't say any of that in the question you asked. I think my answer does set your feet on the right road to answering the question you asked. Being a custom parent view controller is way cool but you have to work out a way of doing it that accords with how you will use the children — i.e., what it _means_ to be a child of yours. – matt Jan 27 '17 at 17:33
  • by all means, I totally agree it is a different question: I was just rambling. thanks again !! – Fattie Jan 27 '17 at 17:34
  • I guess the fact is: **it will not leak**. – Fattie Jan 27 '17 at 17:35
  • "current" gets set after the end of the willSet, but very soon afterwards in the didSet. Awesome. – Fattie Jan 27 '17 at 17:35
  • I don't think that using `didSet` would actually be wrong but instead of putting half of the code to `willSet` and the other half to `didSet`, using only `didSet` & `oldValue` would be much much simpler. – Sulthan Jan 27 '17 at 17:55
  • @JoeBlow The problem for me is that you use view controllers in a way totally unfamiliar to me. You say "there will be, as is typical, three or four other container'ed VCs on screen" but that is not at all typical in my world. We've discussed before my opinion of this: you use view controllers as a form of "dumpster diving" to pull views out of nibs, in ways that I regard as unnecessary. — As for the original question, I still think your yellow view could be a UIPageViewController's view and the problem would be neatly solved. – matt Jan 27 '17 at 17:59
  • Hi @matt, right I totally understand what you're saying. Again, it goes beyond the question here anyways - thanks again !! – Fattie Jan 27 '17 at 18:07
  • say @matt. I was thinking a lot about the thrust of what you said. Consider the totally ordinary situation where you have one ordinary container view (say, Clock). You "connect" to Clock, in prepare#forsegue .. `if (segue.identifier == "ClockID") { clock = (segue.destination as! Clock) }`. I'd guess, that you do that yourself and you think it is conventional idiom? However!!! Note that even that pattern exhibits the problem you raise Matt: why would you bother doing that to get "clock"? After all, *it's right there in childViewControllers!* Know what I mean? – Fattie Jan 28 '17 at 17:58
  • 1
    (1) Under _ordinary_ circumstances, where you push or present a view controller, the destination view controller is _not_ your child. (2) What does it matter whether there is more than one way to refer to something? Your "eldest male sibling" is also your "brother". – matt Jan 28 '17 at 18:11
  • say @matt, your point (1) there is so critical in iOS engineering! you're damned right. when you push or present, it's so tempting to assume it will become your child view controller. but more frequently it's the whole window or something. (If I'm not mistaken it *can* become your child view controller, but doesn't necessarily.) it's a priceless insight thanks – Fattie Feb 04 '17 at 12:44
2

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.

Sulthan
  • 128,090
  • 22
  • 218
  • 270
  • @JoeBlow Of course, if you are using this, you cannot have animations. The whole trick with animating the transition is to put the view changes into animations block and the last two lines into the completion block. – Sulthan Jan 27 '17 at 18:12
  • by all means; this is a no-anime situation! – Fattie Jan 27 '17 at 18:30
  • hi @Sulthan ! **What you say is one million percent correct**. The code in my question does not work. You have to interleave the calls as you say. Sent a small bounty to say thanks – Fattie Feb 04 '17 at 12:46
  • @JoeBlow Thanks a lot. – Sulthan Feb 04 '17 at 12:48