33

In my mind, these situations are all parallel:

  • My view controller presented another view controller fullscreen, which has now been dismissed

  • My view controller presented another view controller not fullscreen, which has now been dismissed

  • My view controller presented a popover, which has now been dismissed

  • My view controller pushed another view controller, which has now been popped

In every case, my view controller ceased to be the "frontmost" view controller, and then became "frontmost" again. I find it curious that iOS has no single blanket "became frontmost" event sent to my view controller that covers all these situations.

I think I can cover each of those cases individually, and I think those are all the cases I need to cover, but the resulting code is confusing and scattered:

  • viewDidAppear detects popping of a pushed view controller and dismissal of a fullscreen presented view controller

  • popover delegate message detect dismissal of a popover

  • not sure what detects dismissal of a nonfullscreen presented view controller

How do people handle this coherently and elegantly?

matt
  • 515,959
  • 87
  • 875
  • 1,141
  • `became frontmost` – I have very consistent results with `UIViewController`'s `becomeFirstResponder` and `resignFirstResponder`. See [here](https://developer.apple.com/documentation/uikit/uiresponder). It's not working for child view controllers, though. – bteapot Mar 30 '20 at 16:27
  • @bteapot I sure never thought of that, I'll look into it, thanks! – matt Mar 30 '20 at 16:27
  • Good question. I've been struggling with this for a while (keep punting on it), hoping to figure out a way to inject a proxy responder object between controllers (swizzling?), to then detect these transitions without spilling code into the "otherwise context-free" controllers. – Chris Conover Jul 01 '20 at 19:35
  • I played around a bit with swizzling `becomeFirstResponder` — and it seems to not be called on the new frontmost when a presented view controller is dismissed. I don't think that's super surprising since it could trigger UI that might be disruptive — but it means it won't work for this purpose. – adamz Jan 08 '22 at 14:44

2 Answers2

15

What the cases have in common is not the appearance of the original view controller but the disappearance of the presented/pushed view controller. Therefore, one simple and clear solution seems to be a protocol-and-delegate architecture. Declare a pair of protocols, as follows:

protocol Home : class {
    func comingHome()
}
protocol Away : class {
    var home : Home? {get set}
}
extension Away where Self : UIViewController {
    func notifyComingHome() {
        if self.isBeingDismissed || self.isMovingFromParent {
            self.home?.comingHome()
        }
    }
}
  • The home view controller must adopt Home, and must set each view controller's home to self when it presents or pushes it.

  • The presented or pushed view controllers must adopt Away, and must implement viewWillDisappear as follows:

    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        self.notifyComingHome()
    }
    

This works for the four cases listed in the question. It's a pity, though, that Cocoa Touch doesn't do this for you automatically.


EDIT This approach has become even more important in my apps now that iOS 13 has forced nonfullscreen presented view controllers upon us. Also, I have subclassed UIAlertController so that it conforms to Away.

matt
  • 515,959
  • 87
  • 875
  • 1,141
  • Accepting my own answer for now, but if there’s a better way I’d like to hear about it. – matt Feb 11 '19 at 12:41
  • This approach doesn't work when you presenting a view controller which contains another view controller. Both of those VC return `isBeingDismissed` and `isMovingFromParent` false when dismissing – Andrey Gordeev Jan 03 '20 at 05:16
  • 2
    Also, `viewWillDisappear` called even if you just start dragging the modal view controller down on iOS 13. But starting dragging the VC down doesn't mean it's going to be dismissed. – Andrey Gordeev Jan 03 '20 at 05:18
  • 4
    @AndreyGordeev Fine, give a better answer. I need one. – matt Jan 03 '20 at 05:22
  • 1
    @AndreyGordeev @matt Leaving this here, in case anyone needs it: I implemented `self.home?.comingHome()` in `viewDidDisappear` instead of `viewWillDisappear` and it calls back only when the view is actually dismissed (i.e. after dragging) – vincent Feb 28 '21 at 16:40
2

One solution could be to take the Coordinator approach like in a MVVM-C style architecture. You never directly change view hierarchy in a VC but always call into the Coordinator to do it for you. coordinator.showDetails(...)

Additionally you define a viewDidBecomeForemost method in your VCs that the coordinator can invoke when returning to a VC.

NSRover
  • 932
  • 1
  • 12
  • 29