89 votes

Comment faire défiler automatiquement une liste SwiftUI ?

Lorsque j'ajoute du contenu à mon ListView, je veux qu'il défile automatiquement vers le bas.

J'utilise un SwiftUI List et un BindableObject comme contrôleur. Les nouvelles données sont ajoutées à la liste.

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

Je veux que la liste défile vers le bas lorsque j'ajoute de nouvelles données à la liste de messages. Cependant, je dois faire défiler la liste manuellement.

73voto

cjpais Points 658

Mise à jour : Dans iOS 14, il y a maintenant un moyen natif de faire cela. Je le fais comme suit

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

Pour iOS 13 et inférieur, vous pouvez essayer :

J'ai trouvé que le fait d'inverser les vues semblait fonctionner assez bien pour moi. La ScrollView démarre en bas de l'écran et, lorsqu'on y ajoute de nouvelles données, la vue défile automatiquement vers le bas.

  1. Faites pivoter la vue extérieure de 180 .rotationEffect(.radians(.pi))
  2. Retournez-le sur le plan vertical .scaleEffect(x: -1, y: 1, anchor: .center)

Vous devrez faire de même pour vos vues intérieures, car elles seront désormais toutes tournées et retournées. Pour les retourner, faites la même chose que ci-dessus.

Si vous en avez besoin à de nombreux endroits, il peut être intéressant d'avoir une vue personnalisée pour cela.

Vous pouvez essayer quelque chose comme ce qui suit :

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)

Voici une extension de View pour le retourner.

extension View {
    public func flip() -> some View {
        return self
            .rotationEffect(.radians(.pi))
            .scaleEffect(x: -1, y: 1, anchor: .center)
    }
}

47voto

Asperi Points 123157

Comme il n'y a pas de fonction intégrée de ce type pour le moment (ni pour les listes ni pour les ScrollView), Xcode 11.2, j'ai dû coder un ScrollView personnalisé avec le comportement ScrollToEnd.

! !! Inspiré par ce article.

Voici le résultat de mes expériences, j'espère que vous le trouverez utile aussi. Bien sûr il y a plus de paramètres, qui pourraient être configurables, comme les couleurs, etc., mais cela semble trivial et hors de portée.

scroll to endreverse content

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

7voto

Mojtaba Hosseini Points 2525

SwiftUI 2.0 - iOS 14

c'est celui-là : (en l'enveloppant dans un ScrollViewReader )

scrollView.scrollTo(rowID)

Depuis la sortie de SwiftUI 2.0, vous pouvez intégrer n'importe quel objet défilant dans la balise ScrollViewReader et vous pouvez alors accéder à l'emplacement exact de l'élément que vous devez faire défiler.

Voici une application de démonstration complète :

// 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)
            }
        }
    }
}

Prévisualisation

Preview

6voto

Casper Zandbergen Points 607

IOS 13+

Ce paquet appelé ScrollViewProxy ajoute un ScrollViewReader qui fournit un ScrollViewProxy sur lequel vous pouvez appeler scrollTo(_:) pour tout identifiant que vous avez donné à une vue. Sous le capot, il utilise Introspect pour obtenir le UIScrollView.

Ejemplo:

ScrollView {
    ScrollViewReader { proxy in
        Button("Jump to #8") {
            proxy.scrollTo(8)
        }

        ForEach(0..<10) { i in
            Text("Example \(i)")
                .frame(width: 300, height: 300)
                .scrollId(i)
        }
    }
}

4voto

Vous pouvez le faire maintenant, depuis Xcode 12, avec la toute nouvelle fonction ScrollViewProxy Voici un exemple de code :

Vous pouvez mettre à jour le code ci-dessous avec votre chatController.messages et l'appel scrollViewProxy.scrollTo(chatController.messages.count-1) .

Quand le faire ? Peut-être sur la nouvelle version de l'interface 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)
                        }
                    }
                }
            }
        }
    }
}

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X