46

我使用了简单的数据列表List。我想添加下拉刷新功能,但我不确定哪个是最好的方法。

下拉刷新视图只有在用户尝试从第一个索引下拉时才可见,就像我们在 in 中所做的UITableView一样UIRefreshControlUIKit

这是列出数据的简单代码SwiftUI

struct CategoryHome: View {
    var categories: [String: [Landmark]] {
        .init(
            grouping: landmarkData,
            by: { $0.category.rawValue }
        )
    }

    var body: some View {
        NavigationView {
            List {
                ForEach(categories.keys.sorted().identified(by: \.self)) { key in
                    Text(key)
                }
            }
            .navigationBarTitle(Text("Featured"))
        }
    }
}
4

11 回答 11

26

这是我制作的一个简单小巧纯正的 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 的坐标空间:

ScrollView {
    PullToRefresh(coordinateSpaceName: "pullToRefresh") {
        // do your stuff when pulled
    }
    
    Text("Some view...")
}.coordinateSpace(name: "pullToRefresh")
于 2020-12-02T01:25:15.147 回答
26

对于我正在玩的应用程序,我需要同样的东西,看起来 SwiftUI API 目前不包括ScrollViews 的刷新控制功能。

随着时间的推移,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()
        }
    }
}

但是,当然,您不能在滚动视图中使用任何 SwiftUI 组件,除非将它们包装在 aUIHostingController中并将makeUIView它们放入LegacyScrollView() { // views here }.

于 2019-06-11T14:21:31.610 回答
18

这是一个自省视图层次结构并向UIRefreshControlSwiftUI 列表的表视图添加属性的实现:https ://github.com/timbersoftware/SwiftUIRefresh

大量的内省逻辑可以在这里找到:https ://github.com/timbersoftware/SwiftUIRefresh/blob/15d9deed3fec66e2c0f6fd1fd4fe966142a891db/Sources/PullToRefresh.swift#L39-L73

于 2019-11-26T02:04:18.600 回答
15

从 iOS 15+

NavigationView {
    List(1..<100) { row in
     Text("Row \(row)")
    }
    .refreshable {
         print("write your pull to refresh logic here")
    }
}

更多详情:Apple Doc

于 2021-06-08T05:36:12.960 回答
6

嗨,看看我制作的这个库:https ://github.com/AppPear/SwiftUI-PullToRefresh

您可以通过一行代码来实现它:

struct CategoryHome: View {
    var categories: [String: [Landmark]] {
        .init(
            grouping: landmarkData,
            by: { $0.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
                }
            }
        }
    }
}
于 2019-09-19T15:27:53.243 回答
5

我尝试了许多不同的解决方案,但对我的情况没有任何效果。 GeometryReader基于解决方案的复杂布局表现不佳。

这是一个纯粹的 SwiftUI 2.0 视图,它似乎运行良好,不会随着不断的状态更新而降低滚动性能,并且不使用任何 UIKit hack:

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负填充来调用onAppearonDisappear触发视图。Color.clear

如果触发视图出现和消失之间的时间大于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

于 2021-02-08T18:58:58.013 回答
2

这是使用 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[$0].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上找到

于 2021-05-10T14:45:19.263 回答
2

老实说,没有一个评价最高的答案真的适合我的场景。ScrollView该场景在和 custom之间切换LoadingView。每次我从使用旧版创建的切换LoadingView到使用时,都会变得一团糟。ScrollViewUIScrollViewUIViewRepresentablecontentSize

因此,作为一个解决方案,我创建了一个库,以便对所有试图为这样一个简单问题找到解决方案的开发人员有用。我从互联网上收集了很多信息,浏览了许多网站,最后调整了解决方案,最终给了我最好的解决方案。

脚步

  1. SPM https://github.com/bibinjacobpulickal/BBRefreshableScrollView添加到您的项目中。
  2. import BBRefreshableScrollView到所需的文件。
  3. 更新. 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"))
        }
    }
}

有关更多详细信息,您可以关注自述文件

于 2021-03-04T20:54:47.230 回答
1

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()
                                }
                            }
                        }

有关更多详细信息,您可以访问以下链接。 https://github.com/phuhuynh2411/SwiftUI-PullToRefresh

于 2020-04-06T04:31:57.640 回答
0

我知道最初的问题是针对列表的,但这里是 and 的代码ScrollViewLazyVStack因为有时列表不合适。

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()
    }

}

这是典型的用法:

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")
    }
}
于 2022-01-28T13:46:27.037 回答
0

那这个呢

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
于 2021-12-08T10:18:21.817 回答