将内容添加到ListView时,我希望它自动向下滚动。
我正在使用SwiftUI“列表”和“ BindableObject”作为控制器。新数据将添加到列表中。
List(chatController.messages, id: \.self) { message in
MessageView(message.text, message.isMe)
}
我希望列表向下滚动,因为我将新数据追加到消息列表中。但是我必须手动向下滚动。
答案 0 :(得分:43)
更新:在iOS 14中,现在有一种本机方法。 我就是这样
ScrollView(.vertical) {
ScrollViewReader { scrollView in
LazyVStack {
ForEach(notes, id: \.self) { note in
MessageView(note: note)
}
}
.onAppear {
scrollView.scrollTo(notes[notes.endIndex - 1])
}
}
}
对于iOS 13及更低版本,您可以尝试:
我发现翻转视图对我来说似乎很好。这将在底部启动ScrollView,并在向其中添加新数据时自动向下滚动视图。
.rotationEffect(.radians(.pi))
.scaleEffect(x: -1, y: 1, anchor: .center)
您还必须对内部视图执行此操作,因为现在它们都将旋转和翻转。要翻转它们,请执行上面的相同操作。
如果需要这么多地方,可能值得为此定制视图。
您可以尝试以下操作:
List(chatController.messages, id: \.self) { message in
MessageView(message.text, message.isMe)
.rotationEffect(.radians(.pi))
.scaleEffect(x: -1, y: 1, anchor: .center)
}
.rotationEffect(.radians(.pi))
.scaleEffect(x: -1, y: 1, anchor: .center)
这是一个View扩展,可以翻转它
extension View {
public func flip() -> some View {
return self
.rotationEffect(.radians(.pi))
.scaleEffect(x: -1, y: 1, anchor: .center)
}
}
答案 1 :(得分:9)
由于Xcode 11.2目前没有内置的此类功能(无论是List还是ScrollView),所以我需要使用ScrollToEnd行为对自定义ScrollView进行编码
!!!受this文章的启发。
这是我实验的结果,希望有人也能有所帮助。当然,还有更多的参数,这些参数可能是可配置的,例如颜色等,但它看起来微不足道且超出范围。
import SwiftUI
struct ContentView: View {
@State private var objects = ["0", "1"]
var body: some View {
NavigationView {
VStack {
CustomScrollView(scrollToEnd: true) {
ForEach(self.objects, id: \.self) { object in
VStack {
Text("Row \(object)").padding().background(Color.yellow)
NavigationLink(destination: Text("Details for \(object)")) {
Text("Link")
}
Divider()
}.overlay(RoundedRectangle(cornerRadius: 8).stroke())
}
}
.navigationBarTitle("ScrollToEnd", displayMode: .inline)
// CustomScrollView(reversed: true) {
// ForEach(self.objects, id: \.self) { object in
// VStack {
// Text("Row \(object)").padding().background(Color.yellow)
// NavigationLink(destination: Text("Details for \(object)")) {
// Image(systemName: "chevron.right.circle")
// }
// Divider()
// }.overlay(RoundedRectangle(cornerRadius: 8).stroke())
// }
// }
// .navigationBarTitle("Reverse", displayMode: .inline)
HStack {
Button(action: {
self.objects.append("\(self.objects.count)")
}) {
Text("Add")
}
Button(action: {
if !self.objects.isEmpty {
self.objects.removeLast()
}
}) {
Text("Remove")
}
}
}
}
}
}
struct CustomScrollView<Content>: View where Content: View {
var axes: Axis.Set = .vertical
var reversed: Bool = false
var scrollToEnd: Bool = false
var content: () -> Content
@State private var contentHeight: CGFloat = .zero
@State private var contentOffset: CGFloat = .zero
@State private var scrollOffset: CGFloat = .zero
var body: some View {
GeometryReader { geometry in
if self.axes == .vertical {
self.vertical(geometry: geometry)
} else {
// implement same for horizontal orientation
}
}
.clipped()
}
private func vertical(geometry: GeometryProxy) -> some View {
VStack {
content()
}
.modifier(ViewHeightKey())
.onPreferenceChange(ViewHeightKey.self) {
self.updateHeight(with: $0, outerHeight: geometry.size.height)
}
.frame(height: geometry.size.height, alignment: (reversed ? .bottom : .top))
.offset(y: contentOffset + scrollOffset)
.animation(.easeInOut)
.background(Color.white)
.gesture(DragGesture()
.onChanged { self.onDragChanged($0) }
.onEnded { self.onDragEnded($0, outerHeight: geometry.size.height) }
)
}
private func onDragChanged(_ value: DragGesture.Value) {
self.scrollOffset = value.location.y - value.startLocation.y
}
private func onDragEnded(_ value: DragGesture.Value, outerHeight: CGFloat) {
let scrollOffset = value.predictedEndLocation.y - value.startLocation.y
self.updateOffset(with: scrollOffset, outerHeight: outerHeight)
self.scrollOffset = 0
}
private func updateHeight(with height: CGFloat, outerHeight: CGFloat) {
let delta = self.contentHeight - height
self.contentHeight = height
if scrollToEnd {
self.contentOffset = self.reversed ? height - outerHeight - delta : outerHeight - height
}
if abs(self.contentOffset) > .zero {
self.updateOffset(with: delta, outerHeight: outerHeight)
}
}
private func updateOffset(with delta: CGFloat, outerHeight: CGFloat) {
let topLimit = self.contentHeight - outerHeight
if topLimit < .zero {
self.contentOffset = .zero
} else {
var proposedOffset = self.contentOffset + delta
if (self.reversed ? proposedOffset : -proposedOffset) < .zero {
proposedOffset = 0
} else if (self.reversed ? proposedOffset : -proposedOffset) > topLimit {
proposedOffset = (self.reversed ? topLimit : -topLimit)
}
self.contentOffset = proposedOffset
}
}
}
struct ViewHeightKey: PreferenceKey {
static var defaultValue: CGFloat { 0 }
static func reduce(value: inout Value, nextValue: () -> Value) {
value = value + nextValue()
}
}
extension ViewHeightKey: ViewModifier {
func body(content: Content) -> some View {
return content.background(GeometryReader { proxy in
Color.clear.preference(key: Self.self, value: proxy.size.height)
})
}
}
#if DEBUG
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
#endif
答案 2 :(得分:4)
您可以立即执行此操作,因为Xcode 12带有全新的ScrollViewProxy
,下面是示例代码:
您可以使用chatController.messages
和呼叫scrollViewProxy.scrollTo(chatController.messages.count-1)
更新以下代码。
什么时候做?也许在SwiftUI的新onChange
上!
struct ContentView: View {
let itemCount: Int = 100
var body: some View {
ScrollViewReader { scrollViewProxy in
VStack {
Button("Scroll to top") {
scrollViewProxy.scrollTo(0)
}
Button("Scroll to buttom") {
scrollViewProxy.scrollTo(itemCount-1)
}
ScrollView {
LazyVStack {
ForEach(0 ..< itemCount) { i in
Text("Item \(i)")
.frame(height: 50)
.id(i)
}
}
}
}
}
}
}
答案 3 :(得分:4)
随着SwiftUI 2.0的发布,您可以在ScrollViewReader
中嵌入任何可滚动内容,然后可以访问需要滚动的确切元素位置。
这是一个完整的演示应用程序:
// A simple list of messages
struct MessageListView: View {
var messages = (1...100).map { "Message number: \($0)" }
var body: some View {
ScrollView {
LazyVStack {
ForEach(messages, id:\.self) { message in
Text(message)
Divider()
}
}
}
}
}
struct ContentView: View {
@State var search: String = ""
var body: some View {
ScrollViewReader { scrollView in
VStack {
MessageListView()
Divider()
HStack {
TextField("Number to search", text: $search)
Button("Go") {
withAnimation {
scrollView.scrollTo("Message number: \(search)")
}
}
}.padding(.horizontal, 16)
}
}
}
}
答案 4 :(得分:2)
在苹果公司改进可用方法之前,我提出了使用库Introspect获取UITableView引用的其他解决方案。
struct LandmarkList: View {
@EnvironmentObject private var userData: UserData
@State private var tableView: UITableView?
private var disposables = Set<AnyCancellable>()
var body: some View {
NavigationView {
VStack {
List(userData.landmarks, id: \.id) { landmark in
LandmarkRow(landmark: landmark)
}
.introspectTableView { (tableView) in
if self.tableView == nil {
self.tableView = tableView
print(tableView)
}
}
}
.navigationBarTitle(Text("Landmarks"))
.onReceive(userData.$landmarks) { (id) in
// Do something with the table for example scroll to the bottom
self.tableView?.setContentOffset(CGPoint(x: 0, y: CGFloat.greatestFiniteMagnitude), animated: false)
}
}
}
}
答案 5 :(得分:2)
This package called ScrollViewProxy添加了一个ScrollViewReader,它提供了ScrollViewProxy,您可以在该ScrollViewProxy上调用scrollTo(_:)
以获取您提供给View的任何ID。在后台,它使用Introspect来获取UIScrollView。
示例:
ScrollView {
ScrollViewReader { (scrollView: ScrollViewProxy<Int>) in
Button("Jump to #8") {
scrollView.scrollTo(8)
}
ForEach(0..<10) { i in
Text("Example \(i)")
.frame(width: 300, height: 300)
.id(i, scrollView: scrollView)
}
}
}
答案 6 :(得分:1)
这是我为观察到的对象提供的工作解决方案,该对象可以动态获取数据,例如聊天中通过对话填充的消息数组。
消息数组的模型:
struct Message: Identifiable, Codable, Hashable {
//MARK: Attributes
var id: String
var message: String
init(id: String, message: String){
self.id = id
self.message = message
}
}
实际视图:
@ObservedObject var messages = [Message]()
@State private var scrollTarget: Int?
var scrollView : some View {
ScrollView(.vertical) {
ScrollViewReader { scrollView in
ForEach(self.messages) { msg in
Text(msg).id(message.id)
}
//When you add new element it will scroll automatically to last element or its ID
.onChange(of: scrollTarget) { target in
withAnimation {
scrollView.scrollTo(target, anchor: .bottom)
}
}
.onReceive(self.$messages) { updatedMessages in
//When new element is added to observed object/array messages, change the scroll position to bottom, or last item in observed array
scrollView.scrollTo(umessages.id, anchor: .bottom)
//Update the scrollTarget to current position
self.scrollTarget = updatedChats.first!.messages.last!.message_timestamp
}
}
}
}
答案 7 :(得分:0)
这可以在macOS上通过将NSScrollView包裹在NSViewControllerRepresentable对象中来实现(并且我认为使用UIScrollView和UIViewControllerRepresentable在iOS上也可以进行相同的操作。)我认为这可能比此处的其他答案更可靠。操作系统仍将管理控件的大部分功能。
我现在已经开始工作了,我打算尝试做更多的事情,例如确定内容中某些行的位置,但是到目前为止,这是我的代码:
import SwiftUI
struct ScrollableView<Content:View>: NSViewControllerRepresentable {
typealias NSViewControllerType = NSScrollViewController<Content>
var scrollPosition : Binding<CGPoint?>
var hasScrollbars : Bool
var content: () -> Content
init(hasScrollbars: Bool = true, scrollTo: Binding<CGPoint?>, @ViewBuilder content: @escaping () -> Content) {
self.scrollPosition = scrollTo
self.hasScrollbars = hasScrollbars
self.content = content
}
func makeNSViewController(context: NSViewControllerRepresentableContext<Self>) -> NSViewControllerType {
let scrollViewController = NSScrollViewController(rootView: self.content())
scrollViewController.scrollView.hasVerticalScroller = hasScrollbars
scrollViewController.scrollView.hasHorizontalScroller = hasScrollbars
return scrollViewController
}
func updateNSViewController(_ viewController: NSViewControllerType, context: NSViewControllerRepresentableContext<Self>) {
viewController.hostingController.rootView = self.content()
if let scrollPosition = self.scrollPosition.wrappedValue {
viewController.scrollView.contentView.scroll(scrollPosition)
DispatchQueue.main.async(execute: {self.scrollPosition.wrappedValue = nil})
}
viewController.hostingController.view.frame.size = viewController.hostingController.view.intrinsicContentSize
}
}
class NSScrollViewController<Content: View> : NSViewController, ObservableObject {
var scrollView = NSScrollView()
var scrollPosition : Binding<CGPoint>? = nil
var hostingController : NSHostingController<Content>! = nil
@Published var scrollTo : CGFloat? = nil
override func loadView() {
scrollView.documentView = hostingController.view
view = scrollView
}
init(rootView: Content) {
self.hostingController = NSHostingController<Content>(rootView: rootView)
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
}
}
struct ScrollableViewTest: View {
@State var scrollTo : CGPoint? = nil
var body: some View {
ScrollableView(scrollTo: $scrollTo)
{
Text("Scroll to bottom").onTapGesture {
self.$scrollTo.wrappedValue = CGPoint(x: 0,y: 1000)
}
ForEach(1...50, id: \.self) { (i : Int) in
Text("Test \(i)")
}
Text("Scroll to top").onTapGesture {
self.$scrollTo.wrappedValue = CGPoint(x: 0,y: 0)
}
}
}
}
答案 8 :(得分:0)
可以简化.....
.onChange(of: messages) { target in
withAnimation {
scrollView.scrollTo(target.last?.id, anchor: .bottom)
}
}
答案 9 :(得分:0)
iOS 14/15:
我是通过像这样使用 ScrollView 的 onChange
修饰符做到的:
// View
struct ChatView: View {
@ObservedObject var viewModel = ChatViewModel()
@State var newText = ""
var body: some View {
ScrollViewReader { scrollView in
VStack {
ScrollView(.vertical) {
VStack {
ForEach(viewModel.messages) { message in
VStack {
Text(message.text)
Divider()
}
}
}.id("ChatScrollView")
}.onChange(of: viewModel.messages) { _ in
withAnimation {
scrollView.scrollTo("ChatScrollView", anchor: .bottom)
}
}
Spacer()
VStack {
TextField("Enter message", text: $newText)
.padding()
.frame(width: 400, height: 40, alignment: .center)
Button("Send") {
viewModel.addMessage(with: newText)
}
.frame(width: 400, height: 80, alignment: .center)
}
}
}
}
}
// View Model
class ChatViewModel: ObservableObject {
@Published var messages: [Message] = [Message]()
func addMessage(with text: String) {
messages.append(Message(text: text))
}
}
// Message Model
struct Message: Hashable, Codable, Identifiable {
var id: String = UUID().uuidString
var text: String
}
答案 10 :(得分:-2)
我可以告诉您如何自动滚动视图。
您需要使用状态和三元运算符来更改其offset属性
您需要在.onAppear()方法中更改状态,以使其自动为更改添加动画
如果要循环播放此动画,请使用此修饰符
.animation(Animation.easeIn().repeatsForever(自动反转:)
//如果选择autoreverses true,则将循环播放具有反转效果的动画,这意味着如果向下滚动,则它将向上滚动,然后循环这种行为。
您可以使用它来实现所需的效果,您将不得不做一些进一步的修改才能在列表中使用它,但是核心逻辑保持不变