写三行代码搞定SwiftUI中的上拉刷新和加载更多

1,110 阅读1分钟

这几天基于SwitUIIntrospect, MJRefresh 封装了一个刷新控件,我们知道,刷新逻辑其实挺多的,比如

  1. page的递增,数组拼接
  2. 下拉刷新后,加载更多控件(footer)要恢复初始状态
  3. Cell不多时,footer应隐藏
  4. 没有更多数据时应显示No More Data状态

现在只需写三行代码:

struct TestView: View {
    @StateObject var refresher = refreshManager<ItemModel>(firstPage: 1, pageSize: 20)//第一行

    var body: some View {
        ScrollView {
            LazyVStack {
                ForEach(refresher.items, id: \.id) { item in
                    ...
                }
            }
        }
        .refreshable(refresher: refresher, refreshBlock: {fistPage, pageSize in
            return try await fetchDataFromServer(page: firstPage, size: pageSize)//第二行
        }, loadMoreBlock: {currentPage, pageSize in
            return try await fetchDataFromServer(page: currentPage, size: pageSize)//第三行
        })
    }
}

封装源码如下:

//
//  RefreshManager.swift
//  SwiftUITools
//
//  Created by weijie.zhou on 2024/4/5.
//

import SwiftUI
import MJRefresh

class RefreshManager<T>: ObservableObject {
    private(set) var firstPage: Int = 1
    lazy private(set) var currentPage: Int = firstPage
    private(set) var pageSize: Int = 20
    @Published var items: [T] = [] {
        didSet {
            if items.count < self.pageSize {
                self.footer.isHidden = true
            } else {
                self.footer.isHidden = false
                self.footer.state = .idle
            }
        }
    }
    var refreshBlock: RefreshBlock?
    var loadMoreBlock: LoadMoreBlock?
    lazy private(set) var header = MJRefreshNormalHeader(refreshingTarget: self, refreshingAction: #selector(refresh))
    lazy private(set) var footer = {
        let footer = MJRefreshBackNormalFooter(refreshingTarget: self, refreshingAction: #selector(loadMore))
        return footer
    }()
    
    typealias RefreshBlock = (_ firstPage: Int, _ pageSize: Int) async throws -> [T]
    typealias LoadMoreBlock = (_ currentPage: Int, _ pageSize: Int) async throws -> [T]
    
    init(firstPage: Int, pageSize: Int) {
        self.firstPage = firstPage
        self.pageSize = pageSize
    }
    
    @objc private func refresh() {
        Task { @MainActor in
            do {
                let items = try await self.refreshBlock?(self.firstPage, self.pageSize) ?? []
                header.endRefreshing { [weak self] in
                    guard let self = self else {return}
                    self.items = items
                    self.currentPage = self.firstPage
                }
            } catch {
                await header.endRefreshing()
            }
        }
    }
    
    @objc private func loadMore() {
        guard let loadMoreBlock = loadMoreBlock else {return}
        Task { @MainActor in
            do {
                let items = try await loadMoreBlock(currentPage, pageSize)
                if items.count > 0 {
                    footer.endRefreshing(completionBlock: { [weak self] in
                        guard let self = self else {return}
                        self.currentPage += 1
                        self.items.append(contentsOf: items)
                    })
                } else {
                    footer.endRefreshingWithNoMoreData()
                }
            } catch {
                await footer.endRefreshing()
            }
        }
    }
}

extension List {
    func refreshable<T>(refresher: RefreshManager<T>, refreshBlock: RefreshManager<T>.RefreshBlock?, loadMoreBlock: RefreshManager<T>.LoadMoreBlock?) -> some View {
        self
            .introspect(.list, on: .iOS(.v16,.v17), customize: { cv in
                cv.mj_header = refresher.header
                cv.mj_footer = refresher.footer
            })
            .task {
                refresher.refreshBlock = refreshBlock
                refresher.loadMoreBlock = loadMoreBlock
                Task {@MainActor in
                    do {
                        refresher.items = try await refresher.refreshBlock?(refresher.firstPage, refresher.pageSize) ?? []
                    } catch {
                        refresher.items = []
                    }
                }
            }
    }
}

extension ScrollView {
    func refreshable<T>(refresher: RefreshManager<T>, refreshBlock: RefreshManager<T>.RefreshBlock?, loadMoreBlock: RefreshManager<T>.LoadMoreBlock?) -> some View {
        self.introspect(.scrollView, on: .iOS(.v16,.v17), customize: { sv in
            sv.mj_header = refresher.header
            sv.mj_footer = refresher.footer
        })
        .task {
            refresher.refreshBlock = refreshBlock
            refresher.loadMoreBlock = loadMoreBlock
            Task {@MainActor in
                do {
                    refresher.items = try await refresher.refreshBlock?(refresher.firstPage, refresher.pageSize) ?? []
                } catch {
                    refresher.items = []
                }
            }
        }
    }
}