下拉以刷新SwiftUI中的数据

Pin*_*Gjr 9 list uirefreshcontrol swift swiftui

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

下拉刷新视图才可以看到,当用户尝试从同一像我们的第一个指数下拉在做UITableViewUIRefreshControlUIKit

这是在中列出数据的简单代码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"))
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

小智 20

这是我制作的一个简单小巧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)
    }
}
Run Code Online (Sandbox Code Playgroud)

要使用它很简单,只需将它添加到 ScrollView 的顶部并为其指定 ScrollView 的坐标空间:

ScrollView {
    PullToRefresh(coordinateSpaceName: "pullToRefresh") {
        // do your stuff when pulled
    }
    
    Text("Some view...")
}.coordinateSpace(name: "pullToRefresh")
Run Code Online (Sandbox Code Playgroud)


ldi*_*ual 18

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

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


Eva*_*ubl 12

我正在玩的应用程序需要同样的东西,而且看来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()
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

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


Ana*_*kin 11

我尝试了许多不同的解决方案,但没有一个适合我的情况。 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)
    }
}
Run Code Online (Sandbox Code Playgroud)

当进入或离开屏幕边界时,它使用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
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

要点:https://gist.github.com/tkashkin/e5f6b65b255b25269d718350c024f550


Ron*_*dia 7

从 iOS 15+

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

更多详情:Apple Doc

  • 这可以与 LazyVGrid 中的“ForEach”一起使用吗?如果没有,让下拉刷新在 LazyVGrid 上工作的最佳方法是什么? (2认同)
  • .refreshable *仅*适用于 iOS 15 中的 List,希望在以后的版本中我们能获得更多支持。 (2认同)

Sam*_*ras 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
                }
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)