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?
So I finally got this working thanks to this great article.
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