SwiftUI 自动滚动到 ScrollView 底部(先底部)

54

我的问题是,我在SwiftUI中有一个ScrollView,并且其中包含一个foreach循环。当foreach加载完所有条目后,我希望最后一个条目获得焦点。

我做了一些谷歌搜索,但没有找到答案。

  ScrollView {
    VStack {
      ForEach (0..<self.entries.count) { index in
        Group {
          Text(self.entries[index].getName())
        }
      }
    }
  }

2
可能是如何使SwiftUI列表自动滚动?的重复问题。 - LuLuGaGa
1
我认为这个人有一个解决方案适合你 https://github.com/mremond/SwiftUI-ScrollView-Demo 。我明天会试一下。 - gujci
8个回答

42

滚动到最底部的方法

我还没有足够的声望来发表评论,所以这里提供给@Dam和@Evert

如果想在ForEach中的条目数量发生更改时自动滚动到底部,可以使用与上面回答中提到的相同方法,即通过使用ScrollViewReader并添加视图修饰符onChange来实现:

struct ContentView: View {
    let colors: [Color] = [.red, .blue, .green]
    var entries: [Entry] = Array(repeating: Entry(), count: 10)

    var body: some View {
        ScrollView {
            ScrollViewReader { value in
                            
                ForEach(0..<entries.count) { i in
                    Text(self.entries[i].getName())
                        .frame(width: 300, height: 200)
                        .background(colors[i % colors.count])
                        .padding(.all, 20)
                }
                .onChange(of: entries.count) { _ in
                    value.scrollTo(entries.count - 1)
                }
            }
        }
    }
}

也许你的声望不够,但你应该获得奖章。非常好的解决方案,避免了需要一个符合Hashable的元素数组(我的数组是[AnyView])。 - Luc-Olivier

38
根据苹果公司有关 ScrollViewReader文档,它应该包装滚动视图,而不是嵌套在其中。
来自苹果文档的示例:
@Namespace var topID
@Namespace var bottomID

var body: some View {
    ScrollViewReader { proxy in
        ScrollView {
            Button("Scroll to Bottom") {
                withAnimation {
                    proxy.scrollTo(bottomID)
                }
            }
            .id(topID)

            VStack(spacing: 0) {
                ForEach(0..<100) { i in
                    color(fraction: Double(i) / 100)
                        .frame(height: 32)
                }
            }

            Button("Top") {
                withAnimation {
                    proxy.scrollTo(topID)
                }
            }
            .id(bottomID)
        }
    }
}

func color(fraction: Double) -> Color {
    Color(red: fraction, green: 1 - fraction, blue: 0.5)
}

这篇文章也展示了如何使用ScrollViewReader来包装一个元素的List,这也很方便。来自该文章的代码示例:

ScrollViewReader { scrollView in
    List {
        ForEach(photos.indices) { index in
            Image(photos[index])
                .resizable()
                .scaledToFill()
                .cornerRadius(25)
                .id(index)
        }
    }
 
    .
    .
    .
}

对我而言,使用value.scrollTo(entries.count - 1)不起作用。此外,在使用value.scrollTo(entries.last?.id)之前,也不起作用,直到我使用 .id(...)视图修饰符(可能是因为我的条目不符合Identifiable)。

在使用ForEach时,这是我正在处理的代码:

ScrollViewReader { value in
    ScrollView {
        ForEach(state.messages, id: \.messageId) { message in
            MessageView(message: message)
                .id(message.messageId)
        }
    }
    .onAppear {
        value.scrollTo(state.messages.last?.messageId)
    }
    .onChange(of: state.messages.count) { _ in
        value.scrollTo(state.messages.last?.messageId)
    }
}

使用onAppear时需要注意一个重要的问题,那就是在ScrollView/List上使用该修饰符而不是在ForEach中使用。在ForEach中这样做会导致它在手动滚动列表时触发列表中的元素出现。


1
这应该是适用于Xcode 13.4.1和iOS 15.5的正确答案 - (1) Reader包装ScrollView。 (2) 其他答案忽略的关键元素是使用.id(message.id)... (3) 在我的情况下,.onAppear {} 不需要。 - eharo2
1
可以确认这一点,只有在视图使用了.id()修饰符标记时才会滚动。 - YosefAhab
当我第一次看到有关ScrollViewReader的帖子时,我很困惑为什么ScrollView是外部父级而不是reader。感谢您确认了这一点。 - chitgoks

26

iOS14中使用ScrollViewReader的解决方案

iOS14中,SwiftUI具有了新的功能,可以通过给定的idScrollView中滚动到特定的项目。新增了一种类型叫做ScrollViewReader,其工作方式与Geometry Reader类似。

以下代码将滚动到您视图中的最后一个项目。

所以,我猜这是您的“Entry”结构体:

struct Entry {
    let id = UUID()
    
    func getName() -> String {
         return "Entry with id \(id.uuidString)"
    }
}

并且主要的 ContentView:

struct ContentView: View {
    var entries: [Entry] = Array(repeating: Entry(), count: 10)

    var body: some View {
        ScrollView {
            ScrollViewReader { value in
                ForEach(entries, id: \.id) { entry in
                    Text(entry.getName())
                        .frame(width: 300, height: 200)
                        .padding(.all, 20)
                }
                .onAppear {
                    value.scrollTo(entries.last?.id, anchor: .center)
                }
            }
        }
    }
}

尝试在WWDC20宣布的新版本SwiftUI中运行此代码。我认为这是一个很好的改进。


4
嘿,你有没有想过如何在每次添加新条目时使其滚动?我正在制作一个聊天应用程序,我希望它能在每次收到新消息时滚动到底部。 - Evert
1
我有同样的问题,当我打开视图时,ForEach为空,当我添加内容时,它不会滚动。这是否可能使用水平ScrollView? - Dam
@Dam,你需要探索View.onPreferenceChange。 - Arutyun Enfendzhyan

6

如果在您的ScrollView中只有一个子视图作为显示视图。

这里提供的所有解决方案都假设具有多个子视图,每个子视图都有一个ID。 但是,如果您只有一个子视图,例如多行文本,在时间上增长并且希望具有此自动向下滚动功能,该怎么办?

然后,您可以简单地添加一个标识符(在我的情况下为“1”):

VStack {
   ScrollViewReader { proxy in
       ScrollView {
           Text(historyText)
               .id(1)            // this is where to add an id
               .multilineTextAlignment(.leading)
               .padding()
       }
       .onChange(of: historyText) { _ in
            proxy.scrollTo(1, anchor: .bottom)
       }
    }
 }

5
我想我们都在做聊天应用程序!
这就是我最终的实现方式,主要是受到这里的答案的启发,加上了一些变化(经过多次尝试)。
struct ChatItemListView: View {
    
    @StateObject var lesson: Lesson

    var body: some View {
        
        ScrollViewReader { scrollViewProxy in

            List(lesson.safeChatItems) { chatItem in
                ChatItemRowView(chatItem: chatItem).id(chatItem.id)
            }
            .onReceive(lesson.objectWillChange) { _ in
                guard !lesson.chatItems.isEmpty else { return }
                Task {
                    scrollViewProxy.scrollTo(
                        lesson.chatItems.last!.id, 
                        anchor: .top
                    )
                }
            }

        }
    }
}

这里有两个重要的细节:
  • 通过.id(chatItem.id)将id强制传递到视图中
  • 将scrollTo操作放入Task
如果没有这些,即使我的chatItem实现了Identifiable协议,也无法正常工作。

你能否解释一下为什么这个解决方案有效而其他解决方案无效? - Miroslav Kuťák

4
在滚动视图底部添加一个带有空字符串和ID的TextView,每当有某些变化时使用scrollTo滚动到这个视图。以下是代码:
struct ContentView: View {
@Namespace var bottomID
@State var data : [Int] = []

init(){
    var temp : [Int] = []
    for i in 0...100 {
        temp.append(i)
    }
    _data = .init(initialValue: temp)
}

var body: some View {
    
    ScrollViewReader { value in
        ScrollView {
        
            ForEach(data, id: \.self) { i in
                Text("\(i)")
            }
            
            // Empty text view with id
            Text("").id(bottomID)
        }
        .onAppear{
            withAnimation{
                // scrolling to the text value present at bottom
                value.scrollTo(bottomID)
            }
        }
    }
}

}


0
有一个功能是随着 iOS 17 一起提供的。
ScrollView {
    ForEach(0..<50) { i in
        Text("Item \(i)")
            .frame(maxWidth: .infinity)
            .padding()
            .background(.blue)
            .clipShape(.rect(cornerRadius: 25))
    }
}
.scrollPosition(initialAnchor: .bottom)

0
这是一个例子,确保设置锚点,然后你就可以轻松实现你的行为。
 import SwiftUI

  struct ContentView: View {

    @Namespace private var topID
    @Namespace private var bottomID
    
    var body: some View {
        VStack(spacing: 20) {
            HStack {
                Button("Scroll to Top") {
                    withAnimation {
                        proxy.scrollTo(topID, anchor: .top)
                    }
                }
                
                Button("Scroll to Bottom") {
                    withAnimation {
                        proxy.scrollTo(bottomID, anchor: .bottom)
                    }
                }
            }
            
            ScrollViewReader { proxy in
                ScrollView {
                    VStack(spacing: 20) {
                        Text("Top Text")
                            .id(topID)
                            .background(Color.yellow)
                            .padding()
                        
                        ForEach(0..<30) { i in
                            Text("Dynamic Text \(i)")
                                .padding()
                                .background(i % 2 == 0 ? Color.blue : Color.green)
                                .foregroundColor(.white)
                        }
                        
                        Text("Bottom Text")
                            .id(bottomID)
                            .background(Color.red)
                            .padding()
                    }
                }
            }
        }
        .padding()
    }
}

@main
struct MyApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接