6

我追求的是双向无限的垂直滚动视图:向上滚动到顶部或向下滚动到底部会导致更多项目被动态添加。我遇到的几乎所有帮助都只关心底部的范围是无限的。我确实遇到了这个相关的答案,但这不是我要特别寻找的(它根据持续时间自动添加项目,并且需要与方向按钮交互以指定滚动方式)。然而,这个不太相关的答案非常有帮助。根据那里提出的建议,我意识到我可以随时记录可见的项目,如果它们恰好是顶部/底部的 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)
            }
        }
    }
}

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

在此处输入图像描述

4

3 回答 3

0

在查看您的代码后,我相信您看到的这种跳跃是由以下原因引起的:

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

如果您同时删除两者并仅离开items.insert(items.first! - 1, at: 0),那么跳跃就会停止。

于 2021-04-23T15:48:36.277 回答
0

在过去的两天里,我一直在用这个问题把我的头撞到墙上......像@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)
                }
            }
        }
    }
}

和视图模型:

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

下拉刷新

于 2021-08-04T15:56:24.823 回答
0

我尝试了您的代码,但无法使用 List OR ScrollView 修复任何问题,但可以作为无限滚动的 uiscrollview。

1.在 UIViewRepresentable 中包装 uiscrollView

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。目前在屏幕上看到视图的唯一方法是为hosting.view指定一个显式大小,如果你知道如何让SwiftUIView调整hosting.view的大小,请在评论中告诉我,我正在寻找一个答案。希望代码对某人有所帮助,如果有问题请发表评论。

于 2021-03-09T15:07:22.677 回答