这几天基于SwitUIIntrospect, MJRefresh 封装了一个刷新控件,我们知道,刷新逻辑其实挺多的,比如
- page的递增,数组拼接
- 下拉刷新后,加载更多控件(footer)要恢复初始状态
- Cell不多时,footer应隐藏
- 没有更多数据时应显示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 = []
}
}
}
}
}