You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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:

  1. 通过Swift Package Manager添加依赖:https://github.com/siteline/SwiftUI-Introspect
  2. 修改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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.16 20:40:54