如何在 Swiftui 中拖动以刷新网格视图 (LazyVGrid)?

How can you Drag to refresh a Grid View (LazyVGrid) in Swiftui?

在swiftui中如何拖动刷新网格视图?我知道您可以在 iOS 15 中使用带有可刷新修饰符的列表视图来做到这一点,但是如何使用 LazyVGrid 来做到这一点?在 iOS 15 之前的列表或网格视图中,您将如何操作?我在 swiftui 还很新。我附上了一张 gif,展示了我正在努力实现的目标。

Drag to Refresh

代码如下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")
    }
}

这可以很容易地适应 LazyVGrid,只需替换 LazyVStack

编辑: 这是更精致的变体:

struct PullToRefresh: View {
    
    private enum Constants {
        static let refreshTriggerOffset = CGFloat(-140)
    }
    
    @Binding private var needsRefresh: Bool
    private let coordinateSpaceName: String
    private let onRefresh: () -> Void
    
    init(needsRefresh: Binding<Bool>, coordinateSpaceName: String, onRefresh: @escaping () -> Void) {
        self._needsRefresh = needsRefresh
        self.coordinateSpaceName = coordinateSpaceName
        self.onRefresh = onRefresh
    }
    
    var body: some View {
        HStack(alignment: .center) {
            if needsRefresh {
                VStack {
                    Spacer()
                    ProgressView()
                    Spacer()
                }
                .frame(height: 60)
            }
        }
        .background(GeometryReader {
            Color.clear.preference(key: ScrollViewOffsetPreferenceKey.self,
                                   value: -[=12=].frame(in: .named(coordinateSpaceName)).origin.y)
        })
        .onPreferenceChange(ScrollViewOffsetPreferenceKey.self) { offset in
            guard !needsRefresh, offset < Constants.refreshTriggerOffset else { return }
            withAnimation { needsRefresh = true }
            onRefresh()
        }
    }
}


private struct ScrollViewOffsetPreferenceKey: PreferenceKey {
    typealias Value = CGFloat
    static var defaultValue = CGFloat.zero
    static func reduce(value: inout Value, nextValue: () -> Value) {
        value += nextValue()
    }
}


private enum Constants {
    static let coordinateSpaceName = "PullToRefreshScrollView"
}

struct PullToRefreshScrollView<Content: View>: View {
    @Binding private var needsRefresh: Bool
    private let onRefresh: () -> Void
    private let content: () -> Content
    
    init(needsRefresh: Binding<Bool>,
         onRefresh: @escaping () -> Void,
         @ViewBuilder content: @escaping () -> Content) {
        self._needsRefresh = needsRefresh
        self.onRefresh = onRefresh
        self.content = content
    }
    
    var body: some View {
        ScrollView {
            PullToRefresh(needsRefresh: $needsRefresh,
                          coordinateSpaceName: Constants.coordinateSpaceName,
                          onRefresh: onRefresh)
            content()
        }
        .coordinateSpace(name: Constants.coordinateSpaceName)
    }
}