【问题标题】:Pull down to refresh data in SwiftUI下拉刷新SwiftUI中的数据
【发布时间】:2019-10-22 22:07:54
【问题描述】:

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

只有当用户尝试从第一个索引下拉时,下拉刷新视图才可见,就像我们在 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"))
        }
    }
}

【问题讨论】:

  • 我建议 PullDownButton 应该用于此,但目前看来还没有实现

标签: swift list uirefreshcontrol swiftui


【解决方案1】:

我正在玩的应用程序也需要同样的东西,而且看起来 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 组件,除非将它们包装在 UIHostingController 中并将它们放入 makeUIView,而不是将它们放入 LegacyScrollView() { // views here } 中。

【讨论】:

    【解决方案2】:

    这里是一个simplesmallpure SwiftUI解决方案,目的是为ScrollView添加拉动刷新功能。 p>

    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")
    

    【讨论】:

    • 很好的简单实现,但是当你有一个 URLSession 数据任务时,后台线程 (task.resume()) 立即返回并且 UI 更新在从会话中获取数据后启动时不起作用,并且正在更新结果。当 UI 仍在更新时,进度控制会很快结束,从而使应用程序在更新期间无响应。几秒钟后应用更新,在此期间 ProgressView 不可见
    • 你用这个小宝石为我节省了几个小时不必要的头痛,谢谢!
    • 不错的解决方案,但在拉动时不做你的事情(1)当视图加载时,再次(2)当拉动时?难道我们不希望它只执行 (2) 吗?
    • 我没有看到第二个 onAppear 被调用,或者 ProgressView 消失了,除非我将测试更改为 maxY
    【解决方案3】:

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

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

    【讨论】:

    • 非常好的解决方案。有点 hacky,但应该很容易修复,如果未来的 SwiftUI 更新会破坏它。
    【解决方案4】:

    来自 iOS 15+

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

    更多详情:Apple Doc

    【讨论】:

    • 在 LazyVGrid 中可以与 ForEach 一起使用吗?如果不是,那么在 LazyVGrid 上进行下拉刷新的最佳方法是什么?
    • .refreshable only 适用于 iOS 15 中的 List,希望在以后的版本中我们能获得更多支持。
    【解决方案5】:

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

    【讨论】:

    • 看起来不错,但我无法通过 Swift 包管理器导入您的包。你介意在 repo 中添加一个 Package.swift 清单吗?
    • @michasaurus 这个“库”保存在一个文件中。所以只要复制它就完成了
    【解决方案6】:

    我尝试了许多不同的解决方案,但没有一个对我的情况足够好。 基于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 在触发视图Color.clear 上调用onAppearonDisappear

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

    【讨论】:

      【解决方案7】:

      说实话,没有一个评价最高的答案真的适合我的场景。该场景在ScrollView 和自定义LoadingView 之间切换。每次我从 LoadingView 切换到使用旧版 UIScrollView 使用 UIViewRepresentable 创建的 ScrollView 时,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

      【讨论】:

        【解决方案8】:

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

        【讨论】:

          【解决方案9】:

          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

          【讨论】:

          • 第二次拉动时加载指示消失了
          【解决方案10】:

          这个呢

          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
          

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2011-09-29
            • 1970-01-01
            • 2020-08-05
            • 1970-01-01
            • 2013-01-29
            • 2013-11-23
            相关资源
            最近更新 更多