¿Cómo hacer que una lista SwiftUI se desplace automáticamente?

Resuelto Seb asked hace 5 años • 12 respuestas

Al agregar contenido a mi ListView, quiero que se desplace hacia abajo automáticamente.

Estoy usando SwiftUI Listy BindableObjectcomo controlador. Se están agregando nuevos datos a la lista.

List(chatController.messages, id: \.self) { message in
    MessageView(message.text, message.isMe)
}

Quiero que la lista se desplace hacia abajo a medida que agrego nuevos datos a la lista de mensajes. Sin embargo, tengo que desplazarme hacia abajo manualmente.

Seb avatar Jul 30 '19 00:07 Seb
Aceptado

Actualización: en iOS 14 ahora hay una forma nativa de hacer esto. lo estoy haciendo asi

        ScrollViewReader { scrollView in
            ScrollView(.vertical) {
                LazyVStack {
                    ForEach(notes, id: \.self) { note in
                        MessageView(note: note)
                    }
                }
                .onAppear {
                    scrollView.scrollTo(notes[notes.endIndex - 1])
                }
            }
        }

Para iOS 13 y versiones anteriores, puedes probar:

Descubrí que cambiar las vistas parecía funcionar bastante bien para mí. Esto inicia ScrollView en la parte inferior y, al agregarle nuevos datos, automáticamente desplaza la vista hacia abajo.

  1. Girar la vista más exterior 180.rotationEffect(.radians(.pi))
  2. Gíralo a lo largo del plano vertical..scaleEffect(x: -1, y: 1, anchor: .center)

Tendrás que hacer esto también con tus vistas internas, ya que ahora todas estarán rotadas y volteadas. Para darles la vuelta, haga lo mismo que arriba.

Si necesita tantos lugares, puede que valga la pena tener una vista personalizada para esto.

Puedes probar algo como lo siguiente:

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)

Aquí hay una extensión de Vista para darle la vuelta.

extension View {
    public func flip() -> some View {
        return self
            .rotationEffect(.radians(.pi))
            .scaleEffect(x: -1, y: 1, anchor: .center)
    }
}
cjpais avatar Apr 05 '2020 00:04 cjpais

Como no existe una función de este tipo incorporada por ahora (ni para List ni para ScrollView), Xcode 11.2, necesitaba codificar ScrollView personalizado con el comportamiento ScrollToEnd

!!! Inspirado en este artículo.

Aquí está el resultado de mis experimentos, espero que a uno también le resulte útil. Por supuesto, hay más parámetros, que pueden ser configurables, como colores, etc., pero parece trivial y fuera de alcance.

desplazarse hasta el final contenido inverso

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)
        })
    }
}
Asperi avatar Nov 05 '2019 09:11 Asperi

iOS 14/15:

Lo hice usando el onChangemodificador de ScrollView así:

// 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
}
atulkhatri avatar Jul 28 '2021 20:07 atulkhatri