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.

3voto

user3814420 Points 326

Cela peut être réalisé sur macOS en enveloppant un NSScrollView dans un objet NSViewControllerRepresentable (et je suppose que la même chose fonctionne sur iOS en utilisant UIScrollView et UIViewControllerRepresentable). Je pense que cela peut être un peu plus fiable que l'autre réponse ici puisque le système d'exploitation gère toujours une grande partie de la fonction du contrôle.

Je viens juste de le faire fonctionner, et je prévois d'essayer de faire fonctionner d'autres choses, comme obtenir la position de certaines lignes dans mon contenu, mais voici mon code jusqu'à présent :

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

3voto

Saúl Moreno Abril Points 1113

Je présente une autre solution pour obtenir la référence de UITableView en utilisant la bibliothèque Introspection jusqu'à ce qu'Apple améliore les méthodes disponibles.

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

1voto

Kenan Begić Points 930

Voici ma solution de travail pour un objet observé qui reçoit des données dynamiquement, comme un tableau de messages dans le chat qui est alimenté par la conversation.

Modèle de tableau de messages :

 struct Message: Identifiable, Codable, Hashable {

        //MARK: Attributes
        var id: String
        var message: String

        init(id: String, message: String){
            self.id = id
            self.message = message
        }
    }

Vue actuelle :

@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
            }
        }
    }
}

Jetez un coup d'œil à cet exemple entièrement fonctionnel sur GitHub : https://github.com/kenagt/AutoScrollViewExample

0voto

Anton IOS Points 29

Et être simplifié.....

.onChange(of: messages) { target in
                withAnimation {
                    scrollView.scrollTo(target.last?.id, anchor: .bottom)
                }
            }

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