【SwiftUI】下拉刷新

本文原创,在本人的CSDN发过一次。

由于目前swiftui未提供Scrollview的实现,我在网上找到了一个swiftuiLab出得代码,利用滚动偏移实现的下拉刷新。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
// 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()
}
}

使用很简单:设置偏移,设置绑定的刷新状态就行了。

1
RefreshableScrollView(height: 70, refreshing: self.$articleList.loading)