SwiftUI 在 ScrollView 中自动滚动到底部(从底部开始)

Lea*_*par 20 foreach scroll scrollview swiftui swiftui-list

我的问题是我(在 SwiftUI 中)有一个 ScrollView,里面有一个 foreach。知道 foreach 何时加载我的所有条目,我希望最后一个条目得到关注。

我做了一些谷歌研究,但我没有找到任何答案。

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

Ene*_*nso 42

根据Apple的文档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)
}
Run Code Online (Sandbox Code Playgroud)

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

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

对我来说,使用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)
    }
}
Run Code Online (Sandbox Code Playgroud)

使用时的一个重要考虑onAppear因素是在ScrollView/List而不是在ForEach. 在 中这样做ForEach将导致它在手动滚动列表时出现列表中的元素时触发。


mul*_*des 18

适用于 iOS14 的解决方案 ScrollViewReader

在 iOS14 SwiftUI 获得了新的能力。为了能够在 ScrollView 中滚动到具有给定id. 有一个新的类型,称为ScrollViewReader该作品就像Geometry Reader

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

所以这是你的“条目”结构,我猜:

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

和主要的内容视图:

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)
                }
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

尝试在 WWDC20 宣布的新版 SwiftUI 中运行它。我认为这是一个很大的增强。

  • 嘿,你知道如何让它在每次添加新条目时滚动吗?我正在制作一个聊天应用程序,我希望每次收到新消息时它都能滚动到底部。 (4认同)

nor*_*kin 13

更改时滚动到底部

我还没有足够的声誉发表评论,所以你去@Dam 和@Evert

要在ForEach更改条目数时滚动到底部,您还可以使用与 a 相同的方法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)
                }
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)


Luk*_*ker 7

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

这里提供的所有解决方案都假设有多个子视图,每个子视图都有一个 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)
       }
    }
 }
Run Code Online (Sandbox Code Playgroud)


jrm*_*mgx 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
                    )
                }
            }

        }
    }
}
Run Code Online (Sandbox Code Playgroud)

这里的两个重要细节是:

  • 通过强制将 id 传递给视图.id(chatItem.id)
  • 将scrollTo操作放入Task

没有这些,即使我chatItem实现了Identifiable协议,它也无法工作。