双向无限垂直滚动视图(在顶部/底部动态添加项目),当您添加到列表开始时,不会干扰滚动位置

问题描述 投票:0回答:5

我想要一个双向无限的垂直滚动视图:向上滚动到顶部或向下滚动到底部会导致动态添加更多项目。我遇到的几乎所有帮助都只涉及范围无限的底部。我确实遇到了这个相关答案,但这不是我专门寻找的(它根据持续时间自动添加项目,并且需要与方向按钮交互来指定滚动方式)。然而,这个“不太相关的答案”非常有帮助。根据那里提出的建议,我意识到我可以随时记录可见的项目,如果它们恰好是从顶部/底部的 X 个位置,则可以在列表的开始/结束索引处插入一个项目。 另一个注意事项是我让列表从中间开始,因此不需要以任何方式添加任何内容,除非您向上/向下移动了 50%。

需要明确的是,这是一个日历屏幕,我希望用户可以自由滚动到任何时间。

struct TestInfinityList: View { @State var visibleItems: Set<Int> = [] @State var items: [Int] = Array(0...20) var body: some View { ScrollViewReader { value in List(items, id: \.self) { item in VStack { Text("Item \(item)") }.id(item) .onAppear { self.visibleItems.insert(item) /// if this is the second item on the list, then time to add with a short delay /// another item at the top if items[1] == item { DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) { withAnimation(.easeIn) { items.insert(items.first! - 1, at: 0) } } } } .onDisappear { self.visibleItems.remove(item) } .frame(height: 300) } .onAppear { value.scrollTo(10, anchor: .top) } } } }

除了一个小而重要的细节外,大部分工作正常。当从顶部添加项目时,根据我向下滚动的方式,它有时可能会跳跃。这在所附夹子的末端最为明显。

enter image description here

ios swiftui scrollview infinite-scroll vertical-scroll
5个回答
6
投票

1.将uiscrollView包装在UIViewRepresentable中

struct ScrollViewWrapper: UIViewRepresentable { private let uiScrollView: UIInfiniteScrollView init<Content: View>(content: Content) { uiScrollView = UIInfiniteScrollView() } init<Content: View>(@ViewBuilder content: () -> Content) { self.init(content: content()) } func makeUIView(context: Context) -> UIScrollView { return uiScrollView } func updateUIView(_ uiView: UIScrollView, context: Context) { } }

2.这是我无限滚动uiscrollview的全部代码

class UIInfiniteScrollView: UIScrollView { private enum Placement { case top case bottom } var months: [Date] { return Calendar.current.generateDates(inside: Calendar.current.dateInterval(of: .year, for: Date())!, matching: DateComponents(day: 1, hour: 0, minute: 0, second: 0)) } var visibleViews: [UIView] = [] var container: UIView! = nil var visibleDates: [Date] = [Date()] override init(frame: CGRect) { super.init(frame: frame) setup() } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } //MARK: (*) otherwise can cause a bug of infinite scroll func setup() { contentSize = CGSize(width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height * 6) scrollsToTop = false // (*) showsVerticalScrollIndicator = false container = UIView(frame: CGRect(x: 0, y: 0, width: contentSize.width, height: contentSize.height)) container.backgroundColor = .purple addSubview(container) } override func layoutSubviews() { super.layoutSubviews() recenterIfNecessary() placeViews(min: bounds.minY, max: bounds.maxY) } func recenterIfNecessary() { let currentOffset = contentOffset let contentHeight = contentSize.height let centerOffsetY = (contentHeight - bounds.size.height) / 2.0 let distanceFromCenter = abs(contentOffset.y - centerOffsetY) if distanceFromCenter > contentHeight / 3.0 { contentOffset = CGPoint(x: currentOffset.x, y: centerOffsetY) visibleViews.forEach { v in v.center = CGPoint(x: v.center.x, y: v.center.y + (centerOffsetY - currentOffset.y)) } } } func placeViews(min: CGFloat, max: CGFloat) { // first run if visibleViews.count == 0 { _ = place(on: .bottom, edge: min) } // place on top var topEdge: CGFloat = visibleViews.first!.frame.minY while topEdge > min {topEdge = place(on: .top, edge: topEdge)} // place on bottom var bottomEdge: CGFloat = visibleViews.last!.frame.maxY while bottomEdge < max {bottomEdge = place(on: .bottom, edge: bottomEdge)} // remove invisible items var last = visibleViews.last while (last?.frame.minY ?? max) > max { last?.removeFromSuperview() visibleViews.removeLast() visibleDates.removeLast() last = visibleViews.last } var first = visibleViews.first while (first?.frame.maxY ?? min) < min { first?.removeFromSuperview() visibleViews.removeFirst() visibleDates.removeFirst() first = visibleViews.first } } //MARK: returns the new edge either biggest or smallest private func place(on: Placement, edge: CGFloat) -> CGFloat { switch on { case .top: let newDate = Calendar.current.date(byAdding: .month, value: -1, to: visibleDates.first ?? Date())! let newMonth = makeUIViewMonth(newDate) visibleViews.insert(newMonth, at: 0) visibleDates.insert(newDate, at: 0) container.addSubview(newMonth) newMonth.frame.origin.y = edge - newMonth.frame.size.height return newMonth.frame.minY case .bottom: let newDate = Calendar.current.date(byAdding: .month, value: 1, to: visibleDates.last ?? Date())! let newMonth = makeUIViewMonth(newDate) visibleViews.append(newMonth) visibleDates.append(newDate) container.addSubview(newMonth) newMonth.frame.origin.y = edge return newMonth.frame.maxY } } func makeUIViewMonth(_ date: Date) -> UIView { let month = makeSwiftUIMonth(from: date) let hosting = UIHostingController(rootView: month) hosting.view.bounds.size = CGSize(width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height * 0.55) hosting.view.clipsToBounds = true hosting.view.center.x = container.center.x return hosting.view } func makeSwiftUIMonth(from date: Date) -> some View { return MonthView(month: date) { day in Text(String(Calendar.current.component(.day, from: day))) } } }

仔细观察这一点,它几乎是不言自明的,取自 WWDC 2011 的想法,当你足够接近边缘时,你将偏移重置到屏幕中间,这一切都归结为平铺你的视图,以便它们都出现在一个彼此之上。如果您想对该课程有任何说明,请在评论中提问。
当你弄清楚这两个之后,你就可以粘合 SwiftUIView 了,它也在提供的类中。目前,在屏幕上看到视图的唯一方法是为主机视图指定显式大小,如果您知道如何使 SwiftUIView 大小为主机视图,请在评论中告诉我,我正在寻找对此的答案。希望代码对某人有所帮助,如果有问题请发表评论。


0
投票

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

如果你把两者都去掉,只留下
items.insert(items.first! - 1, at: 0)

,跳跃就会停止。

    


0
投票
SwiftUIRefresh

从顶部加载新项目。它现在可以完成工作,但我仍然很想知道如何实现真正的无限滚动! import SwiftUI import SwiftUIRefresh struct InfiniteChatView: View { @ObservedObject var viewModel = InfiniteChatViewModel() var body: some View { VStack { Text("Infinite Scroll View Testing...") Divider() ScrollViewReader { proxy in List(viewModel.stagedChats, id: \.id) { chat in Text(chat.text) .padding() .id(chat.id) .transition(.move(edge: .top)) } .pullToRefresh(isShowing: $viewModel.chatLoaderShowing, onRefresh: { withAnimation { viewModel.insertPriors() } viewModel.chatLoaderShowing = false }) .onAppear { proxy.scrollTo(viewModel.stagedChats.last!.id, anchor: .bottom) } } } } }

和视图模型:

class InfiniteChatViewModel: ObservableObject { @Published var stagedChats = [Chat]() @Published var chatLoaderShowing = false var chatRepo: [Chat] init() { self.chatRepo = Array(0...1000).map { Chat($0) } self.stagedChats = Array(chatRepo[500...520]) } func insertPriors() { guard let id = stagedChats.first?.id else { print("first member of stagedChats does not exist") return } guard let firstIndex = self.chatRepo.firstIndex(where: {$0.id == id}) else { print(chatRepo.count) print("ID \(id) not found in chatRepo") return } stagedChats.insert(contentsOf: chatRepo[firstIndex-5...firstIndex-1], at: 0) } } struct Chat: Identifiable { var id: String = UUID().uuidString var text: String init(_ number: Int) { text = "Chat \(number)" } }

Pull Down to Refresh


0
投票
LazyVStack

,然后滚动到当前月份

.onAppear
。这里明显的问题是,您会获得令人困惑的用户体验,他们会在日历跳转到当前月份之前看到遥远过去的随机月份。我通过将整个日历隐藏在矩形和
ProgressView
后面直到
.onAppear
块的末尾来处理此问题。用户看到加载动画时有一个非常小的延迟,然后日历弹出,一切准备就绪,并且在当前月份。
    


0
投票
ScrollView

LazyVStack
一起使用而不是
List
的性能要高得多。通过这样做,您还可以消除滞后。 (确保同时删除
asyncAfter
withAnimation
)。这是修改后的代码:
struct TestInfinityList: View {
    
    @State var visibleItems: Set<Int> = []
    @State var items: [Int] = Array(0...20)
    
    var body: some View {
        GeometryReader { geometryProxy in
            ScrollViewReader { scrollProxy in
                ScrollView(.vertical, showsIndicators: false) {
                    LazyVStack {
                        ForEach(items, id: \.self) { item in
                            ZStack {
                                if item % 2 == 0 {
                                    Color.gray
                                } else {
                                    Color.black
                                }
                                
                                Text("Item \(item)")
                            }
                            .frame(width: geometryProxy.size.width, height: geometryProxy.size.height)
                            .id(item)
                            .onAppear {
                                self.visibleItems.insert(item)
                                
                                if items[1] == item {
                                    items.insert(items.first! - 1, at: 0)
                                } else if items[items.count - 2] == item {
                                    items.append(items.last! + 1)
                                }
                            }
                            .onDisappear {
                                self.visibleItems.remove(item)
                            }
                        }
                    }
                }
                .onAppear {
                    scrollProxy.scrollTo(10)
                }
            }
        }
    }
}

示例中
GeometryReader

,着色部分仅用于演示。

还要记住隐藏滚动指示器以消除奇怪的跳跃。享受吧;)

© www.soinside.com 2019 - 2024. All rights reserved.