![](/img/trans.png)
[英]Add a top view to UITableview and scroll it to bottom of navigationbar
[英]Infinite vertical scrollview both ways (add items dynamically at top/bottom) that doesn’t interfere with scroll position when you add to list start
我追求的是雙向無限的垂直滾動視圖:向上滾動到頂部或向下滾動到底部會導致更多項目被動態添加。 我遇到的幾乎所有幫助都只涉及 scope 中的底部是無限的。 我確實遇到了這個相關的答案,但這不是我要特別尋找的(它根據持續時間自動添加項目,並且需要與方向按鈕交互以指定滾動方式)。 然而,這個不太相關的答案非常有幫助。 根據那里提出的建議,我意識到我可以隨時記錄可見的項目,如果它們恰好是頂部/底部的 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)
}
}
}
}
除了一個小而重要的細節外,這大部分工作正常。 從頂部添加項目時,取決於我向下滾動的方式,有時可能會跳動。 這在連接的夾子末端最為明顯。
我嘗試了您的代碼,但無法使用 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 的想法,當你足夠靠近邊緣時,你將偏移重置為屏幕中間,這一切都歸結為平鋪你的視圖,所以它們都出現在頂部彼此的。 如果您想對該 class 進行任何澄清,請在評論中提問。 當你弄清楚了這 2 個時,然后你將 SwiftUIView 粘合在提供的 class 中。 目前在屏幕上看到視圖的唯一方法是為hosting.view指定一個顯式大小,如果你知道如何讓SwiftUIView調整hosting.view的大小,請在評論中告訴我,我正在尋找一個答案。 希望代碼對某人有所幫助,如果有問題請發表評論。
在查看您的代碼后,我相信您看到的這種跳躍是由以下原因引起的:
DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) {
withAnimation(.easeIn) {
items.insert(items.first! - 1, at: 0)
}
}
如果你同時刪除這兩個並且只留下items.insert(items.first, - 1: at: 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)"
}
}
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.