本文原创,在本人的 CSDN 发过一次。
由于目前 swiftui 未提供 Scrollview 的实现,我在网上找到了一个 swiftuiLab 出得代码,利用滚动偏移实现的下拉刷新。
// Authoer: The SwiftUI Lab // Full article: https://swiftui-lab.com/scrollview-pull-to-refresh/ import SwiftUI import Foundation struct RefreshableScrollView<Content: View>: View { @State private var previousScrollOffset: CGFloat = 0 @State private var scrollOffset: CGFloat = 0 @State private var frozen: Bool = false @State private var rotation: Angle = .degrees(0) var threshold: CGFloat = 80 @Binding var refreshing: Bool let content: Content init(height: CGFloat = 80, refreshing: Binding<Bool>, @ViewBuilder content: () -> Content) { self.threshold = height self._refreshing = refreshing self.content = content() } var body: some View { return VStack { ScrollView { ZStack(alignment: .top) { MovingView() VStack { self.content }.alignmentGuide(.top, computeValue: { d in (self.refreshing && self.frozen) ? -self.threshold : 0.0 }) SymbolView(height: self.threshold, loading: self.refreshing, frozen: self.frozen, rotation: self.rotation) } } .background(FixedView()) .onPreferenceChange(RefreshableKeyTypes.PrefKey.self) { values in self.refreshLogic(values: values) } } } func refreshLogic(values: [RefreshableKeyTypes.PrefData]) { DispatchQueue.main.async { // Calculate scroll offset let movingBounds = values.first { $0.vType == .movingView }?.bounds ?? .zero let fixedBounds = values.first { $0.vType == .fixedView }?.bounds ?? .zero self.scrollOffset = movingBounds.minY - fixedBounds.minY self.rotation = self.symbolRotation(self.scrollOffset) // Crossing the threshold on the way down, we start the refresh process if !self.refreshing && (self.scrollOffset > self.threshold && self.previousScrollOffset <= self.threshold) { self.refreshing = true } if self.refreshing { // Crossing the threshold on the way up, we add a space at the top of the scrollview if self.previousScrollOffset > self.threshold && self.scrollOffset <= self.threshold { self.frozen = true } } else { // remove the sapce at the top of the scroll view self.frozen = false } // Update last scroll offset self.previousScrollOffset = self.scrollOffset } } func symbolRotation(_ scrollOffset: CGFloat) -> Angle { // We will begin rotation, only after we have passed // 60% of the way of reaching the threshold. if scrollOffset < self.threshold * 0.60 { return .degrees(0) } else { // Calculate rotation, based on the amount of scroll offset let h = Double(self.threshold) let d = Double(scrollOffset) let v = max(min(d - (h * 0.6), h * 0.4), 0) return .degrees(180 * v / (h * 0.4)) } } struct SymbolView: View { var height: CGFloat var loading: Bool var frozen: Bool var rotation: Angle var body: some View { Group { if self.loading { // If loading, show the activity control VStack { Spacer() ActivityRep() Spacer() }.frame(height: height).fixedSize() .offset(y: -height + (self.loading && self.frozen ? height : 0.0)) } else { Image(systemName: "arrow.down") // If not loading, show the arrow .resizable() .aspectRatio(contentMode: .fit) .frame(width: height * 0.25, height: height * 0.25).fixedSize() .padding(height * 0.375) .rotationEffect(rotation) .offset(y: -height + (loading && frozen ? +height : 0.0)) } } } } struct MovingView: View { var body: some View { GeometryReader { proxy in Color.clear.preference(key: RefreshableKeyTypes.PrefKey.self, value: [RefreshableKeyTypes.PrefData(vType: .movingView, bounds: proxy.frame(in: .global))]) }.frame(height: 0) } } struct FixedView: View { var body: some View { GeometryReader { proxy in Color.clear.preference(key: RefreshableKeyTypes.PrefKey.self, value: [RefreshableKeyTypes.PrefData(vType: .fixedView, bounds: proxy.frame(in: .global))]) } } } } struct RefreshableKeyTypes { enum ViewType: Int { case movingView case fixedView } struct PrefData: Equatable { let vType: ViewType let bounds: CGRect } struct PrefKey: PreferenceKey { static var defaultValue: [PrefData] = [] static func reduce(value: inout [PrefData], nextValue: () -> [PrefData]) { value.append(contentsOf: nextValue()) } typealias Value = [PrefData] } } struct ActivityRep: UIViewRepresentable { func makeUIView(context: UIViewRepresentableContext<ActivityRep>) -> UIActivityIndicatorView { return UIActivityIndicatorView() } func updateUIView(_ uiView: UIActivityIndicatorView, context: UIViewRepresentableContext<ActivityRep>) { uiView.startAnimating() } }
使用很简单:设置偏移,设置绑定的刷新状态就行了。
RefreshableScrollView(height: 70, refreshing: self.$articleList.loading)
欢迎来到这里!
我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。
注册 关于