下拉以刷新SwiftUI中的数据

时间:2019-06-07 11:40:56

标签: swift list uirefreshcontrol swiftui

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

仅当用户尝试像从UITableViewUIRefreshControl的{​​{1}}一样从第一个索引下拉时,下拉刷新视图才可见

这是在UIKit中列出数据的简单代码。

SwiftUI

10 个答案:

答案 0 :(得分:6)

对于正在玩的应用程序,我也需要同样的东西,而且看来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()
        }
    }
}

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

答案 1 :(得分:5)

来自 iOS 15+

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

了解更多详情:Apple Doc

答案 2 :(得分:3)

我尝试了许多不同的解决方案,但对于我的案例来说,没有一种效果足够好。 对于复杂布局,基于 GeometryReader 的解决方案性能不佳。

这是一个看起来运行良好的纯 SwiftUI 2.0 视图,不会因不断更新状态而降低滚动性能,也不会使用任何 UIKit 技巧:

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 在触发器视图 onAppear 上调用 onDisappearColor.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

答案 3 :(得分:2)

老实说,没有一个最受好评的答案真的适合我的场景。该场景在 ScrollView 和自定义 LoadingView 之间切换。每次我从 LoadingView 切换到使用旧版 ScrollView 使用 UIScrollView 创建的 UIViewRepresentable 时,contentSize 都会搞砸。

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

步骤

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

答案 4 :(得分:1)

您好,请查看我制作的这个库: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
                }
            }
        }
    }
}

答案 5 :(得分:1)

这是一个实现视图层次结构并为SwiftUI列表的表视图添加适当的UIRefreshControl的实现:https://github.com/timbersoftware/SwiftUIRefresh

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

答案 6 :(得分:1)

masift尚不支持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

答案 7 :(得分:1)

这是我制作的简单小型 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")

答案 8 :(得分:1)

iOS 15 中的新功能,SwiftUI 的 refreshable() 修饰符可让您将功能附加到列表,以便在用户向下拖动足够远时触发。只要您的代码完成运行,iOS 就会自动显示活动指示器。


struct CallItem: Decodable, Identifiable {
    let id: Int
    let title: String
    let strap: String
}

struct ContentView: View {
    @State private var calls = [
        CallItem(id: 0, title: "Want the latest news?", strap: "Pull to refresh!")
    ]

    var body: some View {
        NavigationView {
            List(calls) { item in
                VStack(alignment: .leading) {
                    Text(item.title)
                        .font(.headline)
                    Text(item.strap)
                        .foregroundColor(.secondary)
                }
            }
            .refreshable {
                do {
                    // Fetch and decode JSON into news items 
                    let url = URL(string: "https://www.someCoolsite.com/samples/calls.json")!
                    let (data, _) = try await URLSession.shared.data(from: url)
                    calls = try JSONDecoder().decode([CallItem].self, from: data)
                } catch {
                    // Something went wrong; clear the news
                    calls = []
                }
            }
        }
    }
}


答案 9 :(得分:0)

这是一种使用 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

上找到