"Nice" way to exit app when Menu button is pressed?

I'm working on an app that presents a "Sign In" view controller from a tab bar controller. The user has to sign in before doing anything. Normally the user complets the sign in process and that sign in view controller is dismissed and the user can enjoy all the goodness of the things on the tab bar controller.


Note: It's not easy, so far, for us to revise our view hierarchy and we'd really like to keep it.


The problem is that if the user presses Menu on the remote control, while on the Sign In screen, they arrive at the tab bar's contents but haven't signed in yet. That's no good. We need the user to complete sign in before seeing that stuff. It's easy for us to add a gesture recognizer to disallow use of the Menu button on the Sign In screen. However, it feels like the Menu button should exit the app at this point because there's nothing else the user can do unless they sign in. So if the user presses Menu from the Sign In screen we'd like to exit the app, but need to that with our own code and can't rely on the system handling it since this view controller isn't the root one.

As far as I know there's no approved way to tell our tvOS app to exit gracefully or to pass that menu button press on up to the next view controller. We can use "exit()" but of course that's like a crash and the transition out of the app looks weird.


Key questions:


  1. Is there any way to tell a tvOS app to exit so that we can handle the Menu button in the way a user will expect in cases like this where our view hierarchy doesn't exactly match how the Menu button works?
  2. Besides redoing our view hierarchy... is there some other solution that you can suggest?

Replies

Are you responding to pressesBegan/Changed/Ended? If you do, I think you can just pass that event to 'super' and it will exit the app.


[super pressesBegan:presses withEvent:event];

No, we're not using pressesBegan. If we could pass the Menu button event up to the presenting view controller, so the system could handle it at that level (and then exit the app), that would work – I'm not sure that can be done though.

You can use exit(0) but it doesn't look good due to the missing fade-out animation and most likely would be rejected at review.


Implement pressesBegan in the tab view controller. If user presses Menu at Signin then do as bsabiston says and super the press. Because in your use case, there is no screen higher up where the user can go if they are not signed in, so Menu at Signin means Back to Home.


It's likely that the tab view controller gets the press before the sign in controller depending on how you have things set up. You can add pressesBegan to both controllers and add logging to see which is being called, and in which order. You will need to super the first one in order for the system to handle Menu back to Home screen.

Implement pressesEnded in the Sign In screen like below. The app will exit gracefully with the exit chime.


override func pressesEnded(presses: Set<UIPress>, withEvent event: UIPressesEvent?) {     
     for press in presses {
          switch press.type {
          case .Menu:
               UIApplication.sharedApplication().pressesEnded(presses, withEvent: event)
...

I don't think calling pressesEnded on UIApplication directly is a good idea, you're better off just calling super here and letting it bubble-up to UIApplication normally.

Just a small point...but...

Apple's guidelines say you should avoid presenting a sign-on screen as your first screen.

Instead you should not ask the user to sign-in until absolutely necessary.

Give them all the free goodies first.

We're doing a similar thing for our app. We load the sign in screen as out rootviewcontroller when the user isn't logged in and then swap this out for the tabbarcontroller once the user has logged in.


Seems to work quite nicely and fixes the issue of menu going back. Alternatively, I have seen people using a global "App Nav Controller" and then changing the root view of the nav controller rather than the app.

You can also do this without using pressesBegan,Cancel,Changed,Ended
The idea is that you provide a custom uipress and send it to the uiapplication.shared.pressesEnded and it gets called on the tap gesture pressEnded
Code Block
open class MenuUIPress : UIPress {
weak var view: UIView?
init(view: UIView) {
self.view = view
}
open override var timestamp: TimeInterval {
Date().timeIntervalSince1970
}
open override var phase: UIPress.Phase {
UIPress.Phase.began
}
open override var type: UIPress.PressType {
UIPress.PressType.menu
}
open override var window: UIWindow? {
view?.window
}
open override var responder: UIResponder? {
view
}
open override var gestureRecognizers: [UIGestureRecognizer]? {
return view?.gestureRecognizers
}
}
class SignInViewController: UIViewController {
func viewDidLoad() {
setUpTapGesture()
}
private func setUpTapGesture() {
#if os(tvOS)
let tap = UITapGestureRecognizer()
tap.allowedPressTypes = [NSNumber(value: UIPress.PressType.menu.rawValue)]
tap.addTarget(self, action: #selector(SignInViewController.sendEndMenuTap))
view.addGestureRecognizer(tap)
#endif
}
@objc func sendEndMenuTap(_ gestureRecognizer: UITapGestureRecognizer) {
print("@@@@@ called on touch end 3 state=", gestureRecognizer.state.rawValue)
UIApplication.shared.pressesEnded([MenuUIPress(view: view)], with: UIPressesEvent())
}
}