能否从ViewModel订阅SwiftUI tvOS视图的焦点状态?
问题根因说明
你使用废弃的focusable(_:onFocusChange:)修饰符后原有@Environment(\.isFocused)失效,是因为该修饰符会将当前视图本身标记为可聚焦元素,覆盖了内部子视图(比如内置Button)的聚焦响应链,导致子视图的焦点状态无法正常同步到环境变量中。
兼容tvOS 14+ 可用方案
推荐通过UIKit桥接的方式自定义焦点监听修饰符,不会破坏原有SwiftUI的焦点逻辑,同时可以拿到焦点变更回调,实现思路如下:
1. 自定义焦点监听修饰符
通过UIViewRepresentable桥接UIKit的焦点更新回调,不会影响原有视图的isFocused环境变量:
// 底层监听焦点的UIView class FocusMonitorView: UIView { var onFocusChange: (Bool) -> Void init(onFocusChange: @escaping (Bool) -> Void) { self.onFocusChange = onFocusChange super.init(frame: .zero) isUserInteractionEnabled = false } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func didUpdateFocus(in context: UIFocusUpdateContext, with coordinator: UIFocusAnimationCoordinator) { super.didUpdateFocus(in: context, with: coordinator) // 判断当前绑定的父视图是否为下一个聚焦视图 let isParentFocused = context.nextFocusedView == superview onFocusChange(isParentFocused) } } // SwiftUI 桥接层 struct FocusListener: UIViewRepresentable { var onFocusChange: (Bool) -> Void func makeUIView(context: Context) -> FocusMonitorView { FocusMonitorView(onFocusChange: onFocusChange) } func updateUIView(_ uiView: FocusMonitorView, context: Context) { uiView.onFocusChange = onFocusChange } } // 视图扩展方法 extension View { func onFocusChange(_ handler: @escaping (Bool) -> Void) -> some View { self.background(FocusListener(onFocusChange: handler)) } }
2. 组件内通过Combine抛发焦点事件
在你的可复用组件内定义PassthroughSubject,将焦点变更事件发送给外部容器:
struct ReusableTVComponent: View { // 对外暴露的焦点变化Publisher public let focusPublisher = PassthroughSubject<Bool, Never>() @Environment(\.isFocused) private var isFocused private let buttonStyle = CustomButtonStyle() var body: some View { Button(action: { // 按钮点击逻辑 }) { Text("组件按钮") } // 原有样式逻辑不受影响 .background(isFocused ? buttonStyle.backgroundColor.active : buttonStyle.backgroundColor.normal) // 新增自定义焦点监听 .onFocusChange { isFocused in focusPublisher.send(isFocused) } } }
3. 容器视图订阅事件
容器视图订阅组件的focusPublisher即可拿到内部焦点状态,调整其他UI元素:
struct ContainerView: View { @State private var componentIsFocused = false private var cancellables = Set<AnyCancellable>() var body: some View { VStack { let component = ReusableTVComponent() component // 其他需要根据焦点调整的UI元素 Text(componentIsFocused ? "组件已聚焦" : "组件未聚焦") } .onAppear { // 订阅焦点变化 component.focusPublisher .receive(on: DispatchQueue.main) .sink { isFocused in componentIsFocused = isFocused } .store(in: &cancellables) } } }
常见问题解答
- @FocusState添加didSet不触发是否正常?
正常。@FocusState是SwiftUI管理的状态属性包装器,它的存储逻辑由框架托管,直接添加的didSet不会被触发,若要监听@FocusState的变化(仅限tvOS 15+),可以使用onChange(of:)修饰符监听值变化。 - 能否直接监听
@Environment(\.isFocused)生成Publisher?
不能。@Environment属性包装器没有提供投影值,无法直接生成Publisher,必须通过自定义监听回调结合PassthroughSubject中转实现事件抛发。 - 全局焦点通知的身份比对方案
如果继续使用全局通知的方案,可以给需要监听的视图设置唯一的accessibilityIdentifier,收到通知时比对nextFocusedItem对应的视图的accessibilityIdentifier即可完成身份识别。
内容的提问来源于stack exchange,提问作者invalidArgument
相关产品推荐
相关产品推荐

