Search code examples
swiftsprite-kitfocustvos

TVOS 10 SpriteKit Focus Navigation default focused item


I am trying to update my SpriteKit games to use the new SKNode focus navigation feature, but I am having trouble to change the default focused item.

Essentially I have this code in my button class to support focus navigation

class Button: SKSpriteNode {

   var isFocusable = true // easy way to disable focus incase menus are shown etc

   required public init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)

        userInteractionEnabled = true
   } 

// MARK: - Focus navigation

#if os(tvOS)
extension Button {

    /// Can become focused
    override var canBecomeFocused: Bool {
        return isFocusable
    }

    /// Did update focus
    override func didUpdateFocus(in context: UIFocusUpdateContext, with coordinator: UIFocusAnimationCoordinator) {

        if context.previouslyFocusedItem === self {
            // some SKAction to reset the button to default settings  
        }

        else if context.nextFocusedItem === self {
            // some SKAction to scale the button up 
        }
    }
} 
#endif     

Everything is working great, however by default the first button on the left side of the screen is focused.

I am trying to change this to another button but I cannot do it. I now you are supposed to use

 override var preferredFocusEnvironments: [UIFocusEnvironment]...

 // preferredFocusedView is depreciated

but I dont understand how and where to use this.

I tried adding this code to my menu scene to change the focused button from the default (shop button) to the play button thats on the right side of the screen.

class MenuScene: SKScene {

     // left side of screen
     lazy var shopButton: Button = self.childNode(withName: "shopButton")

    // right side of screen
    lazy var playButton: Button = self.childNode(withName: "playButton")

    // Set preferred focus 
    open override var preferredFocusEnvironments: [UIFocusEnvironment] {
       return [self.playButton]
    }
}

and calling

setNeedsFocusUpdate()
updateFocusIfNeeded()

in didMoveToView but it doesn't work.

How can I change my default focused button in SpriteKit?


Solution

  • So I finally got this working thanks to this great article.

    https://medium.com/folded-plane/tvos-10-getting-started-with-spritekit-and-focus-engine-53d8ef3b34f3#.pfwcw4u70

    The main step I completely missed is that you have tell your GameViewController that your scenes are the preferred focus environment. This essentially means that your SKScenes will handle the preferred focus instead of GameViewController.

    In a SpriteKit game the SKScenes should handle the UI such as buttons using SpriteKit APIs such as SKLabelNodes, SKSpriteNodes etc. Therefore you need to pass the preferred focus to the SKScene.

    class GameViewController: UIViewController {
    
          override func viewDidLoad() {
               super.viewDidLoad()
    
               // default code to present your 1st SKScene.
          }
    }
    
    #if os(tvOS)
    extension GameViewController {
    
          /// Tell GameViewController that the currently presented SKScene should always be the preferred focus environment
          override var preferredFocusEnvironments: [UIFocusEnvironment] {
               if let scene = (view as? SKView)?.scene {
                   return [scene]
               } 
               return []
          }
     }
     #endif
    

    Your buttons should be a subclass of SKSpriteNode that you will use for all your buttons in your game. Use enums and give them different names/ identifiers to distinguish between them when they are pressed (checkout Apples sample game DemoBots).

     class Button: SKSpriteNode {
    
          var isFocusable = true // easy way to later turn off focus for your buttons e.g. when overlaying menus etc.
    
          /// Can become focused
          override var canBecomeFocused: Bool {
              return isFocusable
          }
    
          /// Did update focus
          override func didUpdateFocus(in context: UIFocusUpdateContext, with coordinator: UIFocusAnimationCoordinator) {
    
              if context.previouslyFocusedItem === self {
                  // SKAction to reset focus animation for unfocused button
              }
    
              if context.nextFocusedItem === self {
                   // SKAction to run focus animation for focused button
              }
          }
     }
    

    Than in your Start Scene you can set the focus environment to your playButton or any other button.

    class StartScene: SKScene {
       ....
    }
    
    #if os(tvOS)
    extension StartScene {
          override var preferredFocusEnvironments: [UIFocusEnvironment] {
              return [playButton]
         }
    }
    #endif
    

    If you overlay a menu or other UI in a scene you can do something like this e.g GameScene (move focus to gameMenuNode if needed)

    class GameScene: SKScene {
       ....
    }
    
    #if os(tvOS)
    extension GameScene {
    
          override var preferredFocusEnvironments: [UIFocusEnvironment] {
             if isGameMenuShowing { // add some check like this
                 return [gameMenuNode]
             }
             return [] // empty means scene itself
         }    
    }
    #endif
    

    You will also have to tell your GameViewController to update its focus environment when you transition between SKScenes (e.g StartScene -> GameScene). This is especially important if you use SKTransitions, it took me a while to figure this out. If you use SKTransitions than the old and new scene are active during the transition, therefore the GameViewController will use the old scenes preferred focus environments instead of the new one which means the new scene will not focus correctly.

    I do it like this every time I transition between scenes. You will have to use a slight delay or it will not work correctly.

     ...
     view?.presentScene(newScene, transition: ...)
    
     #if os(tvOS)
        newScene.run(SKAction.wait(forDuration: 0.02)) { // wont work without delay
             view?.window?.rootViewController?.setNeedsFocusUpdate()
             view?.window?.rootViewController?.updateFocusIfNeeded()
        }
     #endif