SwiftUI快速滚动视图问题:无法中断拖拽引发的滚动动作
解决SwiftUI列表惯性滚动时快速滚动延迟问题
我实现了一个带有叠加快速滚动视图的可滚动列表,点击或拖拽快速滚动视图可快速定位到列表任意位置,但当列表因拖拽产生惯性滚动时,快速滚动操作必须等待原有滚动结束才会执行,等待时间过长。希望能解决这个问题,理想方案是中断原有滚动动作或直接覆盖目标位置,至少可以禁用或降低滚动惯性以缩短延迟。
现有实现代码
视图代码
struct ItemListView: View { @StateObject var viewModel = ItemViewModel() var body: some View { ScrollViewReader { proxy in List { ForEach(0..<200){ index in Text("Item \(index)") .id(String(index)) } } .scrollIndicators(.visible) .overlay(ItemQuickScrollView(scroll: { pos in withAnimation { proxy.scrollTo(viewModel.getQuickScrollTarget(pos)) } })) } .listStyle(.plain) } }
ViewModel代码
@MainActor public class ItemViewModel: ObservableObject { func getQuickScrollTarget(val: Double) -> String { return String(Int(200 * val)) } }
快速滚动视图代码
struct ItemQuickScrollView: View { @State var value: Double = .zero var scroll: (Double) -> Void var body: some View { GeometryReader { geometry in HStack { Spacer() Rectangle() .fill(Color.red.opacity(0.2)) // 调试用 .contentShape(Rectangle()) .frame(width: 40, height: geometry.size.height) .padding(.trailing, -15) .gesture( DragGesture(minimumDistance: 0, coordinateSpace: .local) .onChanged( { value in self.scrollAction(value.location.y / geometry.size.height) }) ) } } } func scrollAction(_ val: Double) -> Void { let yRelative = min(1, max(0, val)) self.scroll(yRelative) } }
解决方案
方法一:中断现有惯性滚动(推荐)
SwiftUI的ScrollViewReader.scrollTo在列表有惯性滚动时会加入动画队列,无法立即生效。我们可以通过获取底层的UITableView,强制停止惯性滚动后再执行定位。
这里使用Introspect库来获取UITableView:
- 通过Swift Package Manager添加依赖:
https://github.com/siteline/SwiftUI-Introspect - 修改
ItemListView代码:
struct ItemListView: View { @StateObject var viewModel = ItemViewModel() @State private var tableView: UITableView? // 存储底层UITableView引用 var body: some View { ScrollViewReader { proxy in List { ForEach(0..<200){ index in Text("Item \(index)") .id(String(index)) } } .scrollIndicators(.visible) .introspectTableView { tableView in self.tableView = tableView } .overlay(ItemQuickScrollView(scroll: { pos in // 先停止惯性滚动 if let tableView = tableView { tableView.setContentOffset(tableView.contentOffset, animated: false) } // 执行快速滚动定位 withAnimation { proxy.scrollTo(viewModel.getQuickScrollTarget(pos)) } })) } .listStyle(.plain) } }
方法二:禁用列表滚动惯性
如果不想依赖第三方库,可以通过自定义UIViewRepresentable包装UITableView,直接禁用滚动惯性:
struct NonInertialList<Content: View>: UIViewRepresentable { let content: () -> Content let ids: [String] // 列表项ID,用于滚动定位 func makeUIView(context: Context) -> UITableView { let tableView = UITableView() tableView.delegate = context.coordinator tableView.dataSource = context.coordinator tableView.register(HostingTableViewCell.self, forCellReuseIdentifier: "Cell") return tableView } func updateUIView(_ uiView: UITableView, context: Context) { context.coordinator.content = content() context.coordinator.ids = ids uiView.reloadData() } func makeCoordinator() -> Coordinator { Coordinator(content: content(), ids: ids) } class Coordinator: NSObject, UITableViewDelegate, UITableViewDataSource { var content: Content var ids: [String] init(content: Content, ids: [String]) { self.content = content self.ids = ids } func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { ids.count } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! HostingTableViewCell cell.hostingController.rootView = content return cell } // 禁用滚动惯性 func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) { targetContentOffset.pointee = scrollView.contentOffset } } } class HostingTableViewCell: UITableViewCell { let hostingController = UIHostingController(rootView: EmptyView()) override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) configureHostingController() } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } private func configureHostingController() { hostingController.view.translatesAutoresizingMaskIntoConstraints = false contentView.addSubview(hostingController.view) NSLayoutConstraint.activate([ hostingController.view.topAnchor.constraint(equalTo: contentView.topAnchor), hostingController.view.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), hostingController.view.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), hostingController.view.bottomAnchor.constraint(equalTo: contentView.bottomAnchor) ]) } }
然后修改ItemListView使用这个自定义列表:
struct ItemListView: View { @StateObject var viewModel = ItemViewModel() private let ids = (0..<200).map { String($0) } var body: some View { ScrollViewReader { proxy in NonInertialList(content: { ForEach(0..<200){ index in Text("Item \(index)") .id(String(index)) } }, ids: ids) .overlay(ItemQuickScrollView(scroll: { pos in withAnimation { proxy.scrollTo(viewModel.getQuickScrollTarget(pos)) } })) } } }
方法三:直接使用UIKit实现快速滚动
如果需要完全自定义滚动行为,可以直接用UIKit的UITableView和自定义快速滚动控件,这样能完全控制滚动的每一步,包括中断惯性滚动、立即定位等。这种方式灵活性最高,但需要放弃SwiftUI的List语法糖。
内容的提问来源于stack exchange,提问作者cheersmate
相关产品推荐
相关产品推荐

