PullRefreshScrollView.swift 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168
  1. //
  2. // PullRefreshScrollView.swift
  3. // fiveConstant
  4. //
  5. // Created by 李建 on 2023/1/31.
  6. //
  7. import SwiftUI
  8. import Foundation
  9. struct RefreshableScrollView<Content: View>: View {
  10. @State private var previousScrollOffset: CGFloat = 0
  11. @State private var scrollOffset: CGFloat = 0
  12. @State private var frozen: Bool = false
  13. @State private var rotation: Angle = .degrees(0)
  14. var threshold: CGFloat = 80
  15. @Binding var refreshing: Bool
  16. let content: Content
  17. init(height: CGFloat = 80, refreshing: Binding<Bool>, @ViewBuilder content: () -> Content) {
  18. self.threshold = height
  19. self._refreshing = refreshing
  20. self.content = content()
  21. }
  22. var body: some View {
  23. return VStack {
  24. ScrollView {
  25. ZStack(alignment: .top) {
  26. MovingView()
  27. VStack { self.content }.alignmentGuide(.top, computeValue: { d in (self.refreshing && self.frozen) ? -self.threshold : 0.0 })
  28. SymbolView(height: self.threshold, loading: self.refreshing, frozen: self.frozen, rotation: self.rotation)
  29. }
  30. }
  31. .background(FixedView())
  32. .onPreferenceChange(RefreshableKeyTypes.PrefKey.self) { values in
  33. self.refreshLogic(values: values)
  34. }
  35. }
  36. }
  37. func refreshLogic(values: [RefreshableKeyTypes.PrefData]) {
  38. DispatchQueue.main.async {
  39. // Calculate scroll offset
  40. let movingBounds = values.first { $0.vType == .movingView }?.bounds ?? .zero
  41. let fixedBounds = values.first { $0.vType == .fixedView }?.bounds ?? .zero
  42. self.scrollOffset = movingBounds.minY - fixedBounds.minY
  43. self.rotation = self.symbolRotation(self.scrollOffset)
  44. // Crossing the threshold on the way down, we start the refresh process
  45. if !self.refreshing && (self.scrollOffset > self.threshold && self.previousScrollOffset <= self.threshold) {
  46. self.refreshing = true
  47. }
  48. if self.refreshing {
  49. // Crossing the threshold on the way up, we add a space at the top of the scrollview
  50. if self.previousScrollOffset > self.threshold && self.scrollOffset <= self.threshold {
  51. self.frozen = true
  52. }
  53. } else {
  54. // remove the sapce at the top of the scroll view
  55. self.frozen = false
  56. }
  57. // Update last scroll offset
  58. self.previousScrollOffset = self.scrollOffset
  59. }
  60. }
  61. func symbolRotation(_ scrollOffset: CGFloat) -> Angle {
  62. // We will begin rotation, only after we have passed
  63. // 60% of the way of reaching the threshold.
  64. if scrollOffset < self.threshold * 0.60 {
  65. return .degrees(0)
  66. } else {
  67. // Calculate rotation, based on the amount of scroll offset
  68. let h = Double(self.threshold)
  69. let d = Double(scrollOffset)
  70. let v = max(min(d - (h * 0.6), h * 0.4), 0)
  71. return .degrees(180 * v / (h * 0.4))
  72. }
  73. }
  74. struct SymbolView: View {
  75. var height: CGFloat
  76. var loading: Bool
  77. var frozen: Bool
  78. var rotation: Angle
  79. var body: some View {
  80. Group {
  81. if self.loading { // If loading, show the activity control
  82. VStack {
  83. Spacer()
  84. ActivityRep()
  85. Spacer()
  86. }.frame(height: height).fixedSize()
  87. .offset(y: -height + (self.loading && self.frozen ? height : 0.0))
  88. } else {
  89. Image(systemName: "arrow.down") // If not loading, show the arrow
  90. .resizable()
  91. .aspectRatio(contentMode: .fit)
  92. .frame(width: height * 0.25, height: height * 0.25).fixedSize()
  93. .padding(height * 0.375)
  94. .rotationEffect(rotation)
  95. .offset(y: -height + (loading && frozen ? +height : 0.0))
  96. }
  97. }
  98. }
  99. }
  100. struct MovingView: View {
  101. var body: some View {
  102. GeometryReader { proxy in
  103. Color.clear.preference(key: RefreshableKeyTypes.PrefKey.self, value: [RefreshableKeyTypes.PrefData(vType: .movingView, bounds: proxy.frame(in: .global))])
  104. }.frame(height: 0)
  105. }
  106. }
  107. struct FixedView: View {
  108. var body: some View {
  109. GeometryReader { proxy in
  110. Color.clear.preference(key: RefreshableKeyTypes.PrefKey.self, value: [RefreshableKeyTypes.PrefData(vType: .fixedView, bounds: proxy.frame(in: .global))])
  111. }
  112. }
  113. }
  114. }
  115. struct RefreshableKeyTypes {
  116. enum ViewType: Int {
  117. case movingView
  118. case fixedView
  119. }
  120. struct PrefData: Equatable {
  121. let vType: ViewType
  122. let bounds: CGRect
  123. }
  124. struct PrefKey: PreferenceKey {
  125. static var defaultValue: [PrefData] = []
  126. static func reduce(value: inout [PrefData], nextValue: () -> [PrefData]) {
  127. value.append(contentsOf: nextValue())
  128. }
  129. typealias Value = [PrefData]
  130. }
  131. }
  132. struct ActivityRep: UIViewRepresentable {
  133. func makeUIView(context: UIViewRepresentableContext<ActivityRep>) -> UIActivityIndicatorView {
  134. return UIActivityIndicatorView()
  135. }
  136. func updateUIView(_ uiView: UIActivityIndicatorView, context: UIViewRepresentableContext<ActivityRep>) {
  137. uiView.startAnimating()
  138. }
  139. }