Reputation: 1539
I'm trying to share a single swiftUI Button View for iOS and tvOS. for iOS everything is working as expected but tvOS is not triggering button actions.
var body: some View {
Button(action: tapEvent) {
HStack {
if let image = icon, let uiimage = UIImage(named: image) {
Image(uiImage: uiimage)
.resizable()
.renderingMode(.template)
.foregroundColor(iconColorForState(active: active, buttonMode: mode))
.frame(width: 20, height: 20, alignment: .center)
}
if let title = label {
Text(title)
.lineLimit(1)
}
}
}
.buttonStyle(PlainButtonStyle())
.foregroundColor(foregroundColorForState(active: active, buttonMode: mode))
.padding()
.disabled(disabled)
.background(self.focused ? focussedBackgroundColorFor(active: active, mode: mode) : backgroundColorFor(active: active, mode: mode))
.frame(height: heightForButtonSize(size: size ?? .Base))
.cornerRadius(8)
.modifier(FocusableModifier { focused in
self.focused = focused
})
}
FocusableModifier (to allow building the button class in iOS and tvOS):
struct FocusableModifier: 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
}
}
Then I use this button like this in my iOS and tvOs views:
ButtonView(label: "Primary", mode: .Primary, tapEvent: { print("Tapped") })
Problem is that on iOS the button action is executed on tap, but on tvOS the button tap event is not executed.
Update: I found that when removing cornerRadius and the modifier, the button event DOES get triggered. If cornerRadius or modifier is present on the button, tvOS doesn't want to respond to button taps anymore. I still need the modifier and cornerRadius Though.
Upvotes: 3
Views: 1382
Reputation: 87605
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:
Upvotes: 4