import SwiftUI
struct RefreshScrollView<Content: View>: View {
@State private var preOffset: CGFloat = 0
@State private var offset: CGFloat = 0
@State private var frozen = false
@State private var rotation: Angle = .degrees(0)
@State private var updateTime: Date = Date()
var threshold: CGFloat = 70
@Binding var refreshing: Bool
let content: Content
init(_ threshold: CGFloat = 70, refreshing: Binding<Bool>, @ViewBuilder content: () -> Content) {
self.threshold = threshold
self._refreshing = refreshing
self.content = content()
}
var body: some View {
VStack {
ScrollView {
ZStack(alignment: .top) {
MovingPositionView()
VStack {
self.content
.alignmentGuide(.top, computeValue: { _ in
(self.refreshing && self.frozen) ? -self.threshold : 0
})
}
RefreshHeader(height: self.threshold,
loading: self.refreshing,
frozen: self.frozen,
rotation: self.rotation,
updateTime: self.updateTime)
}
}
.background(FixedPositionView())
.onPreferenceChange(RefreshPreferenceTypes.RefreshPreferenceKey.self) { values in
self.calculate(values)
}
.onChange(of: refreshing) { refreshing in
DispatchQueue.main.async {
if !refreshing {
self.updateTime = Date()
}
}
}
}
}
func calculate(_ values: [RefreshPreferenceTypes.RefreshPreferenceData]) {
DispatchQueue.main.async {
/// 计算croll offset
let movingBounds = values.first(where: { $0.viewType == .movingPositionView })?.bounds ?? .zero
let fixedBounds = values.first(where: { $0.viewType == .fixedPositionView })?.bounds ?? .zero
self.offset = movingBounds.minY - fixedBounds.minY
self.rotation = self.headerRotation(self.offset)
/// 触发刷新
if !self.refreshing, self.offset > self.threshold, self.preOffset <= self.threshold {
self.refreshing = true
}
if self.refreshing {
if self.preOffset > self.threshold, self.offset <= self.threshold {
self.frozen = true
}
} else {
self.frozen = false
}
self.preOffset = self.offset
}
}
func headerRotation(_ scrollOffset: CGFloat) -> Angle {
if scrollOffset < self.threshold * 0.60 {
return .degrees(0)
} else {
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))
}
}
// 位置固定不变的view
struct FixedPositionView: View {
var body: some View {
GeometryReader { proxy in
Color
.clear
.preference(key: RefreshPreferenceTypes.RefreshPreferenceKey.self,
value: [RefreshPreferenceTypes.RefreshPreferenceData(viewType: .fixedPositionView, bounds: proxy.frame(in: .global))])
}
}
}
// 位置随着滑动变化的view,高度为0
struct MovingPositionView: View {
var body: some View {
GeometryReader { proxy in
Color
.clear
.preference(key: RefreshPreferenceTypes.RefreshPreferenceKey.self,
value: [RefreshPreferenceTypes.RefreshPreferenceData(viewType: .movingPositionView, bounds: proxy.frame(in: .global))])
}
.frame(height: 0)
}
}
struct RefreshHeader: View {
var height: CGFloat
var loading: Bool
var frozen: Bool
var rotation: Angle
var updateTime: Date
let dateFormatter: DateFormatter = {
let df = DateFormatter()
df.dateFormat = "MM月dd日 HH时mm分ss秒"
return df
}()
var body: some View {
HStack(spacing: 20) {
Spacer()
Group {
if self.loading {
VStack {
Spacer()
ActivityRep()
Spacer()
}
} else {
Image(systemName: "arrow.down")
.resizable()
.aspectRatio(contentMode: .fit)
.rotationEffect(rotation)
}
}
.frame( height * 0.25, height: height * 0.8)
.fixedSize()
.offset(y: (loading && frozen) ? 0 : -height)
VStack(spacing: 5) {
Text("(self.loading ? "正在刷新数据" : "下拉刷新数据")")
.foregroundColor(.secondary)
.font(.subheadline)
Text("(self.dateFormatter.string(from: updateTime))")
.foregroundColor(.secondary)
.font(.subheadline)
}
.offset(y: -height + (loading && frozen ? +height : 0.0))
Spacer()
}
.frame(height: height)
}
}
}
struct RefreshPreferenceTypes {
enum ViewType: Int {
case fixedPositionView
case movingPositionView
}
struct RefreshPreferenceData: Equatable {
let viewType: ViewType
let bounds: CGRect
}
struct RefreshPreferenceKey: PreferenceKey {
static var defaultValue: [RefreshPreferenceData] = []
static func reduce(value: inout [RefreshPreferenceData],
nextValue: () -> [RefreshPreferenceData]) {
value.append(contentsOf: nextValue())
}
}
}
struct ActivityRep: UIViewRepresentable {
func makeUIView(context: UIViewRepresentableContext<ActivityRep>) -> UIActivityIndicatorView {
return UIActivityIndicatorView()
}
func updateUIView(_ uiView: UIActivityIndicatorView, context: UIViewRepresentableContext<ActivityRep>) {
uiView.startAnimating()
}
}