Бесконечная вертикальная прокрутка в обоих направлениях (добавление элементов динамически вверху / внизу), которая не мешает положению прокрутки при добавлении в начало списка

Мне нужна вертикальная прокрутка, которая бесконечна в обоих направлениях: прокрутка вверх или вниз вниз приводит к динамическому добавлению большего количества элементов. Почти вся помощь, с которой я сталкивался, касается только бесконечности нижней стороны. Я наткнулся на этот соответствующий ответ, но это не то, что я специально ищу (он добавляет элементы автоматически в зависимости от продолжительности , и требует взаимодействия с кнопками направления, чтобы указать способ прокрутки). Этот менее актуальный ответ, однако, был довольно полезный. Основываясь на сделанном там предложении, я понял, что могу вести учет элементов, видимых в любое время, и, если они окажутся позициями 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)
            }
        }
    }
}

В основном это работает нормально, за исключением небольшой, но важной детали. Когда элемент добавляется сверху, в зависимости от того, как я прокручиваю вниз, иногда он может быть скачкообразным. Это наиболее заметно ближе к концу прикрепленного зажима.

См. также:  Alpine.js - отображение поля ввода и одновременное добавление к нему фокуса

введите описание изображения здесь

Понравилась статья? Поделиться с друзьями:
IT Шеф
Комментарии: 2
  1. Barrrdi

    Я пробовал ваш код и не смог ничего исправить с помощью List OR ScrollView, но это возможно как uiscrollview, который бесконечно прокручивается.

    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, вы сбрасываете смещение до середины экрана, когда подходите достаточно близко к краю, и все сводится к мозаичному размещению ваших представлений, чтобы все они отображались одним сверху друг друга. если вам нужны какие-либо разъяснения по этому классу, спрашивайте в комментариях. когда у вас есть эти 2 выяснения, вы приклеиваете SwiftUIView, который также находится в предоставленном классе. на данный момент единственный способ, чтобы представления были видны на экране, — это указать явный размер для hosting.view, если вы выясните, как сделать размер SwiftUIView размером hosting.view, сообщите мне в комментариях, я ищу ответ на это. надеюсь, что код кому-то поможет, если что-то не так, оставьте комментарий.

  2. Barrrdi

    После изучения вашего кода я считаю, что эта нервозность, которую вы видите, вызвана следующим:

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

    Если вы удалите оба и оставите только items.insert(items.first! - 1, at: 0), нервозность прекратится.

Добавить комментарий

;-) :| :x :twisted: :smile: :shock: :sad: :roll: :razz: :oops: :o :mrgreen: :lol: :idea: :grin: :evil: :cry: :cool: :arrow: :???: :?: :!: