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

The*_*ell 5 refresh drag swiftui refreshable

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

拖动刷新

Meg*_*anX 4

这是代码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: $0.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()
    }

}
Run Code Online (Sandbox Code Playgroud)

这是典型用法:

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")
    }
}
Run Code Online (Sandbox Code Playgroud)

这很容易适应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: -$0.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)
    }
}
Run Code Online (Sandbox Code Playgroud)

  • 我在改进的变体中注意到的一件事是,当我下拉到触发偏移时,它会运行刷新。但通常刷新应该在用户滚动到触发偏移后然后释放拖动后发生。(就像在推特上一样)。我更新了“onPreferenceChange”来实现这一点。`if !needsRefresh &amp;&amp; offset &lt; Constants.refreshTriggerOffset { withAnimation { needRefresh = true } } else if needRefresh &amp;&amp; offset == 0 { onRefresh() }` (2认同)