SwiftUI Button tvOS+iOS 操作适用于 iOS 不适用于 tvOS

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

我正在尝试为 iOS 和 tvOS 共享一个 swiftUI 按钮视图。 iOS 一切正常,但 tvOS 未触发按钮操作。

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(允许在 iOS 和 tvOS 中构建按钮 class):

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
    }
}

然后我在 iOS 和 tvOs 视图中像这样使用这个按钮:

ButtonView(label: "Primary", mode: .Primary, tapEvent: { print("Tapped") })

问题是在 iOS 上按钮动作在点击时执行,但在 tvOS 上按钮点击事件不执行。

更新: 我发现当删除 cornerRadius 和修改器时,按钮事件确实被触发了。如果按钮上存在 cornerRadius 或修饰符,tvOS 不想再响应按钮点击。我仍然需要修饰符和 cornerRadius。

您的代码中的问题是您的 focusable 修饰符阻止了点击。

要解决这个问题,您可以从头开始重新实现按钮。我受 启发创建了 CustomButton:它在 iOS 上是一个普通按钮,在 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({ [=10=].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()
        }
    }
}

用法:

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

结果: