ios 无限垂直滚动视图的两种方式(添加项目动态顶部/底部),不干扰滚动位置时,您添加到列表开始

von4xj4u  于 2023-06-25  发布在  iOS
关注(0)|答案(4)|浏览(170)

我追求的是一个垂直滚动视图,这是无限的双向:向上滚动到顶部或向下滚动到底部导致动态添加更多的项目。我遇到的几乎所有帮助都只关心底部的范围是无限的。我确实遇到过this relevant answer,但它不是我特别想要的(它是根据持续时间自动添加项目,需要与方向按钮交互以指定滚动方式)。less relevant answer非常有用。根据这里提出的建议,我意识到我可以随时记录可见的项目,如果它们碰巧是从顶部/底部的X个位置,在列表的开始/结束索引处插入一个项目。
另一个注意事项是,我得到的列表从中间开始,所以没有必要添加任何东西,除非你已经移动了50%的向上/向下。
需要说明的是,这是一个日历屏幕,我希望用户可以自由地滚动到任何时间。

  1. struct TestInfinityList: View {
  2. @State var visibleItems: Set<Int> = []
  3. @State var items: [Int] = Array(0...20)
  4. var body: some View {
  5. ScrollViewReader { value in
  6. List(items, id: \.self) { item in
  7. VStack {
  8. Text("Item \(item)")
  9. }.id(item)
  10. .onAppear {
  11. self.visibleItems.insert(item)
  12. /// if this is the second item on the list, then time to add with a short delay
  13. /// another item at the top
  14. if items[1] == item {
  15. DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) {
  16. withAnimation(.easeIn) {
  17. items.insert(items.first! - 1, at: 0)
  18. }
  19. }
  20. }
  21. }
  22. .onDisappear {
  23. self.visibleItems.remove(item)
  24. }
  25. .frame(height: 300)
  26. }
  27. .onAppear {
  28. value.scrollTo(10, anchor: .top)
  29. }
  30. }
  31. }
  32. }

除了一个很小但很重要的细节之外,这基本上工作得很好。当从顶部添加一个项目时,根据我向下滚动的方式,它有时会很不稳定。这在连接的夹子末端最明显。

vs91vp4v

vs91vp4v1#

我试过你的代码,不能修复任何与列表或滚动视图,但它是可能的作为一个uiscrollview,无限滚动。

1.将uiscrollView Package 在UIViewRepresentable中

  1. struct ScrollViewWrapper: UIViewRepresentable {
  2. private let uiScrollView: UIInfiniteScrollView
  3. init<Content: View>(content: Content) {
  4. uiScrollView = UIInfiniteScrollView()
  5. }
  6. init<Content: View>(@ViewBuilder content: () -> Content) {
  7. self.init(content: content())
  8. }
  9. func makeUIView(context: Context) -> UIScrollView {
  10. return uiScrollView
  11. }
  12. func updateUIView(_ uiView: UIScrollView, context: Context) {
  13. }
  14. }

2.this is my whole code for the infinitly scrolling uiscrollview

  1. class UIInfiniteScrollView: UIScrollView {
  2. private enum Placement {
  3. case top
  4. case bottom
  5. }
  6. var months: [Date] {
  7. return Calendar.current.generateDates(inside: Calendar.current.dateInterval(of: .year, for: Date())!, matching: DateComponents(day: 1, hour: 0, minute: 0, second: 0))
  8. }
  9. var visibleViews: [UIView] = []
  10. var container: UIView! = nil
  11. var visibleDates: [Date] = [Date()]
  12. override init(frame: CGRect) {
  13. super.init(frame: frame)
  14. setup()
  15. }
  16. required init?(coder: NSCoder) {
  17. fatalError("init(coder:) has not been implemented")
  18. }
  19. //MARK: (*) otherwise can cause a bug of infinite scroll
  20. func setup() {
  21. contentSize = CGSize(width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height * 6)
  22. scrollsToTop = false // (*)
  23. showsVerticalScrollIndicator = false
  24. container = UIView(frame: CGRect(x: 0, y: 0, width: contentSize.width, height: contentSize.height))
  25. container.backgroundColor = .purple
  26. addSubview(container)
  27. }
  28. override func layoutSubviews() {
  29. super.layoutSubviews()
  30. recenterIfNecessary()
  31. placeViews(min: bounds.minY, max: bounds.maxY)
  32. }
  33. func recenterIfNecessary() {
  34. let currentOffset = contentOffset
  35. let contentHeight = contentSize.height
  36. let centerOffsetY = (contentHeight - bounds.size.height) / 2.0
  37. let distanceFromCenter = abs(contentOffset.y - centerOffsetY)
  38. if distanceFromCenter > contentHeight / 3.0 {
  39. contentOffset = CGPoint(x: currentOffset.x, y: centerOffsetY)
  40. visibleViews.forEach { v in
  41. v.center = CGPoint(x: v.center.x, y: v.center.y + (centerOffsetY - currentOffset.y))
  42. }
  43. }
  44. }
  45. func placeViews(min: CGFloat, max: CGFloat) {
  46. // first run
  47. if visibleViews.count == 0 {
  48. _ = place(on: .bottom, edge: min)
  49. }
  50. // place on top
  51. var topEdge: CGFloat = visibleViews.first!.frame.minY
  52. while topEdge > min {topEdge = place(on: .top, edge: topEdge)}
  53. // place on bottom
  54. var bottomEdge: CGFloat = visibleViews.last!.frame.maxY
  55. while bottomEdge < max {bottomEdge = place(on: .bottom, edge: bottomEdge)}
  56. // remove invisible items
  57. var last = visibleViews.last
  58. while (last?.frame.minY ?? max) > max {
  59. last?.removeFromSuperview()
  60. visibleViews.removeLast()
  61. visibleDates.removeLast()
  62. last = visibleViews.last
  63. }
  64. var first = visibleViews.first
  65. while (first?.frame.maxY ?? min) < min {
  66. first?.removeFromSuperview()
  67. visibleViews.removeFirst()
  68. visibleDates.removeFirst()
  69. first = visibleViews.first
  70. }
  71. }
  72. //MARK: returns the new edge either biggest or smallest
  73. private func place(on: Placement, edge: CGFloat) -> CGFloat {
  74. switch on {
  75. case .top:
  76. let newDate = Calendar.current.date(byAdding: .month, value: -1, to: visibleDates.first ?? Date())!
  77. let newMonth = makeUIViewMonth(newDate)
  78. visibleViews.insert(newMonth, at: 0)
  79. visibleDates.insert(newDate, at: 0)
  80. container.addSubview(newMonth)
  81. newMonth.frame.origin.y = edge - newMonth.frame.size.height
  82. return newMonth.frame.minY
  83. case .bottom:
  84. let newDate = Calendar.current.date(byAdding: .month, value: 1, to: visibleDates.last ?? Date())!
  85. let newMonth = makeUIViewMonth(newDate)
  86. visibleViews.append(newMonth)
  87. visibleDates.append(newDate)
  88. container.addSubview(newMonth)
  89. newMonth.frame.origin.y = edge
  90. return newMonth.frame.maxY
  91. }
  92. }
  93. func makeUIViewMonth(_ date: Date) -> UIView {
  94. let month = makeSwiftUIMonth(from: date)
  95. let hosting = UIHostingController(rootView: month)
  96. hosting.view.bounds.size = CGSize(width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height * 0.55)
  97. hosting.view.clipsToBounds = true
  98. hosting.view.center.x = container.center.x
  99. return hosting.view
  100. }
  101. func makeSwiftUIMonth(from date: Date) -> some View {
  102. return MonthView(month: date) { day in
  103. Text(String(Calendar.current.component(.day, from: day)))
  104. }
  105. }
  106. }

仔细观察这个,它几乎是不言自明的,取自WWDC 2011的想法,当你足够接近边缘时,你将偏移量重置为屏幕中间,这一切都归结为平铺视图,以便它们都显示在彼此的顶部。如果你想对课堂有任何澄清,请在评论中提出。当你弄清楚了这两个后,你就粘上SwiftUIView,它也在提供的类中。现在,唯一的方法,为视图被看到在屏幕上是指定一个显式的大小为hosting.view,如果你弄清楚如何使SwiftUIView大小hosting.view,请告诉我在评论,我正在寻找答案。希望代码对某人有所帮助,如果有错误请留言。

展开查看全部
8oomwypt

8oomwypt2#

在浏览了你的代码之后,我相信你看到的这种跳跃是由以下原因引起的:

  1. DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) {
  2. withAnimation(.easeIn) {
  3. items.insert(items.first! - 1, at: 0)
  4. }
  5. }

如果你把两者都去掉,只留下items.insert(items.first! - 1, at: 0),跳跃就会停止。

qzlgjiam

qzlgjiam3#

这两天我一直在为这个问题绞尽脑汁。。像@Ferologics建议的那样去掉DispatchQueue几乎可以工作,但是如果你拉得太用力,你会遇到无限自动滚动的潜在问题。我最终放弃了无限滚动,并使用下拉刷新SwiftUIRefresh从顶部加载新项目。它现在做的工作,但我仍然很想知道如何获得真正的无限滚动上升!

  1. import SwiftUI
  2. import SwiftUIRefresh
  3. struct InfiniteChatView: View {
  4. @ObservedObject var viewModel = InfiniteChatViewModel()
  5. var body: some View {
  6. VStack {
  7. Text("Infinite Scroll View Testing...")
  8. Divider()
  9. ScrollViewReader { proxy in
  10. List(viewModel.stagedChats, id: \.id) { chat in
  11. Text(chat.text)
  12. .padding()
  13. .id(chat.id)
  14. .transition(.move(edge: .top))
  15. }
  16. .pullToRefresh(isShowing: $viewModel.chatLoaderShowing, onRefresh: {
  17. withAnimation {
  18. viewModel.insertPriors()
  19. }
  20. viewModel.chatLoaderShowing = false
  21. })
  22. .onAppear {
  23. proxy.scrollTo(viewModel.stagedChats.last!.id, anchor: .bottom)
  24. }
  25. }
  26. }
  27. }
  28. }

以及ViewModel:

  1. class InfiniteChatViewModel: ObservableObject {
  2. @Published var stagedChats = [Chat]()
  3. @Published var chatLoaderShowing = false
  4. var chatRepo: [Chat]
  5. init() {
  6. self.chatRepo = Array(0...1000).map { Chat($0) }
  7. self.stagedChats = Array(chatRepo[500...520])
  8. }
  9. func insertPriors() {
  10. guard let id = stagedChats.first?.id else {
  11. print("first member of stagedChats does not exist")
  12. return
  13. }
  14. guard let firstIndex = self.chatRepo.firstIndex(where: {$0.id == id}) else {
  15. print(chatRepo.count)
  16. print("ID \(id) not found in chatRepo")
  17. return
  18. }
  19. stagedChats.insert(contentsOf: chatRepo[firstIndex-5...firstIndex-1], at: 0)
  20. }
  21. }
  22. struct Chat: Identifiable {
  23. var id: String = UUID().uuidString
  24. var text: String
  25. init(_ number: Int) {
  26. text = "Chat \(number)"
  27. }
  28. }

展开查看全部
n3h0vuf2

n3h0vuf24#

对于其他仍然遇到SwiftUI这个问题的人,我的解决方法是从两个方向上的一组荒谬的月开始,显示一个LazyVStack,然后滚动到当前的月.onAppear。这里明显的问题是你会得到一个令人困惑的用户体验,他们在日历跳到当前月份之前看到遥远过去的一个随机月份。我通过将整个日历隐藏在一个矩形和一个ProgressView后面,直到.onAppear块的末尾来处理这个问题。有一个非常小的延迟,用户看到加载动画,然后日历弹出所有准备好去和当前月份。

相关问题