SwiftUI 下拉刷新数据
Pull down to refresh data in SwiftUI
我使用 List
使用了简单的数据列表。我想添加下拉刷新功能,但我不确定哪种方法最好。
下拉刷新视图仅在用户尝试从第一个索引下拉时可见,就像我们在 UITableView
和 UIRefreshControl
在 UIKit
[=16 中所做的一样=]
这是在 SwiftUI
.
中列出数据的简单代码
struct CategoryHome: View {
var categories: [String: [Landmark]] {
.init(
grouping: landmarkData,
by: { [=10=].category.rawValue }
)
}
var body: some View {
NavigationView {
List {
ForEach(categories.keys.sorted().identified(by: \.self)) { key in
Text(key)
}
}
.navigationBarTitle(Text("Featured"))
}
}
}
我正在玩的应用程序需要同样的东西,看起来 SwiftUI API 目前不包含 ScrollView
的刷新控制功能。
随着时间的推移,API 将开发并纠正这些情况,但 SwiftUI 中缺少功能的一般回退将始终实现实现 UIViewRepresentable
的结构。这是用于 UIScrollView
的带有刷新控件的快速而肮脏的。
struct LegacyScrollView : UIViewRepresentable {
// any data state, if needed
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIView(context: Context) -> UIScrollView {
let control = UIScrollView()
control.refreshControl = UIRefreshControl()
control.refreshControl?.addTarget(context.coordinator, action:
#selector(Coordinator.handleRefreshControl),
for: .valueChanged)
// Simply to give some content to see in the app
let label = UILabel(frame: CGRect(x: 0, y: 0, width: 200, height: 30))
label.text = "Scroll View Content"
control.addSubview(label)
return control
}
func updateUIView(_ uiView: UIScrollView, context: Context) {
// code to update scroll view from view state, if needed
}
class Coordinator: NSObject {
var control: LegacyScrollView
init(_ control: LegacyScrollView) {
self.control = control
}
@objc func handleRefreshControl(sender: UIRefreshControl) {
// handle the refresh event
sender.endRefreshing()
}
}
}
但是,当然,如果不将它们包装在 UIHostingController
中并将它们放入 makeUIView
而不是将它们放在 [=16] 中,则不能在滚动视图中使用任何 SwiftUI 组件=].
你好,看看我制作的这个库:https://github.com/AppPear/SwiftUI-PullToRefresh
一行代码即可实现:
struct CategoryHome: View {
var categories: [String: [Landmark]] {
.init(
grouping: landmarkData,
by: { [=10=].category.rawValue }
)
}
var body: some View {
RefreshableNavigationView(title: "Featured", action:{
// your refresh action
}){
ForEach(categories.keys.sorted().identified(by: \.self)) { key in
Text(key)
Divider() // !!! this is important to add cell separation
}
}
}
}
}
这是一个内省视图层次结构并将适当的 UIRefreshControl
添加到 SwiftUI 列表的 table 视图的实现:https://github.com/timbersoftware/SwiftUIRefresh
可以在此处找到大部分内省逻辑:https://github.com/timbersoftware/SwiftUIRefresh/blob/15d9deed3fec66e2c0f6fd1fd4fe966142a891db/Sources/PullToRefresh.swift#L39-L73
masOS 尚不支持 swiftui-introspects,因此如果您要构建适用于 iOS 和 macOS 的 UI,请考虑 Samu Andras 库。
我分叉了他的代码,添加了一些增强功能,并添加了在没有 NavigationView 的情况下使用的功能
这里是示例代码。
RefreshableList(showRefreshView: $showRefreshView, action:{
// Your refresh action
// Remember to set the showRefreshView to false
self.showRefreshView = false
}){
ForEach(self.numbers, id: \.self){ number in
VStack(alignment: .leading){
Text("\(number)")
Divider()
}
}
}
更多详情,您可以访问下面的link。
https://github.com/phuhuynh2411/SwiftUI-PullToRefresh
这是我制作的 simple、small 和 pure SwiftUI 解决方案向 ScrollView 添加下拉刷新功能。
struct PullToRefresh: View {
var coordinateSpaceName: String
var onRefresh: ()->Void
@State var needRefresh: Bool = false
var body: some View {
GeometryReader { geo in
if (geo.frame(in: .named(coordinateSpaceName)).midY > 50) {
Spacer()
.onAppear {
needRefresh = true
}
} else if (geo.frame(in: .named(coordinateSpaceName)).maxY < 10) {
Spacer()
.onAppear {
if needRefresh {
needRefresh = false
onRefresh()
}
}
}
HStack {
Spacer()
if needRefresh {
ProgressView()
} else {
Text("⬇️")
}
Spacer()
}
}.padding(.top, -50)
}
}
要使用它很简单,只需将它添加到 ScrollView 的顶部并为其指定 ScrollView 的坐标 space :
ScrollView {
PullToRefresh(coordinateSpaceName: "pullToRefresh") {
// do your stuff when pulled
}
Text("Some view...")
}.coordinateSpace(name: "pullToRefresh")
我尝试了很多不同的解决方案,但没有一个对我的情况有效。
GeometryReader
基于解决方案的复杂布局性能不佳。
这是一个看起来运行良好的纯 SwiftUI 2.0 视图,不会通过持续状态更新降低滚动性能并且不使用任何 UIKit hacks:
import SwiftUI
struct PullToRefreshView: View
{
private static let minRefreshTimeInterval = TimeInterval(0.2)
private static let triggerHeight = CGFloat(100)
private static let indicatorHeight = CGFloat(100)
private static let fullHeight = triggerHeight + indicatorHeight
let backgroundColor: Color
let foregroundColor: Color
let isEnabled: Bool
let onRefresh: () -> Void
@State private var isRefreshIndicatorVisible = false
@State private var refreshStartTime: Date? = nil
init(bg: Color = .white, fg: Color = .black, isEnabled: Bool = true, onRefresh: @escaping () -> Void)
{
self.backgroundColor = bg
self.foregroundColor = fg
self.isEnabled = isEnabled
self.onRefresh = onRefresh
}
var body: some View
{
VStack(spacing: 0)
{
LazyVStack(spacing: 0)
{
Color.clear
.frame(height: Self.triggerHeight)
.onAppear
{
if isEnabled
{
withAnimation
{
isRefreshIndicatorVisible = true
}
refreshStartTime = Date()
}
}
.onDisappear
{
if isEnabled, isRefreshIndicatorVisible, let diff = refreshStartTime?.distance(to: Date()), diff > Self.minRefreshTimeInterval
{
onRefresh()
}
withAnimation
{
isRefreshIndicatorVisible = false
}
refreshStartTime = nil
}
}
.frame(height: Self.triggerHeight)
indicator
.frame(height: Self.indicatorHeight)
}
.background(backgroundColor)
.ignoresSafeArea(edges: .all)
.frame(height: Self.fullHeight)
.padding(.top, -Self.fullHeight)
}
private var indicator: some View
{
ProgressView()
.progressViewStyle(CircularProgressViewStyle(tint: foregroundColor))
.opacity(isRefreshIndicatorVisible ? 1 : 0)
}
}
当它进入或离开屏幕边界时,它使用带负填充的 LazyVStack
在触发视图 Color.clear
上调用 onAppear
和 onDisappear
。
如果触发视图出现和消失之间的时间大于 minRefreshTimeInterval
以允许 ScrollView
反弹而不触发刷新,则会触发刷新。
要使用它,请将 PullToRefreshView
添加到 ScrollView
的顶部:
import SwiftUI
struct RefreshableScrollableContent: View
{
var body: some View
{
ScrollView
{
VStack(spacing: 0)
{
PullToRefreshView { print("refreshing") }
// ScrollView content
}
}
}
}
要点:https://gist.github.com/tkashkin/e5f6b65b255b25269d718350c024f550
老实说,none 个评分最高的答案确实很适合我的场景。该场景在 ScrollView
和自定义 LoadingView
之间切换。每次我从 LoadingView
切换到使用遗留 UIScrollView
使用 UIViewRepresentable
创建的 ScrollView
时, contentSize
就会变得一团糟。
因此,作为一种解决方案,我创建了一个库,以便它对所有试图为这种简单问题寻找解决方案的开发人员都有用。我从互联网上浏览了许多网站,并最终调整了解决方案,最终为我提供了最佳解决方案。
步骤
- 将
SPM
https://github.com/bibinjacobpulickal/BBRefreshableScrollView 添加到您的项目。
import BBRefreshableScrollView
到所需的文件。
- 更新
View
的 body
。
struct CategoryHome: View {
...
var body: some View {
NavigationView {
BBRefreshableScrollView { completion in
// do refreshing stuff here
} content: {
ForEach(categories.keys.sorted().identified(by: \.self)) { key in
Text(key)
}
}
.navigationBarTitle(Text("Featured"))
}
}
}
更多详情可以关注Readme.
这是一种使用 ScrollView、GeometryReader 和 PreferenceKey 的纯 SwiftUI 方法
我能够读取 ScrollView 中的滚动偏移量,一旦它高于阈值,我就可以执行操作
import SwiftUI
struct RefreshableView<Content:View>: View {
init(action: @escaping () -> Void, @ViewBuilder content: @escaping () -> Content) {
self.content = content
self.refreshAction = action
}
var body: some View {
GeometryReader { geometry in
ScrollView {
content()
.anchorPreference(key: OffsetPreferenceKey.self, value: .top) {
geometry[[=10=]].y
}
}
.onPreferenceChange(OffsetPreferenceKey.self) { offset in
if offset > threshold {
refreshAction()
}
}
}
}
private var content: () -> Content
private var refreshAction: () -> Void
private let threshold:CGFloat = 50.0
}
fileprivate struct OffsetPreferenceKey: PreferenceKey {
static var defaultValue: CGFloat = 0
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
value = nextValue()
}
}
这是RefreshableView 的用法示例。
进度指示器不包含在 RefreshableView 中,它只提供一个 GeometryReader 和一个包含您要刷新的内容的 ScrollView。您需要提供 ProgressView 或其他视图来显示加载正在进行中。
它不适用于 List,但您可以改用 ForEach,内容将滚动,这要归功于 ScrollView
RefreshableView(action: {
viewModel.refreshFeed(forceReload: true)
}) {
if viewModel.showProgressView {
VStack {
ProgressView()
Text("reloading feed...")
.font(Font.caption2)
}
}
ForEach(viewModel.feed.entries) { entry in
viewForEntry(entry)
}
}
完整示例可在 GitHub
上找到
来自 iOS 15+
NavigationView {
List(1..<100) { row in
Text("Row \(row)")
}
.refreshable {
print("write your pull to refresh logic here")
}
}
更多详情:Apple Doc
这个呢
import SwiftUI
public struct FreshScrollView<Content>: View where Content : View{
private let content: () -> Content
private let action: () -> Void
init(@ViewBuilder content: @escaping () -> Content, action: @escaping ()-> Void){
self.content = content
self.action = action
}
@State var startY: Double = 10000
public var body: some View{
ScrollView{
GeometryReader{ geometry in
HStack {
Spacer()
if geometry.frame(in: .global) .minY - startY > 30{
ProgressView()
.padding(.top, -30)
.animation(.easeInOut)
.transition(.opacity)
.onAppear{
let noti = UIImpactFeedbackGenerator(style: .light)
noti.prepare()
noti.impactOccurred()
action()
}
}
Spacer()
}
.onAppear {
startY = geometry.frame(in:.global).minY
}
}
content()
}
}
}
#if DEBUG
struct FreshScrollView_Previews: PreviewProvider {
static var previews: some View {
FreshScrollView {
Text("A")
Text("B")
Text("C")
Text("D")
} action: {
print("text")
}
}
}
#endif
我知道最初的问题是针对列表的,但这里是 ScrollView
和 LazyVStack
的代码,因为有时列表不合适。
import SwiftUI
struct PullToRefreshSwiftUI: View {
@Binding private var needRefresh: Bool
private let coordinateSpaceName: String
private let onRefresh: () -> Void
init(needRefresh: Binding<Bool>, coordinateSpaceName: String, onRefresh: @escaping () -> Void) {
self._needRefresh = needRefresh
self.coordinateSpaceName = coordinateSpaceName
self.onRefresh = onRefresh
}
var body: some View {
HStack(alignment: .center) {
if needRefresh {
VStack {
Spacer()
ProgressView()
Spacer()
}
.frame(height: 100)
}
}
.background(GeometryReader {
Color.clear.preference(key: ScrollViewOffsetPreferenceKey.self,
value: [=10=].frame(in: .named(coordinateSpaceName)).origin.y)
})
.onPreferenceChange(ScrollViewOffsetPreferenceKey.self) { offset in
guard !needRefresh else { return }
if abs(offset) > 50 {
needRefresh = true
onRefresh()
}
}
}
}
struct ScrollViewOffsetPreferenceKey: PreferenceKey {
typealias Value = CGFloat
static var defaultValue = CGFloat.zero
static func reduce(value: inout Value, nextValue: () -> Value) {
value += nextValue()
}
}
这是典型用法:
struct ContentView: View {
@State private var refresh: Bool = false
@State private var itemList: [Int] = {
var array = [Int]()
(0..<40).forEach { value in
array.append(value)
}
return array
}()
var body: some View {
ScrollView {
PullToRefreshSwiftUI(needRefresh: $refresh,
coordinateSpaceName: "pullToRefresh") {
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
withAnimation { refresh = false }
}
}
LazyVStack {
ForEach(itemList, id: \.self) { item in
HStack {
Spacer()
Text("\(item)")
Spacer()
}
}
}
}
.coordinateSpace(name: "pullToRefresh")
}
}
我对 SwiftUI 中的下拉刷新选项非常不满意。即使在使用基于内省 UIKit 的解决方案时,在使用导航视图时也会出现奇怪的行为和一些视图跳跃。我还需要一些更可定制的东西,所以我写了一个库。它是下拉刷新控件的(几乎)纯 SwiftUI 实现,它适用于 iOS 14 和 15。
https://github.com/gh123man/SwiftUI-Refresher
它是高度可定制的,并支持一些额外的功能,例如能够覆盖视图内容(在您有静态 header 的情况下)。
由于 swiftUi 最好使用纯 swiftUi,正如@Anatoliy Kashkin 在他的回答中提到的那样,所以我更新了他的回答以支持 enable/disable 从视图外部和内部刷新。
创建PullToRefresh
结构视图:
import Foundation
import SwiftUI
struct PullToRefresh: View {
@Binding var isRefreshing:Bool
var coordinateSpaceName: String
var onRefresh: ()->Void
@State var needRefresh: Bool = false
var body: some View {
GeometryReader { geo in
if (geo.frame(in: .named(coordinateSpaceName)).midY > 50) {
Spacer()
.onAppear {
needRefresh = true
}
} else if (geo.frame(in: .named(coordinateSpaceName)).maxY < 10) {
Spacer()
.onAppear {
if needRefresh {
needRefresh = false
onRefresh()
}
}
}
HStack {
Spacer()
if needRefresh || isRefreshing {
ProgressView()
} else {
Image(systemName: "arrow.clockwise")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 20, height: 20)
.foregroundColor(.gray)
Text("Refresh")
.foregroundColor(.gray)
}
Spacer()
}
}.padding(.top, isRefreshing ? 0 : -50)
}
}
现在你可以像这样使用它了:
@State var isRefreshing:Bool = true
var body: some View {
ScrollView(){
PullToRefresh(isRefreshing: $isRefreshing, coordinateSpaceName: "pullToRefresh") {
fetchOrders()
}
VStack(alignment : .leading){
Text("Some view...")
}
}.coordinateSpace(name: "pullToRefresh")
通过设置 isRefreshing
true ,它将显示进度并在您完成 http 请求或更新数据等时将其设置为 false
我使用 List
使用了简单的数据列表。我想添加下拉刷新功能,但我不确定哪种方法最好。
下拉刷新视图仅在用户尝试从第一个索引下拉时可见,就像我们在 UITableView
和 UIRefreshControl
在 UIKit
[=16 中所做的一样=]
这是在 SwiftUI
.
struct CategoryHome: View {
var categories: [String: [Landmark]] {
.init(
grouping: landmarkData,
by: { [=10=].category.rawValue }
)
}
var body: some View {
NavigationView {
List {
ForEach(categories.keys.sorted().identified(by: \.self)) { key in
Text(key)
}
}
.navigationBarTitle(Text("Featured"))
}
}
}
我正在玩的应用程序需要同样的东西,看起来 SwiftUI API 目前不包含 ScrollView
的刷新控制功能。
随着时间的推移,API 将开发并纠正这些情况,但 SwiftUI 中缺少功能的一般回退将始终实现实现 UIViewRepresentable
的结构。这是用于 UIScrollView
的带有刷新控件的快速而肮脏的。
struct LegacyScrollView : UIViewRepresentable {
// any data state, if needed
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIView(context: Context) -> UIScrollView {
let control = UIScrollView()
control.refreshControl = UIRefreshControl()
control.refreshControl?.addTarget(context.coordinator, action:
#selector(Coordinator.handleRefreshControl),
for: .valueChanged)
// Simply to give some content to see in the app
let label = UILabel(frame: CGRect(x: 0, y: 0, width: 200, height: 30))
label.text = "Scroll View Content"
control.addSubview(label)
return control
}
func updateUIView(_ uiView: UIScrollView, context: Context) {
// code to update scroll view from view state, if needed
}
class Coordinator: NSObject {
var control: LegacyScrollView
init(_ control: LegacyScrollView) {
self.control = control
}
@objc func handleRefreshControl(sender: UIRefreshControl) {
// handle the refresh event
sender.endRefreshing()
}
}
}
但是,当然,如果不将它们包装在 UIHostingController
中并将它们放入 makeUIView
而不是将它们放在 [=16] 中,则不能在滚动视图中使用任何 SwiftUI 组件=].
你好,看看我制作的这个库:https://github.com/AppPear/SwiftUI-PullToRefresh
一行代码即可实现:
struct CategoryHome: View {
var categories: [String: [Landmark]] {
.init(
grouping: landmarkData,
by: { [=10=].category.rawValue }
)
}
var body: some View {
RefreshableNavigationView(title: "Featured", action:{
// your refresh action
}){
ForEach(categories.keys.sorted().identified(by: \.self)) { key in
Text(key)
Divider() // !!! this is important to add cell separation
}
}
}
}
}
这是一个内省视图层次结构并将适当的 UIRefreshControl
添加到 SwiftUI 列表的 table 视图的实现:https://github.com/timbersoftware/SwiftUIRefresh
可以在此处找到大部分内省逻辑:https://github.com/timbersoftware/SwiftUIRefresh/blob/15d9deed3fec66e2c0f6fd1fd4fe966142a891db/Sources/PullToRefresh.swift#L39-L73
masOS 尚不支持 swiftui-introspects,因此如果您要构建适用于 iOS 和 macOS 的 UI,请考虑 Samu Andras 库。
我分叉了他的代码,添加了一些增强功能,并添加了在没有 NavigationView 的情况下使用的功能
这里是示例代码。
RefreshableList(showRefreshView: $showRefreshView, action:{
// Your refresh action
// Remember to set the showRefreshView to false
self.showRefreshView = false
}){
ForEach(self.numbers, id: \.self){ number in
VStack(alignment: .leading){
Text("\(number)")
Divider()
}
}
}
更多详情,您可以访问下面的link。 https://github.com/phuhuynh2411/SwiftUI-PullToRefresh
这是我制作的 simple、small 和 pure SwiftUI 解决方案向 ScrollView 添加下拉刷新功能。
struct PullToRefresh: View {
var coordinateSpaceName: String
var onRefresh: ()->Void
@State var needRefresh: Bool = false
var body: some View {
GeometryReader { geo in
if (geo.frame(in: .named(coordinateSpaceName)).midY > 50) {
Spacer()
.onAppear {
needRefresh = true
}
} else if (geo.frame(in: .named(coordinateSpaceName)).maxY < 10) {
Spacer()
.onAppear {
if needRefresh {
needRefresh = false
onRefresh()
}
}
}
HStack {
Spacer()
if needRefresh {
ProgressView()
} else {
Text("⬇️")
}
Spacer()
}
}.padding(.top, -50)
}
}
要使用它很简单,只需将它添加到 ScrollView 的顶部并为其指定 ScrollView 的坐标 space :
ScrollView {
PullToRefresh(coordinateSpaceName: "pullToRefresh") {
// do your stuff when pulled
}
Text("Some view...")
}.coordinateSpace(name: "pullToRefresh")
我尝试了很多不同的解决方案,但没有一个对我的情况有效。
GeometryReader
基于解决方案的复杂布局性能不佳。
这是一个看起来运行良好的纯 SwiftUI 2.0 视图,不会通过持续状态更新降低滚动性能并且不使用任何 UIKit hacks:
import SwiftUI
struct PullToRefreshView: View
{
private static let minRefreshTimeInterval = TimeInterval(0.2)
private static let triggerHeight = CGFloat(100)
private static let indicatorHeight = CGFloat(100)
private static let fullHeight = triggerHeight + indicatorHeight
let backgroundColor: Color
let foregroundColor: Color
let isEnabled: Bool
let onRefresh: () -> Void
@State private var isRefreshIndicatorVisible = false
@State private var refreshStartTime: Date? = nil
init(bg: Color = .white, fg: Color = .black, isEnabled: Bool = true, onRefresh: @escaping () -> Void)
{
self.backgroundColor = bg
self.foregroundColor = fg
self.isEnabled = isEnabled
self.onRefresh = onRefresh
}
var body: some View
{
VStack(spacing: 0)
{
LazyVStack(spacing: 0)
{
Color.clear
.frame(height: Self.triggerHeight)
.onAppear
{
if isEnabled
{
withAnimation
{
isRefreshIndicatorVisible = true
}
refreshStartTime = Date()
}
}
.onDisappear
{
if isEnabled, isRefreshIndicatorVisible, let diff = refreshStartTime?.distance(to: Date()), diff > Self.minRefreshTimeInterval
{
onRefresh()
}
withAnimation
{
isRefreshIndicatorVisible = false
}
refreshStartTime = nil
}
}
.frame(height: Self.triggerHeight)
indicator
.frame(height: Self.indicatorHeight)
}
.background(backgroundColor)
.ignoresSafeArea(edges: .all)
.frame(height: Self.fullHeight)
.padding(.top, -Self.fullHeight)
}
private var indicator: some View
{
ProgressView()
.progressViewStyle(CircularProgressViewStyle(tint: foregroundColor))
.opacity(isRefreshIndicatorVisible ? 1 : 0)
}
}
当它进入或离开屏幕边界时,它使用带负填充的 LazyVStack
在触发视图 Color.clear
上调用 onAppear
和 onDisappear
。
如果触发视图出现和消失之间的时间大于 minRefreshTimeInterval
以允许 ScrollView
反弹而不触发刷新,则会触发刷新。
要使用它,请将 PullToRefreshView
添加到 ScrollView
的顶部:
import SwiftUI
struct RefreshableScrollableContent: View
{
var body: some View
{
ScrollView
{
VStack(spacing: 0)
{
PullToRefreshView { print("refreshing") }
// ScrollView content
}
}
}
}
要点:https://gist.github.com/tkashkin/e5f6b65b255b25269d718350c024f550
老实说,none 个评分最高的答案确实很适合我的场景。该场景在 ScrollView
和自定义 LoadingView
之间切换。每次我从 LoadingView
切换到使用遗留 UIScrollView
使用 UIViewRepresentable
创建的 ScrollView
时, contentSize
就会变得一团糟。
因此,作为一种解决方案,我创建了一个库,以便它对所有试图为这种简单问题寻找解决方案的开发人员都有用。我从互联网上浏览了许多网站,并最终调整了解决方案,最终为我提供了最佳解决方案。
步骤
- 将
SPM
https://github.com/bibinjacobpulickal/BBRefreshableScrollView 添加到您的项目。 import BBRefreshableScrollView
到所需的文件。- 更新
View
的body
。
struct CategoryHome: View {
...
var body: some View {
NavigationView {
BBRefreshableScrollView { completion in
// do refreshing stuff here
} content: {
ForEach(categories.keys.sorted().identified(by: \.self)) { key in
Text(key)
}
}
.navigationBarTitle(Text("Featured"))
}
}
}
更多详情可以关注Readme.
这是一种使用 ScrollView、GeometryReader 和 PreferenceKey 的纯 SwiftUI 方法 我能够读取 ScrollView 中的滚动偏移量,一旦它高于阈值,我就可以执行操作
import SwiftUI
struct RefreshableView<Content:View>: View {
init(action: @escaping () -> Void, @ViewBuilder content: @escaping () -> Content) {
self.content = content
self.refreshAction = action
}
var body: some View {
GeometryReader { geometry in
ScrollView {
content()
.anchorPreference(key: OffsetPreferenceKey.self, value: .top) {
geometry[[=10=]].y
}
}
.onPreferenceChange(OffsetPreferenceKey.self) { offset in
if offset > threshold {
refreshAction()
}
}
}
}
private var content: () -> Content
private var refreshAction: () -> Void
private let threshold:CGFloat = 50.0
}
fileprivate struct OffsetPreferenceKey: PreferenceKey {
static var defaultValue: CGFloat = 0
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
value = nextValue()
}
}
这是RefreshableView 的用法示例。 进度指示器不包含在 RefreshableView 中,它只提供一个 GeometryReader 和一个包含您要刷新的内容的 ScrollView。您需要提供 ProgressView 或其他视图来显示加载正在进行中。 它不适用于 List,但您可以改用 ForEach,内容将滚动,这要归功于 ScrollView
RefreshableView(action: {
viewModel.refreshFeed(forceReload: true)
}) {
if viewModel.showProgressView {
VStack {
ProgressView()
Text("reloading feed...")
.font(Font.caption2)
}
}
ForEach(viewModel.feed.entries) { entry in
viewForEntry(entry)
}
}
完整示例可在 GitHub
上找到来自 iOS 15+
NavigationView {
List(1..<100) { row in
Text("Row \(row)")
}
.refreshable {
print("write your pull to refresh logic here")
}
}
更多详情:Apple Doc
这个呢
import SwiftUI
public struct FreshScrollView<Content>: View where Content : View{
private let content: () -> Content
private let action: () -> Void
init(@ViewBuilder content: @escaping () -> Content, action: @escaping ()-> Void){
self.content = content
self.action = action
}
@State var startY: Double = 10000
public var body: some View{
ScrollView{
GeometryReader{ geometry in
HStack {
Spacer()
if geometry.frame(in: .global) .minY - startY > 30{
ProgressView()
.padding(.top, -30)
.animation(.easeInOut)
.transition(.opacity)
.onAppear{
let noti = UIImpactFeedbackGenerator(style: .light)
noti.prepare()
noti.impactOccurred()
action()
}
}
Spacer()
}
.onAppear {
startY = geometry.frame(in:.global).minY
}
}
content()
}
}
}
#if DEBUG
struct FreshScrollView_Previews: PreviewProvider {
static var previews: some View {
FreshScrollView {
Text("A")
Text("B")
Text("C")
Text("D")
} action: {
print("text")
}
}
}
#endif
我知道最初的问题是针对列表的,但这里是 ScrollView
和 LazyVStack
的代码,因为有时列表不合适。
import SwiftUI
struct PullToRefreshSwiftUI: View {
@Binding private var needRefresh: Bool
private let coordinateSpaceName: String
private let onRefresh: () -> Void
init(needRefresh: Binding<Bool>, coordinateSpaceName: String, onRefresh: @escaping () -> Void) {
self._needRefresh = needRefresh
self.coordinateSpaceName = coordinateSpaceName
self.onRefresh = onRefresh
}
var body: some View {
HStack(alignment: .center) {
if needRefresh {
VStack {
Spacer()
ProgressView()
Spacer()
}
.frame(height: 100)
}
}
.background(GeometryReader {
Color.clear.preference(key: ScrollViewOffsetPreferenceKey.self,
value: [=10=].frame(in: .named(coordinateSpaceName)).origin.y)
})
.onPreferenceChange(ScrollViewOffsetPreferenceKey.self) { offset in
guard !needRefresh else { return }
if abs(offset) > 50 {
needRefresh = true
onRefresh()
}
}
}
}
struct ScrollViewOffsetPreferenceKey: PreferenceKey {
typealias Value = CGFloat
static var defaultValue = CGFloat.zero
static func reduce(value: inout Value, nextValue: () -> Value) {
value += nextValue()
}
}
这是典型用法:
struct ContentView: View {
@State private var refresh: Bool = false
@State private var itemList: [Int] = {
var array = [Int]()
(0..<40).forEach { value in
array.append(value)
}
return array
}()
var body: some View {
ScrollView {
PullToRefreshSwiftUI(needRefresh: $refresh,
coordinateSpaceName: "pullToRefresh") {
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
withAnimation { refresh = false }
}
}
LazyVStack {
ForEach(itemList, id: \.self) { item in
HStack {
Spacer()
Text("\(item)")
Spacer()
}
}
}
}
.coordinateSpace(name: "pullToRefresh")
}
}
我对 SwiftUI 中的下拉刷新选项非常不满意。即使在使用基于内省 UIKit 的解决方案时,在使用导航视图时也会出现奇怪的行为和一些视图跳跃。我还需要一些更可定制的东西,所以我写了一个库。它是下拉刷新控件的(几乎)纯 SwiftUI 实现,它适用于 iOS 14 和 15。
https://github.com/gh123man/SwiftUI-Refresher
它是高度可定制的,并支持一些额外的功能,例如能够覆盖视图内容(在您有静态 header 的情况下)。
由于 swiftUi 最好使用纯 swiftUi,正如@Anatoliy Kashkin 在他的回答中提到的那样,所以我更新了他的回答以支持 enable/disable 从视图外部和内部刷新。
创建PullToRefresh
结构视图:
import Foundation
import SwiftUI
struct PullToRefresh: View {
@Binding var isRefreshing:Bool
var coordinateSpaceName: String
var onRefresh: ()->Void
@State var needRefresh: Bool = false
var body: some View {
GeometryReader { geo in
if (geo.frame(in: .named(coordinateSpaceName)).midY > 50) {
Spacer()
.onAppear {
needRefresh = true
}
} else if (geo.frame(in: .named(coordinateSpaceName)).maxY < 10) {
Spacer()
.onAppear {
if needRefresh {
needRefresh = false
onRefresh()
}
}
}
HStack {
Spacer()
if needRefresh || isRefreshing {
ProgressView()
} else {
Image(systemName: "arrow.clockwise")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 20, height: 20)
.foregroundColor(.gray)
Text("Refresh")
.foregroundColor(.gray)
}
Spacer()
}
}.padding(.top, isRefreshing ? 0 : -50)
}
}
现在你可以像这样使用它了:
@State var isRefreshing:Bool = true
var body: some View {
ScrollView(){
PullToRefresh(isRefreshing: $isRefreshing, coordinateSpaceName: "pullToRefresh") {
fetchOrders()
}
VStack(alignment : .leading){
Text("Some view...")
}
}.coordinateSpace(name: "pullToRefresh")
通过设置 isRefreshing
true ,它将显示进度并在您完成 http 请求或更新数据等时将其设置为 false