LKRefreshView_SwiftUI
介紹
LKRefreshView是純SwiftUI自定義的下拉刷新,上拉加載更多列表刷新控件,支持ScrollView列表快速對接
效果

下拉刷新

效果示例
設計架構
1.獲取ScrollView的內容高度;
2.計算滑動偏移offsetY值與刷新事件的閾值對比回調刷新觸發(fā)事件;
3.自定義滑動時的header、footer顯示內容效果。
一、獲取ScrollView的內容高度
//
// LKChildSizeReader.swift
// ZgwBosssCockpit
//
// Created by 李棒棒 on 2023/12/20.
//
import SwiftUI
struct LKChildSizeReader<Content: View>: View {
@Binding var size:CGSize
let content:()->Content
var body: some View {
ZStack {
content()
.background(
GeometryReader { proxy in
Color.clear
.preference(key: LKSizePreferenceKey.self, value: proxy.size)
}
)
}
.onPreferenceChange(LKSizePreferenceKey.self) { preferences in
self.size = preferences
}
}
}
//計算內容size
private struct LKSizePreferenceKey: PreferenceKey {
typealias Value = CGSize
static var defaultValue: Value = .zero
static func reduce(value _: inout Value, nextValue: () -> Value) {
_ = nextValue()
}
}
//計算滑動偏移量
struct LKScrollOffsetPreferenceKey: PreferenceKey {
static var defaultValue = CGFloat.zero
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
value = -(value + nextValue())
}
}
二、計算垂直方向滑動偏移量得到offsetY與刷新觸發(fā)閾值對比
有兩種方案:
第一種:使用第一步中的LKScrollOffsetPreferenceKey來獲取滑動時的偏移量;
第二種:通過計算LKMovingPositionView和LKFixedPositionView兩者之間的y的差,得到offset;
1.LKFixedPositionView的代碼
通過.preference為其綁定了一個LKRefreshPreferenceData類型的數據,最重要的目的是保存該view的bounds
import SwiftUI
//固定位置view
struct LKFixedPositionView: View {
var body: some View {
GeometryReader { proxy in
Color.clear
.preference(key: LKRefreshPreferenceType.LKRefreshPreferenceKey.self,
value: [LKRefreshPreferenceType.LKRefreshPreferenceData(viewType: .fixedPositionView, bounds: proxy.frame(in: .global))])
}
}
}
2.LKMovingPositionView的代碼
/// 位置隨著滑動變化的view,高度為0
struct LKMovingPositionView: View {
var body: some View {
GeometryReader { proxy in
Color.clear
.preference(key: LKRefreshPreferenceType.LKRefreshPreferenceKey.self, value: [LKRefreshPreferenceType.LKRefreshPreferenceData(viewType: .movingPositionView, bounds: proxy.frame(in: .global))])
}.frame(height:0.0)
}
}
這兩個view對用戶來說都是不可見的,一個作為背景,另一個放到ScrollView內容的最上邊
3.計算offset
extension LKRefreshView {
func calculate(_ values: [LKRefreshPreferenceType.LKRefreshPreferenceData]) {
DispatchQueue.main.async {
/// 計算sroll 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
if (self.offset >= 0.0) {//下拉
self.headerRotation = self.headerRotation(self.offset)
} else {//上拽
self.footerRotation = self.footerRotation(self.offset)
}
/// 觸發(fā)刷新
if self.headerRefreshing == false ,
self.offset > self.threshold ,
self.preOffset <= self.threshold {
self.footerRefreshing = false
self.footerFrozen = false
self.headerRefreshing = true
if refreshTrigger != nil {
refreshTrigger!()
}
}
if self.headerRefreshing {
if self.preOffset > self.threshold,
self.offset <= self.threshold {
self.headerFrozen = true
}
} else {
self.headerFrozen = false
}
self.preOffset = self.offset
print("滑動位置偏移:\(self.offset)")
//加載更多觸發(fā)條件
//print("內容高度\(listContentH)","列表物理高度:\(listSizeH)", "當前偏移量\(-(self.preOffset - listSizeH))")
if self.footerRefreshing == false,
self.footerFrozen == false,
self.preOffset < 0.0,
listContentH > 0.0 ,
(listContentH > listSizeH ? ((listContentH + threshold) <= abs(self.preOffset - listSizeH)) : abs(self.preOffset) > self.threshold) {
//if self.footerRefreshing == false && ((listContentH + threshold) <= -(self.preOffset - listSizeH)) && listContentH > 0.0 {
self.headerRefreshing = false
self.headerFrozen = false
self.footerRefreshing = true
if footerHidden == false {//底部控件未隱藏,允許上拉回調
if moreTrigger != nil {
moreTrigger!()
}
}
}
if self.footerRefreshing {
if listContentH > listSizeH ? ((listContentH + threshold) <= -(self.preOffset - listSizeH)) : (abs(self.preOffset) > threshold){
//if ((listContentH + threshold) <= -(self.preOffset - listSizeH)) {
self.footerFrozen = true
}
} else {
self.footerFrozen = false
}
}
}
}
三、實現自定義的header、footer控件效果
目前定義了兩種樣式,菊花和自定義的loading圖效果,可以根據需求修改
struct HeaderConfig {
var indicatorStyle:LKRefreshUIStyle = .indicator
var titleColor:Color = .gray
var titleFont:Font = Font.system(size: 16.0,weight: .regular)
var indicatorColor:Color = .gray
var refreshingTitle:String = "正在刷新數據"
var willRefreshTitle:String = "下拉刷新數據"
var dateFormatter: String
/*
let dateFormatter: DateFormatter = {
let df = DateFormatter()
df.dateFormat = "MM月dd日 HH時mm分ss秒"
return df
}()
*/
init(indicatorStyle: LKRefreshUIStyle = .indicator,
titleColor: Color = .gray,
titleFont: Font = Font.system(size: 16.0,weight: .regular),
indicatorColor: Color = .gray,
refreshingTitle: String = "正在刷新數據",
willRefreshTitle: String = "下拉刷新數據",
dateFormatter: String = "上次更新 MM-dd HH:mm") {
self.indicatorStyle = indicatorStyle
self.titleColor = titleColor
self.titleFont = titleFont
self.indicatorColor = indicatorColor
self.refreshingTitle = refreshingTitle
self.willRefreshTitle = willRefreshTitle
self.dateFormatter = dateFormatter
}
}
struct FooterConfig {
var indicatorStyle:LKRefreshUIStyle = .indicator
var titleColor:Color
var titleFont:Font
var indicatorColor:Color
var refreshingTitle:String
var willRefreshTitle:String
init(indicatorStyle:LKRefreshUIStyle = .indicator,
titleColor: Color = .gray,
titleFont: Font = Font.system(size: 16.0,weight: .regular),
indicatorColor: Color = .gray,
refreshingTitle: String = "正在加載更多數據",
willRefreshTitle: String = "上拉加載更多") {
self.indicatorStyle = indicatorStyle
self.titleColor = titleColor
self.titleFont = titleFont
self.indicatorColor = indicatorColor
self.refreshingTitle = refreshingTitle
self.willRefreshTitle = willRefreshTitle
}
}
安裝教程
- xxxx
調用參數說明
var threshold: CGFloat = 120 //觸發(fā)的臨界高度
/// 下拉刷新
@Binding var headerRefreshing: Bool
/// 加載更多
@Binding var footerRefreshing: Bool
///是否隱藏頭部刷新控件 默認false 不隱藏
var headerHidden: Bool
///是否隱藏尾部刷新控件 默認false 不隱藏
var footerHidden: Bool
///配置 頭部刷新控件樣式配置
var headerConfig:LKRefresh.HeaderConfig
///配置 尾部刷新控件樣式配置
var footerConfig:LKRefresh.FooterConfig
// 下拉刷新出發(fā)回調
var refreshTrigger: (() -> Void)?
// 上拉加載更多回調
var moreTrigger: (() -> Void)?
let content: Content
init(_ threshold: CGFloat = 120,
headerRefreshing: Binding<Bool>,
footerRefreshing: Binding<Bool>,
headerHidden: Bool = false,
footerHidden: Bool = false,
headerConfig:LKRefresh.HeaderConfig = LKRefresh.HeaderConfig(),
footerConfig:LKRefresh.FooterConfig = LKRefresh.FooterConfig(),
refreshTrigger: @escaping () -> Void,
moreTrigger: @escaping () -> Void,
@ViewBuilder content: () -> Content) {
self.threshold = threshold
self._headerRefreshing = headerRefreshing
self._footerRefreshing = footerRefreshing
self.headerHidden = headerHidden
self.footerHidden = footerHidden
self.headerConfig = headerConfig
self.footerConfig = footerConfig
self.refreshTrigger = refreshTrigger
self.moreTrigger = moreTrigger
self.content = content()
}
LKRefreshView_SwiftUI
參考來源
- 參考來源:swiftui-pull-to-refresh
- 解讀參考:原理解讀