Library / Drag, swipe & scroll

Pull-to-refresh threshold

The moment a pull gesture crosses the "release to refresh" threshold, plus a success on completion.

0ms 103ms 205ms 308ms 410ms
Feel
A rigid click exactly at the threshold — the sensation of a mechanism engaging — then a success pattern when new content lands.
APIs
UIImpactFeedbackGenerator(style: .rigid).sensoryFeedback(.impact)
Minimum OS
iOS 17+ / iOS 13+
Raw .md
SwiftUI (iOS 17+)
import SwiftUI

struct FeedView: View {
    @State private var items: [String] = (1...20).map { "Item \($0)" }
    @State private var refreshed = 0

    var body: some View {
        List(items, id: \.self) { Text($0) }
            .refreshable {
                // The system plays the threshold haptic for .refreshable.
                await reload()
                refreshed += 1
            }
            .sensoryFeedback(.success, trigger: refreshed)
    }

    func reload() async {
        try? await Task.sleep(for: .seconds(1))
        items.shuffle()
    }
}
UIKit (custom threshold)
final class FeedViewController: UITableViewController {
    private let threshold: CGFloat = -110
    private let clickHaptic = UIImpactFeedbackGenerator(style: .rigid)
    private var armed = false

    override func scrollViewDidScroll(_ scrollView: UIScrollView) {
        let pull = scrollView.contentOffset.y + scrollView.adjustedContentInset.top

        if pull < threshold && !armed {
            armed = true
            clickHaptic.impactOccurred()   // mechanism engages
        } else if pull > threshold * 0.6 && armed && !scrollView.isDragging {
            armed = false                  // re-arm after release
        }
    }
}
Copied