如何修复SwiftUI tvOS ScrollView滚动卡顿抖动及ScrollViewProxy调用崩溃问题
问题根因
- tvOS 14.7版本中SwiftUI原生ScrollView对长列表的性能优化逻辑存在缺陷,会在滑动过程中暂停视图渲染,仅在滑动结束后才更新内容,导致滚动过程画面停滞。
- 使用ScrollViewReader实现时出现崩溃的核心原因是
onFocusChange回调的触发时机处于视图更新周期内,此时直接修改@State变量、调用scrollTo方法都违反了SwiftUI的视图更新规则,因此触发崩溃和运行时警告。
方案一:修复现有ScrollViewReader实现的崩溃问题
仅需将onFocusChange闭包内的所有状态修改、滚动操作放入主队列异步执行,跳出当前视图更新周期即可解决崩溃问题,同时保留原有的顺滑滚动效果。
修改后的完整代码如下:
import Foundation import SwiftUI struct FocusableButton: View { @State var isFocused = false let index: Int var scrollView: ScrollViewProxy var body: some View { VStack { Text("Button - \(index)") } .id(index) .frame(width: 200, height: 100) .padding() .scaleEffect(isFocused ? 1.1 : 1) .background(isFocused ? Color.white : Color.blue) .focusable(true, onFocusChange: { focused in // 异步执行,避开视图更新周期 DispatchQueue.main.async { isFocused = focused if focused { scrollView.scrollTo(index) } } }) } } struct TestView: View { var body: some View { ScrollViewReader { scrollView in ScrollView(.vertical, showsIndicators: false) { ForEach(0..<600) { index in FocusableButton(index: index, scrollView: scrollView) } } } } }
方案二:桥接UIKit UIScrollView彻底规避SwiftUI问题
如果需要更稳定的表现,避免旧版tvOS上SwiftUI的兼容性问题,可以用UIViewRepresentable封装UIKit的UIScrollView,完全由UIKit处理滚动逻辑,不存在原生SwiftUI ScrollView的滚动卡顿问题,也不需要依赖ScrollViewReader,还可以灵活关闭自动分页行为。
完整实现代码如下:
import SwiftUI import UIKit struct TVScrollView<Content: View>: UIViewRepresentable { let showsIndicators: Bool let disablePagination: Bool let content: Content init(showsIndicators: Bool = false, disablePagination: Bool = true, @ViewBuilder content: () -> Content) { self.showsIndicators = showsIndicators self.disablePagination = disablePagination self.content = content() } func makeUIView(context: Context) -> UIScrollView { let scrollView = UIScrollView() scrollView.showsVerticalScrollIndicator = showsIndicators // 适配tvOS遥控器触摸输入 scrollView.panGestureRecognizer.allowedTouchTypes = [NSNumber(value: UITouch.TouchType.indirect.rawValue)] // 按需求关闭自动分页 scrollView.isPagingEnabled = !disablePagination // 托管SwiftUI内容视图 let hostingVC = UIHostingController(rootView: content) hostingVC.view.translatesAutoresizingMaskIntoConstraints = false scrollView.addSubview(hostingVC.view) NSLayoutConstraint.activate([ hostingVC.view.leadingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.leadingAnchor), hostingVC.view.trailingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.trailingAnchor), hostingVC.view.topAnchor.constraint(equalTo: scrollView.contentLayoutGuide.topAnchor), hostingVC.view.bottomAnchor.constraint(equalTo: scrollView.contentLayoutGuide.bottomAnchor), hostingVC.view.widthAnchor.constraint(equalTo: scrollView.frameLayoutGuide.widthAnchor) ]) return scrollView } func updateUIView(_ uiView: UIScrollView, context: Context) {} } // 使用示例 struct TestView: View { var body: some View { TVScrollView { VStack(spacing: 20) { ForEach(0..<600) { index in Button(action: {}) { Text("Button - \(index)") .frame(width: 200, height: 100) .foregroundColor(.white) .background(Color.blue) } .buttonStyle(.plain) } } .padding(.vertical, 30) } .edgesIgnoringSafeArea(.all) } }
内容的提问来源于stack exchange,提问作者Shaybc
相关产品推荐
相关产品推荐

