andrewz
andrewz

Reputation: 5220

Focusing an SKNode

I have the following code that builds and runs in a simulator in tvOS 17.5. I am trying to navigate focus between the two SKNodes inside the GameScene that is an SKScene. I am unable to have any of the two FocusableNodes become visibly focused. I am able to focus the scene (I get a blue rectangle around it), but not transfer focus to any of the two focusable child nodes. One issue is that pressesBegan is never called, neither is preferredFocusEnvironments.

[EDIT] Once I comment out focus-related view modifiers and removed the button in the body, I get one of the focusable nodes to focus (its color changes to blue), but can't navigate focus, it's just stuck on that first button. Also I get this runtime warning: Using legacy initializer -[UIFocusRegion initWithFrame:] for region <_UIFocusItemRegion: 0x6000017529c0> - if this region is initialized by a client, please move over to using the UIFocusItem API. If this region is coming from UIKit, this is a UIKit bug.

struct ContentView: View {
    //@FocusState private var isSpriteViewFocused: Bool
    
    var body: some View {
        VStack {
            SpriteView(scene: GameScene())
                .frame(width: 600, height: 400)
                //.focusable(true)
                //.focused($isSpriteViewFocused)
                //.overlay(
                //    RoundedRectangle(cornerRadius: 10)
                //        .stroke(isSpriteViewFocused ? Color.blue : Color.clear, lineWidth: 5)
                //)
            
            //Button("Focus SpriteView [\(isSpriteViewFocused)]") {
            //    isSpriteViewFocused = true //.toggle()
            //}
            //.padding()
        }
        .padding()
    }
}

class GameScene: SKScene {
    
    override func didMove(to view: SKView) {
        backgroundColor = .gray
        
        let size1 = view.bounds.size
        self.size = size1
        
            // Create a focusable SKNode
        let focusableNode1 = FocusableNode()
        focusableNode1.position = CGPoint(x: size1.width / 2, y: size1.height / 2 + 100)
        focusableNode1.name = "Focusable Node 1"
        addChild(focusableNode1)
        
        let focusableNode2 = FocusableNode()
        focusableNode2.position = CGPoint(x: size1.width / 2, y: size1.height / 2 - 100)
        focusableNode2.name = "Focusable Node 2"
        addChild(focusableNode2)
        
            // Create a non-focusable SKNode
        let nonFocusableNode = SKLabelNode(text: "Non-Focusable Node".uppercased())
        nonFocusableNode.fontSize = 15
        nonFocusableNode.fontColor = .white
        nonFocusableNode.position = CGPoint(x: size1.width / 2, y: size1.height / 2)
        nonFocusableNode.isUserInteractionEnabled = false // Disable focus
        addChild(nonFocusableNode)
        
        if false {
            addFocusGuides()
        } else {
            self.scene?.view?.window?.setNeedsFocusUpdate()
            self.scene?.view?.window?.updateFocusIfNeeded()
        }
    }
    
    private func addFocusGuides() {
        guard let view = view else { return }
        
        let guide1 = UIFocusGuide()
        view.addLayoutGuide(guide1)
        NSLayoutConstraint.activate([
            guide1.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            guide1.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            guide1.topAnchor.constraint(equalTo: view.topAnchor),
            guide1.bottomAnchor.constraint(equalTo: view.centerYAnchor)
        ])
        guide1.preferredFocusEnvironments = [childNode(withName: "Focusable Node 2")!]
        
        let guide2 = UIFocusGuide()
        view.addLayoutGuide(guide2)
        NSLayoutConstraint.activate([
            guide2.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            guide2.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            guide2.topAnchor.constraint(equalTo: view.centerYAnchor),
            guide2.bottomAnchor.constraint(equalTo: view.bottomAnchor)
        ])
        guide2.preferredFocusEnvironments = [childNode(withName: "Focusable Node 1")!]
    }
    
    override var preferredFocusEnvironments: [UIFocusEnvironment] {
        return children.compactMap { $0 as? FocusableNode }
    }
    
    override func shouldUpdateFocus(in context: UIFocusUpdateContext) -> Bool {
        return true
    }
    
    override func pressesBegan(_ presses: Set<UIPress>, with event: UIPressesEvent?) {
        super.pressesBegan(presses, with: event)
        
            // Handle focusable node presses
        if presses.first?.type == .select {
            if let windowScene = view?.window?.windowScene {
                if let focusedNode = windowScene.focusSystem?.focusedItem as? FocusableNode {
                    print("\(focusedNode.name ?? "Unknown Node") selected")
                    focusedNode.performAction()
                }
            }
        }
    }
}

class FocusableNode: SKSpriteNode {
    private let defaultColor = UIColor.white
    private let focusedColor = UIColor.blue
    
    init() {
        let size = CGSize(width: 200, height: 100)
        super.init(texture: nil, color: defaultColor, size: size)
        isUserInteractionEnabled = true // Enables focus and interaction
    }
    
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
    }
    
    override var canBecomeFocused: Bool {
        return true
    }
    
    override func didUpdateFocus(in context: UIFocusUpdateContext, with coordinator: UIFocusAnimationCoordinator) {
        if context.nextFocusedItem === self {
            color = focusedColor
        } else {
            color = defaultColor
        }
    }
    
    func performAction() {
            // Perform an action when selected
        print("Action performed by \(name ?? "Unknown Node")")
    }
}

@main
struct MyTVOSApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

Upvotes: 2

Views: 61

Answers (1)

andrewz
andrewz

Reputation: 5220

I wasn't able to find a solution that integrates the SwiftUI and SpriteKit focus systems. I found a workaround that uses SwiftUI for focus management and communicates that information to the scene, which updates to match the state. This is not the answer I was looking for, as it is rather complex, but it works. I will not mark this answer as the solution - I'm enclosing it here as an idea for anyone tackling the same problem.

import Foundation
import SwiftUI
import SpriteKit

struct ContentView: View {

    @FocusState private var focusButton0 : Bool
    @FocusState private var focusButton1 : Bool

    @State private var focusedIndex      : Int = -1
    @State private var tappedIndex       : Int = -1
    
    var body: some View {
        ZStack {
            // The SpriteKit scene
            SpriteView(scene: GameScene(focusedIndex: $focusedIndex, tappedIndex: $tappedIndex))
                .frame(width: 600, height: 400)
                .background(Color.black)
            
            // Overlaying SwiftUI buttons that correspond to FocusableNodes
            VStack(spacing: 0) {
                
                Rectangle()
                    .fill(Color.clear)
                    .frame(width: 200, height: 100)
//                    .background(focusedIndex == 0 ? Color.orange.opacity(0.5) : Color.clear)
                    .focusable(true)
                    .focused($focusButton0)
                    .onChange(of: focusButton0) { _,v in
                        if v {
                            focusedIndex = 0
                        }
                    }
                    .onTapGesture {
                        tappedIndex = 0
                    }

                Rectangle()
                    .fill(Color.clear)
                    .frame(width: 200, height: 100)
//                    .background(focusedIndex == 1 ? Color.orange.opacity(0.5) : Color.clear)
                    .focusable(true)
                    .focused($focusButton1)
                    .onChange(of: focusButton1) { _,v in
                        if v {
                            focusedIndex = 1
                        }
                    }
                    .onTapGesture {
                        tappedIndex = 1
                    }

            }
        }
    }
}

class GameScene: SKScene {
    @Binding var focusedIndex: Int
    @Binding var tappedIndex: Int
    private var focusableNodes: [FocusableNode] = []

    init(focusedIndex: Binding<Int>, tappedIndex: Binding<Int>) {
        self._focusedIndex = focusedIndex
        self._tappedIndex = tappedIndex
        super.init(size: CGSize(width: 600, height: 400))
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func didMove(to view: SKView) {
        backgroundColor = .black

        // Define focusable nodes
        let button1 = FocusableNode(name: "Button 1", position: 0)
        button1.position = CGPoint(x: size.width / 2, y: size.height / 2 + 50)
        addChild(button1)

        let button2 = FocusableNode(name: "Button 2", position: 1)
        button2.position = CGPoint(x: size.width / 2, y: size.height / 2 - 50)
        addChild(button2)

        focusableNodes = [button1, button2]
    }

    override func update(_ currentTime: TimeInterval) {
        for node in focusableNodes {
            node.update(isFocused: node.positionIndex == focusedIndex, isTapped: node.positionIndex == tappedIndex)
        }
    }
}

class FocusableNode: SKSpriteNode {
    var positionIndex: Int

    init(name: String, position: Int) {
        self.positionIndex = position
        let size = CGSize(width: 200, height: 100)
        super.init(texture: nil, color: .white, size: size)
//        isUserInteractionEnabled = true
        self.name = name
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    func update(isFocused: Bool, isTapped: Bool) {
        if isFocused {
            color = .blue
        } else if isTapped {
            color = .red
        } else {
            color = .gray
        }
    }
}

@main
struct MyTVOSApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

Upvotes: 1

Related Questions