SwiftUI封裝(二)—— SwiftUI自定義下拉刷新、上拉加載更多控件(LKRefreshView)

LKRefreshView_SwiftUI

介紹

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

效果

下拉刷新
效果示例

LKRefreshView_SwiftUI

設計架構

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
        }
    }

安裝教程

  1. 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

參考來源

  1. 參考來源:swiftui-pull-to-refresh
  2. 解讀參考:原理解讀
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容