Something went wrong. Try again.
A slopfork of Sam Henri Gold's Twitter client, reskinned like Tweetie 2 / Twitter for iPhone circa 2012. Personal use.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778import SwiftUI
extension View { /// The action reports success so a failed request never advances "Last Updated". /// Put `ClassicRefreshControlStyle` inside the scroll content to hide only its system spinner. package func classicRefreshable(lastUpdated: Date? = nil, action: @escaping () async -> Bool) -> some View { modifier(ClassicRefreshable(lastUpdated: lastUpdated, action: action)) }}
private struct ClassicRefreshable: ViewModifier { let lastUpdated: Date? let action: () async -> Bool @State private var pull = PullToRefresh()
func body(content: Content) -> some View { content .refreshable { pull.isRefreshing = true ClassicSound.psst2.play() let succeeded = await action() && !Task.isCancelled pull.finish(succeeded: succeeded, at: .now) if succeeded { ClassicSound.pop.play() } } .onScrollGeometryChange(for: PullGeometry.self) { geometry in PullGeometry( distance: max(-(geometry.contentOffset.y + geometry.contentInsets.top), 0), topInset: geometry.contentInsets.top ) } action: { _, geometry in if !pull.isRefreshing { pull.restingTopInset = geometry.topInset } pull.distance = max(geometry.distance + geometry.topInset - pull.restingTopInset, 0) } .onScrollPhaseChange { _, phase in pull.isDragging = phase == .interacting } .onChange(of: pull.phase) { old, new in if old == .pull, new == .release { ClassicSound.psst1.play() } } .overlay(alignment: .top) { if pull.distance > 0 { ClassicPullToRefresh( phase: pull.phase, lastUpdated: lastUpdated ?? pull.lastSuccessfulRefresh, height: pull.distance ) } } }}
private struct PullGeometry: Equatable { var distance: CGFloat var topInset: CGFloat}
@Observablefinal class PullToRefresh { // UIKit doesn't expose its arm distance. The arrow and catch sound share this approximation. static let triggerDistance: CGFloat = 80
var distance: CGFloat = 0 var restingTopInset: CGFloat = 0 var isDragging = false var isRefreshing = false private(set) var lastSuccessfulRefresh: Date?
func finish(succeeded: Bool, at date: Date) { if succeeded { lastSuccessfulRefresh = date } isRefreshing = false }
var phase: ClassicPullToRefresh.Phase { if isRefreshing { return .loading } return isDragging && distance >= Self.triggerDistance ? .release : .pull }}