Swift中实现ScrollView恒定速度自动滚动的方案咨询
SwiftUI 实现恒定速度自动滚动(含速度调节)
核心思路
通过CADisplayLink(与屏幕刷新率同步的定时器)驱动滚动偏移量持续更新,配合滑块控制速度参数,实现平滑的恒定速度自动滚动。相比普通Timer,CADisplayLink能避免滚动卡顿,更适合这类需要流畅动画的场景。
代码实现
首先封装一个可复用的自动滚动ScrollView组件:
import SwiftUI struct AutoScrollView<Content: View>: UIViewRepresentable { var content: Content @Binding var isAutoScrolling: Bool @Binding var scrollSpeed: CGFloat // 单位:点/秒 private let scrollView = UIScrollView() init(isAutoScrolling: Binding<Bool>, scrollSpeed: Binding<CGFloat>, @ViewBuilder content: () -> Content) { self._isAutoScrolling = isAutoScrolling self._scrollSpeed = scrollSpeed self.content = content() } func makeUIView(context: Context) -> UIScrollView { let hostingView = UIHostingController(rootView: content).view! hostingView.translatesAutoresizingMaskIntoConstraints = false scrollView.addSubview(hostingView) NSLayoutConstraint.activate([ hostingView.topAnchor.constraint(equalTo: scrollView.topAnchor), hostingView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor), hostingView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor), hostingView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor), hostingView.widthAnchor.constraint(equalTo: scrollView.widthAnchor) ]) scrollView.delegate = context.coordinator context.coordinator.setupDisplayLink() return scrollView } func updateUIView(_ uiView: UIScrollView, context: Context) { context.coordinator.isAutoScrolling = isAutoScrolling context.coordinator.scrollSpeed = scrollSpeed } func makeCoordinator() -> Coordinator { Coordinator(parent: self) } class Coordinator: NSObject, UIScrollViewDelegate { var parent: AutoScrollView var isAutoScrolling: Bool = false var scrollSpeed: CGFloat = 0 private var displayLink: CADisplayLink? private var lastTimestamp: CFTimeInterval = 0 init(parent: AutoScrollView) { self.parent = parent } func setupDisplayLink() { displayLink = CADisplayLink(target: self, selector: #selector(updateScrollPosition)) displayLink?.add(to: .main, forMode: .common) } @objc func updateScrollPosition(_ link: CADisplayLink) { guard isAutoScrolling, scrollSpeed > 0, lastTimestamp != 0 else { lastTimestamp = link.timestamp return } let deltaTime = link.timestamp - lastTimestamp let deltaY = scrollSpeed * CGFloat(deltaTime) let maxOffsetY = parent.scrollView.contentSize.height - parent.scrollView.bounds.height let newOffsetY = min(parent.scrollView.contentOffset.y + deltaY, maxOffsetY) parent.scrollView.setContentOffset(CGPoint(x: 0, y: newOffsetY), animated: false) lastTimestamp = link.timestamp } // 手动滚动时自动暂停(可选逻辑) func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { isAutoScrolling = false } } }
然后是使用该组件的示例页面:
struct AutoScrollDemoView: View { @State private var isAutoScrolling = false @State private var scrollSpeed: CGFloat = 50 // 初始速度50点/秒 var body: some View { VStack(spacing: 16) { // 控制栏 HStack { Toggle("自动滚动", isOn: $isAutoScrolling) Slider(value: $scrollSpeed, in: 0...200) Text(String(format: "%.0f 点/秒", scrollSpeed)) } .padding(.horizontal) // 自动滚动区域 AutoScrollView(isAutoScrolling: $isAutoScrolling, scrollSpeed: $scrollSpeed) { VStack(spacing: 20) { ForEach(1...50, id: \.self) { index in Text("第 \(index) 行 - 吉他和弦内容示例") .font(.title) .frame(maxWidth: .infinity, alignment: .leading) .padding(.horizontal) } } } .border(.gray) } .padding() } }
关键细节
scrollSpeed直接控制每秒滚动的像素点数,滑块调节实时生效- 限制滚动边界,避免滚出内容区域
- 手动拖动时自动暂停自动滚动,提升交互体验
UIKit 实现方案
核心思路
直接基于原生UIScrollView,用CADisplayLink驱动contentOffset更新,配合UISlider调节速度,逻辑与SwiftUI版本一致,适合纯UIKit项目。
代码实现
import UIKit class AutoScrollViewController: UIViewController { private let scrollView = UIScrollView() private let contentView = UIView() private let autoScrollToggle = UISwitch() private let speedSlider = UISlider() private let speedLabel = UILabel() private var displayLink: CADisplayLink? private var isAutoScrolling = false { didSet { autoScrollToggle.isOn = isAutoScrolling } } private var scrollSpeed: CGFloat = 50 { didSet { speedLabel.text = String(format: "%.0f 点/秒", scrollSpeed) } } private var lastTimestamp: CFTimeInterval = 0 override func viewDidLoad() { super.viewDidLoad() setupUI() setupDisplayLink() populateContent() } private func setupUI() { // 顶部控制栏 let controlStack = UIStackView(arrangedSubviews: [ UILabel(text: "自动滚动"), autoScrollToggle, UILabel(text: "速度"), speedSlider, speedLabel ]) controlStack.axis = .horizontal controlStack.spacing = 12 controlStack.alignment = .center // 布局配置 [controlStack, scrollView].forEach { $0.translatesAutoresizingMaskIntoConstraints = false view.addSubview($0) } contentView.translatesAutoresizingMaskIntoConstraints = false scrollView.addSubview(contentView) NSLayoutConstraint.activate([ controlStack.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 16), controlStack.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16), controlStack.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16), scrollView.topAnchor.constraint(equalTo: controlStack.bottomAnchor, constant: 16), scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16), scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16), scrollView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -16), contentView.topAnchor.constraint(equalTo: scrollView.topAnchor), contentView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor), contentView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor), contentView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor), contentView.widthAnchor.constraint(equalTo: scrollView.widthAnchor) ]) // 控件事件绑定 autoScrollToggle.addTarget(self, action: #selector(toggleAutoScroll), for: .valueChanged) speedSlider.minimumValue = 0 speedSlider.maximumValue = 200 speedSlider.value = Float(scrollSpeed) speedSlider.addTarget(self, action: #selector(updateSpeed), for: .valueChanged) speedLabel.text = String(format: "%.0f 点/秒", scrollSpeed) } private func populateContent() { var lastView: UIView? = nil for index in 1...50 { let label = UILabel() label.text = "第 \(index) 行 - 吉他和弦内容示例" label.font = .systemFont(ofSize: 24) label.translatesAutoresizingMaskIntoConstraints = false contentView.addSubview(label) NSLayoutConstraint.activate([ label.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16), label.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16), label.topAnchor.constraint(equalTo: lastView?.bottomAnchor ?? contentView.topAnchor, constant: 20) ]) lastView = label } // 确保内容高度足够 if let lastView = lastView { contentView.bottomAnchor.constraint(equalTo: lastView.bottomAnchor, constant: 20).isActive = true } } private func setupDisplayLink() { displayLink = CADisplayLink(target: self, selector: #selector(updateScrollPosition)) displayLink?.add(to: .main, forMode: .common) } @objc private func toggleAutoScroll() { isAutoScrolling = autoScrollToggle.isOn lastTimestamp = 0 // 重置时间戳避免首次滚动跳变 } @objc private func updateSpeed() { scrollSpeed = CGFloat(speedSlider.value) } @objc private func updateScrollPosition(_ link: CADisplayLink) { guard isAutoScrolling, scrollSpeed > 0, lastTimestamp != 0 else { lastTimestamp = link.timestamp return } let deltaTime = link.timestamp - lastTimestamp let deltaY = scrollSpeed * CGFloat(deltaTime) let maxOffsetY = scrollView.contentSize.height - scrollView.bounds.height let newOffsetY = min(scrollView.contentOffset.y + deltaY, maxOffsetY) scrollView.setContentOffset(CGPoint(x: 0, y: newOffsetY), animated: false) lastTimestamp = link.timestamp } // 手动滚动时暂停自动滚动 override func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { isAutoScrolling = false } } // 简化UILabel初始化的扩展 extension UILabel { convenience init(text: String) { self.init() self.text = text } }
内容的提问来源于stack exchange,提问作者Kelvin Jou
相关产品推荐
相关产品推荐

