Onfocuschange Not Triggered in Swiftui Tvos

onFocusChange not triggered in SwiftUI tvOS

The goal the question is unclear, but here is a simple demo of alternate approach to have managed focused & button click using custom button style. Maybe this will be helpful.

Tested with Xcode 12 / tvOS 14 (Simulator) - compare regular button vs custom button

Sample Image

struct MyButtonStyle: ButtonStyle {
func makeBody(configuration: Configuration) -> some View {
configuration.label
.scaleEffect(configuration.isPressed ? 1.2 : 1)
}
}

struct ContentView: View {
@State private var focused = false
var body: some View {
VStack {
Button(action: {
print(">>>> custom button")
}) { LabelView() }.buttonStyle(MyButtonStyle())

Button("Regular Button") {
print(">> regular button")
}
}
}
}

struct LabelView: View {
@Environment(\.isFocused) var focused: Bool
var body: some View {
RoundedRectangle(cornerRadius: 25.0)
.frame(width: 200, height: 100)
.foregroundColor(focused ? .blue : .gray)
.overlay(Text("Title").foregroundColor(.white))
}
}

Getting onFocusChange callback for Buttons in SwiftUI (tvOS)

Try using @Environment(\.isFocused) and .onChange(of:perform:) in a ButtonStyle:

struct ContentView: View {
var body: some View {
Button("top") {
// button action
}
.buttonStyle(MyButtonStyle())
}
}

struct MyButtonStyle: ButtonStyle {
@Environment(\.isFocused) var focused: Bool

func makeBody(configuration: Configuration) -> some View {
configuration.label
.onChange(of: focused) { newValue in
// do whatever based on focus
}
}
}

IIRC using @Environment(\.isFocused) inside a ButtonStyle may only work on iOS 14.5+, but you could create a custom View instead of a ButtonStyle to support older versions.

SwiftUI Share Button between iOS and TVOS, focusable(_:onFocusChange:)' is unavailable in iOS

You need to use conditional compilation for such cases. For better reusability to is possible to wrap it in view modifier, like

struct DemoFocusableModifier: ViewModifier {
private let isFocusable: Bool
private let onFocusChange: (Bool) -> Void

init (_ isFocusable: Bool = true, onFocusChange: @escaping (Bool) -> Void = { _ in }) {
self.isFocusable = isFocusable
self.onFocusChange = onFocusChange
}

@ViewBuilder
func body(content: Content) -> some View {
#if os(tvOS)
content
.focusable(isFocusable, onFocusChange: onFocusChange)
#else
content
#endif
}
}

and use (instead of your .focusable) as

  }
.modifier(DemoFocusableModifier { focused in
// content here
})

Prepared with Xcode 13 / iOS 15

SwiftUI Button tvOS+iOS action working for iOS not on tvOS

The problem in your code is that your focusable modifier is blocking the clicks.

To solve this you can reimplement the button from the scratch. I've created CustomButton inspired by this answer: it'll be a plain button on iOS and a custom one on Apple TV:

struct CustomButton<Content>: View where Content : View {
@State
private var focused = false
@State
private var pressed = false

let action: () -> Void
@ViewBuilder
let content: () -> Content

var body: some View {
contentView
.background(focused ? Color.green : .yellow)
.cornerRadius(20)
.scaleEffect(pressed ? 1.1 : 1)
.animation(.default, value: pressed)
}

var contentView: some View {
#if os(tvOS)
ZStack {
ClickableHack(focused: $focused, pressed: $pressed, action: action)
content()
.padding()
.layoutPriority(1)
}
#else
Button(action: action, label: content)
#endif
}
}

class ClickableHackView: UIView {
weak var delegate: ClickableHackDelegate?

override init(frame: CGRect) {
super.init(frame: frame)
}

override func pressesBegan(_ presses: Set<UIPress>, with event: UIPressesEvent?) {
if validatePress(event: event) {
delegate?.pressesBegan()
} else {
super.pressesBegan(presses, with: event)
}
}

override func pressesEnded(_ presses: Set<UIPress>, with event: UIPressesEvent?) {
if validatePress(event: event) {
delegate?.pressesEnded()
} else {
super.pressesEnded(presses, with: event)
}
}

override func pressesCancelled(_ presses: Set<UIPress>, with event: UIPressesEvent?) {
if validatePress(event: event) {
delegate?.pressesEnded()
} else {
super.pressesCancelled(presses, with: event)
}
}

private func validatePress(event: UIPressesEvent?) -> Bool {
event?.allPresses.map({ $0.type }).contains(.select) ?? false
}

override func didUpdateFocus(in context: UIFocusUpdateContext, with coordinator: UIFocusAnimationCoordinator) {
delegate?.focus(focused: isFocused)
}

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

override var canBecomeFocused: Bool {
return true
}
}

protocol ClickableHackDelegate: AnyObject {
func focus(focused: Bool)
func pressesBegan()
func pressesEnded()
}

struct ClickableHack: UIViewRepresentable {
@Binding var focused: Bool
@Binding var pressed: Bool
let action: () -> Void

func makeUIView(context: UIViewRepresentableContext<ClickableHack>) -> UIView {
let clickableView = ClickableHackView()
clickableView.delegate = context.coordinator
return clickableView
}

func updateUIView(_ uiView: UIView, context: UIViewRepresentableContext<ClickableHack>) {
}

func makeCoordinator() -> Coordinator {
return Coordinator(self)
}

class Coordinator: NSObject, ClickableHackDelegate {
private let control: ClickableHack

init(_ control: ClickableHack) {
self.control = control
super.init()
}

func focus(focused: Bool) {
control.focused = focused
}

func pressesBegan() {
control.pressed = true
}

func pressesEnded() {
control.pressed = false
control.action()
}
}
}

Usage:

CustomButton(action: {
print("clicked 1")
}) {
Text("Clickable 1")
}
CustomButton(action: {
print("clicked 2")
}) {
Text("Clickable 2")
}

Result:

Sample Image



Related Topics



Leave a reply



Submit