【问题标题】:SwiftUI - Detect when ScrollView has finished scrolling?SwiftUI - 检测 ScrollView 何时完成滚动?
【发布时间】:2021-03-11 17:26:57
【问题描述】:

我需要找出我的ScrollView 停止移动的确切时刻。 SwiftUI 有可能吗?

Here 将等同于 UIScrollView

想了很久还是不知道……

一个用于测试的示例项目:

struct ContentView: View {
    
    var body: some View {
        ScrollView {
            VStack(spacing: 20) {
                ForEach(0...100, id: \.self) { i in
                    Rectangle()
                        .frame(width: 200, height: 100)
                        .foregroundColor(.green)
                        .overlay(Text("\(i)"))
                }
            }
            .frame(maxWidth: .infinity)
        }
    }
}

谢谢!

【问题讨论】:

    标签: ios swift swiftui scrollview


    【解决方案1】:

    对我来说,发布者在将 Asperi 的答案实现到更复杂的 SwiftUI 视图时也没有触发。为了解决这个问题,我创建了一个带有已发布变量的 StateObject,并设置了一定的去抖时间。

    据我所知,会发生这种情况:scrollView 的偏移量被写入发布者 (currentOffset),然后发布者通过去抖动处理它。当值在去抖(这意味着滚动已停止)之后传递时,它被分配给另一个发布者(offsetAtScrollEnd),视图(ScrollViewTest)接收。

    import SwiftUI
    import Combine
    
    struct ScrollViewTest: View {
        
        @StateObject var scrollViewHelper = ScrollViewHelper()
        
        var body: some View {
            
            ScrollView {
                ZStack {
                    
                    VStack(spacing: 20) {
                        ForEach(0...100, id: \.self) { i in
                            Rectangle()
                                .frame(width: 200, height: 100)
                                .foregroundColor(.green)
                                .overlay(Text("\(i)"))
                        }
                    }
                    .frame(maxWidth: .infinity)
                    
                    GeometryReader {
                        let offset = -$0.frame(in: .named("scroll")).minY
                        Color.clear.preference(key: ViewOffsetKey.self, value: offset)
                    }
                    
                }
                
            }.coordinateSpace(name: "scroll")
            .onPreferenceChange(ViewOffsetKey.self) {
                scrollViewHelper.currentOffset = $0
            }.onReceive(scrollViewHelper.$offsetAtScrollEnd) {
                print($0)
            }
            
        }
        
    }
    
    class ScrollViewHelper: ObservableObject {
        
        @Published var currentOffset: CGFloat = 0
        @Published var offsetAtScrollEnd: CGFloat = 0
        
        private var cancellable: AnyCancellable?
        
        init() {
            cancellable = AnyCancellable($currentOffset
                                            .debounce(for: 0.2, scheduler: DispatchQueue.main)
                                            .dropFirst()
                                            .assign(to: \.offsetAtScrollEnd, on: self))
        }
        
    }
    
    struct ViewOffsetKey: PreferenceKey {
        static var defaultValue = CGFloat.zero
        static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
            value += nextValue()
        }
    }
    

    【讨论】:

      【解决方案2】:

      我使用以下代码实现了一个 scollview。 并且永远不会调用 "Stopped on: \($0)"。我是不是做错了什么?

      func scrollableView(with geometryProxy: GeometryProxy) -> some View {
              let middleScreenPosition = geometryProxy.size.height / 2
      
              return ScrollView(content: {
                  ScrollViewReader(content: { scrollViewProxy in
                      VStack(alignment: .leading, spacing: 20, content: {
                          Spacer()
                              .frame(height: geometryProxy.size.height * 0.4)
                          ForEach(viewModel.fragments, id: \.id) { fragment in
                              Text(fragment.content) // Outside of geometry ready to set the natural size
                                  .opacity(0)
                                  .overlay(
                                      GeometryReader { textGeometryReader in
                                          let midY = textGeometryReader.frame(in: .global).midY
      
                                          Text(fragment.content) // Actual text
                                              .font(.headline)
                                              .foregroundColor( // Text color
                                                  midY > (middleScreenPosition - textGeometryReader.size.height / 2) &&
                                                      midY < (middleScreenPosition + textGeometryReader.size.height / 2) ? .white :
                                                      midY < (middleScreenPosition - textGeometryReader.size.height / 2) ? .gray :
                                                      .gray
                                              )
                                              .colorMultiply( // Animates better than .foregroundColor animation
                                                  midY > (middleScreenPosition - textGeometryReader.size.height / 2) &&
                                                      midY < (middleScreenPosition + textGeometryReader.size.height / 2) ? .white :
                                                      midY < (middleScreenPosition - textGeometryReader.size.height / 2) ? .gray :
                                                      .clear
                                              )
                                              .animation(.easeInOut)
                                      }
                                  )
                                  .scrollId(fragment.id)
                          }
                          Spacer()
                              .frame(height: geometryProxy.size.height * 0.4)
                      })
                      .frame(maxWidth: .infinity)
                      .background(GeometryReader {
                          Color.clear.preference(key: ViewOffsetKey.self,
                                                 value: -$0.frame(in: .named("scroll")).origin.y)
                      })
                      .onPreferenceChange(ViewOffsetKey.self) { detector.send($0) }
                      .padding()
                      .onReceive(self.fragment.$currentFragment, perform: { currentFragment in
                          guard let id = currentFragment?.id else {
                              return
                          }
                          scrollViewProxy.scrollTo(id, alignment: .center)
                      })
                  })
              })
              .simultaneousGesture(
                  DragGesture().onChanged({ _ in
                      print("Started Scrolling")
                  }))
              .coordinateSpace(name: "scroll")
              .onReceive(publisher) {
                  print("Stopped on: \($0)")
              }
          }
      

      我不确定是否应该在此处发布新的 Stack 帖子,因为我正在尝试使此处的代码正常工作。

      编辑:实际上,如果我同时暂停播放音频播放器,它会起作用。通过暂停它,它允许调用发布者。 尴尬。

      编辑 2:删除 .dropFirst() 似乎可以修复它,但调用它。

      【讨论】:

        【解决方案3】:

        这是一个可能的方法的演示 - 使用发布者更改滚动内容坐标和去抖动,因此仅在坐标停止更改后才报告事件。

        使用 Xcode 12.1 / iOS 14.1 测试

        注意:您可以使用 debounce period 来调整它以满足您的需求。

        import Combine
        
        struct ContentView: View {
            let detector: CurrentValueSubject<CGFloat, Never>
            let publisher: AnyPublisher<CGFloat, Never>
        
            init() {
                let detector = CurrentValueSubject<CGFloat, Never>(0)
                self.publisher = detector
                    .debounce(for: .seconds(0.2), scheduler: DispatchQueue.main)
                    .dropFirst()
                    .eraseToAnyPublisher()
                self.detector = detector
            }
            
            var body: some View {
                ScrollView {
                    VStack(spacing: 20) {
                        ForEach(0...100, id: \.self) { i in
                            Rectangle()
                                .frame(width: 200, height: 100)
                                .foregroundColor(.green)
                                .overlay(Text("\(i)"))
                        }
                    }
                    .frame(maxWidth: .infinity)
                    .background(GeometryReader {
                        Color.clear.preference(key: ViewOffsetKey.self,
                            value: -$0.frame(in: .named("scroll")).origin.y)
                    })
                    .onPreferenceChange(ViewOffsetKey.self) { detector.send($0) }
                }.coordinateSpace(name: "scroll")
                .onReceive(publisher) {
                    print("Stopped on: \($0)")
                }
            }
        }
        

        【讨论】:

        • 哇,这太棒了,非常感谢!
        • 嘿,尴尬,这在我的情况下似乎不起作用。 print 永远不会被调用。还使用一个额外的手势来检测它何时开始这样做.simultaneousGesture( DragGesture().onChanged({ _ in print("Started Scrolling") }))
        • ViewOffsetKey的实现是什么?
        • @OlegG。 ViewOffsetKey 取自我的另一篇帖子stackoverflow.com/a/62588295/12299030
        • Xcode 13 和 iOS 15 中的打印语句对我来说永远不会触发
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2010-11-02
        • 2018-11-22
        • 2012-01-01
        • 1970-01-01
        • 1970-01-01
        • 2020-04-08
        • 1970-01-01
        相关资源
        最近更新 更多