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

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

我追求的是一个垂直滚动视图,这是无限的双向:向上滚动到顶部或向下滚动到底部导致动态添加更多的项目。我遇到的几乎所有帮助都只关心底部的范围是无限的。我确实遇到过this relevant answer,但它不是我特别想要的(它是根据持续时间自动添加项目,需要与方向按钮交互以指定滚动方式)。less relevant answer非常有用。根据这里提出的建议,我意识到我可以随时记录可见的项目,如果它们碰巧是从顶部/底部的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)
            }
        }
    }
}

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

vs91vp4v

vs91vp4v1#

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

1.将uiscrollView Package 在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.this is my whole code for the infinitly scrolling 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,它也在提供的类中。现在,唯一的方法,为视图被看到在屏幕上是指定一个显式的大小为hosting.view,如果你弄清楚如何使SwiftUIView大小hosting.view,请告诉我在评论,我正在寻找答案。希望代码对某人有所帮助,如果有错误请留言。

8oomwypt

8oomwypt2#

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

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

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

qzlgjiam

qzlgjiam3#

这两天我一直在为这个问题绞尽脑汁。。像@Ferologics建议的那样去掉DispatchQueue几乎可以工作,但是如果你拉得太用力,你会遇到无限自动滚动的潜在问题。我最终放弃了无限滚动,并使用下拉刷新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)
                }
            }
        }
    }
}

以及ViewModel:

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

n3h0vuf2

n3h0vuf24#

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

相关问题