Comment faire défiler vers le bas d'un UITableView sur l'iPhone avant que la vue n'apparaisse


136

J'ai un UITableViewqui est rempli de cellules de hauteur variable. Je voudrais que le tableau défile vers le bas lorsque la vue est poussée dans la vue.

J'ai actuellement la fonction suivante

NSIndexPath *indexPath = [NSIndexPath indexPathForRow:[log count]-1 inSection:0];
[self.table scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionBottom animated:NO];

log est un tableau mutable contenant les objets qui composent le contenu de chaque cellule.

Le code ci-dessus fonctionne bien, viewDidAppearmais cela a pour effet secondaire malheureux d'afficher le haut du tableau lorsque la vue apparaît pour la première fois, puis de sauter vers le bas. Je préférerais que le table viewpuisse être fait défiler vers le bas avant d'apparaître.

J'ai essayé le défilement viewWillAppearet viewDidLoadmais dans les deux cas, les données n'ont pas encore été chargées dans la table et les deux lancent une exception.

Toute orientation serait très appréciée, même s'il ne s'agit que de me dire ce que j'ai, c'est tout ce qui est possible.

Réponses:


148

Je crois que l'appel

 tableView.setContentOffset(CGPoint(x: 0, y: CGFloat.greatestFiniteMagnitude), animated: false)

fera ce que vous voulez.


14
C'est parfait merci. J'ai créé un CGPoint avec une valeur Y suffisamment élevée pour qu'il affiche toujours le bas. Une fois la vue chargée, je peux utiliser (self.table.contentSize.height - self.table.frame.size.height) pour me déplacer vers le bas avec la même méthode
acqu13sce

8
Bien que ce soit une réponse parfaite, car nous n'avons pas besoin de faire de calcul pour le nombre de cellules, la hauteur de la table, etc. MAIS je ferais remarquer que nous devons appeler cela avant de recharger la vue de la table ... Cela ne fonctionnera pas si nous écrire ceci après[table reloadData];
Fahim Parkar

4
ne fonctionne pas sur iOS 10-12 - la table disparaît simplement pour la première fois
Vyachaslav Gerchicov

2
Ou faites défiler jusqu'à CGPoint(x: 0, y: tableView.contentSize.height)?
Amber K

5
Yikes !!! Fait disparaître la table: -o. Utilisation optimale de [self.table scrollToRowAtIndexPath: indexPath atScrollPosition: UITableViewScrollPositionBottom animated: NO];
Carl Hine

122

Je pense que le moyen le plus simple est le suivant:

if (self.messages.count > 0)
{
    [self.tableView 
        scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:self.messages.count-1 
        inSection:0] 
        atScrollPosition:UITableViewScrollPositionBottom animated:YES];
}

Version Swift 3:

if messages.count > 0 {
    userDefinedOptionsTableView.scrollToRow(at: IndexPath(item:messages.count-1, section: 0), at: .bottom, animated: true)
}

3
Cela ne fonctionne pas si la cellule est plus haute que l'UITableView
Olav Gausaker

3
La cellule est plus grande que UITableView? jamais entendu un tel cas d'utilisation.
Chamira Fernando

@ChamiraFernando c'est le moyen le plus simple :)
AITAALI_ABDERRAHMANE

Notez qu'il peut être judicieux de remplacer le messages.countpar le déjà implémenté myTableView.dataSource!.tableView(myTableView, numberOfRowsInSection: 0). Oui, c'est plus long, mais cela peut éviter la répétition du code. Vous devez également gérer l'option facultative dataSource(ne forcez pas le déballage comme dans cet exemple).
Nikolay Suvandzhiev

@ChamiraFernando Je sais que cette question est ancienne, mais ce n'est pas parce que vous n'avez jamais vu que cela ne se produit pas. Pour répondre à votre question, des applications comme Foursquare peuvent avoir cette situation, où l'utilisateur rédige un avis. La hauteur de la cellule est supérieure à la hauteur de la vue du tableau. C'est une situation parfaitement bien.
Caio le

121

D'après la réponse de Jacob, voici le code:

- (void) viewDidAppear:(BOOL)animated
{
    [super viewDidAppear:animated];

    if (self.messagesTableView.contentSize.height > self.messagesTableView.frame.size.height) 
    {
        CGPoint offset = CGPointMake(0, self.messagesTableView.contentSize.height - self.messagesTableView.frame.size.height);
        [self.messagesTableView setContentOffset:offset animated:YES];
    }
}

Sur iOS 11, vous devez utiliser la hauteur ajustée du cadre de la vue de la table:UIEdgeInsetsInsetRect(self.messagesTableView.frame, self.messagesTableView.safeAreaInsets).height
Slav

41

Si vous avez besoin de faire défiler jusqu'à la fin EXACT du contenu, vous pouvez le faire comme ceci:

- (void)scrollToBottom
{
    CGFloat yOffset = 0;

    if (self.tableView.contentSize.height > self.tableView.bounds.size.height) {
        yOffset = self.tableView.contentSize.height - self.tableView.bounds.size.height;
    }

    [self.tableView setContentOffset:CGPointMake(0, yOffset) animated:NO];
}

7
Fonctionne avec la mise en page automatique, MAIS il est important d'appeler cette méthode à partir de viewDidLayoutSubviews
Omaty

Pourriez-vous s'il vous plaît expliquer pourquoi nous devons faire ceci yOffset = self.tableView.contentSize.height - self.tableView.bounds.size.height; :? Merci.
Unheilig

3
@Unheilig Si vous voulez faire défiler jusqu'au self.tableView.contentSize.heightcontenu de la vue de table peut ne pas être visible, car vous faites défiler sous le contenu. Par conséquent, vous devez faire défiler jusqu'à un "espace de vue de tableau visible" au-dessus de la fin de la vue de tableau.
Hans One

31

J'utilise la mise en page automatique et aucune des réponses n'a fonctionné pour moi. Voici ma solution qui a finalement fonctionné:

@property (nonatomic, assign) BOOL shouldScrollToLastRow;


- (void)viewDidLoad {
    [super viewDidLoad];

    _shouldScrollToLastRow = YES;
}


- (void)viewDidLayoutSubviews {
    [super viewDidLayoutSubviews];

    // Scroll table view to the last row
    if (_shouldScrollToLastRow)
    {
        _shouldScrollToLastRow = NO;
        [self.tableView setContentOffset:CGPointMake(0, CGFLOAT_MAX)];
    }
}

1
Cela fonctionne presque pour moi, mais j'obtiens un problème graphique étrange pendant le chargement des données de ma table à partir d'une API externe. Dans mon cas, dois-je appeler setContentOffsetà un autre moment lorsque les données ont été récupérées et la tableview rechargée?
jmoz

Essayez de définir le décalage dans un gestionnaire d'achèvement de votre demande.
RaffAl

2
Cela ne fonctionne pas dans iOS 10 - montre simplement un tableau avec un fond noir
RunLoop

2
Au lieu d'utiliser, CGFLOAT_MAXj'ai utilisé contentSize.height - frame.height + contentInset.bottomlors de la définition du décalage de contenu initial. L'utilisation a CGFLOAT_MAXsemblé gâcher pour moi.
Baza207

23

La solution acceptée par @JacobRelkin ne fonctionnait pas pour moi dans iOS 7.0 en utilisant Auto Layout.

J'ai une sous-classe personnalisée de UIViewControlleret ajouté une variable d'instance en _tableViewtant que sous-vue de son view. Je me suis positionné en _tableViewutilisant la mise en page automatique. J'ai essayé d'appeler cette méthode à la fin viewDidLoadet même dans viewWillAppear:. Aucun des deux n'a fonctionné.

J'ai donc ajouté la méthode suivante à ma sous-classe personnalisée de UIViewController.

- (void)tableViewScrollToBottomAnimated:(BOOL)animated {
    NSInteger numberOfRows = [_tableView numberOfRowsInSection:0];
    if (numberOfRows) {
        [_tableView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:numberOfRows-1 inSection:0] atScrollPosition:UITableViewScrollPositionBottom animated:animated];
    }
}

Appel [self tableViewScrollToBottomAnimated:NO]à la fin des viewDidLoadtravaux. Malheureusement, il provoque également tableView:heightForRowAtIndexPath:un appel trois fois pour chaque cellule.


23

Voici une extension que j'ai implémentée dans Swift 2.0. Ces fonctions doivent être appelées après le tableviewchargement de:

import UIKit

extension UITableView {
    func setOffsetToBottom(animated: Bool) {
        self.setContentOffset(CGPointMake(0, self.contentSize.height - self.frame.size.height), animated: true)
    }

    func scrollToLastRow(animated: Bool) {
        if self.numberOfRowsInSection(0) > 0 {
            self.scrollToRowAtIndexPath(NSIndexPath(forRow: self.numberOfRowsInSection(0) - 1, inSection: 0), atScrollPosition: .Bottom, animated: animated)
        }
    }
}

3
C'est mieux que d'utiliser la taille du contenu. Pour Swift3. if self.numberOfRows (inSection: 0)> 0 {self.scrollToRow (at: IndexPath.init (row: self.numberOfRows (inSection: 0) -1, section: 0), at: .bottom, animé: animé)}
Soohwan Park

si scrollToLastRow cette méthode ne fonctionne pas parfaitement, ajoutez simplement self.layoutIfNeeded () et cela fonctionne parfaitement !!
Yogesh Patel

16

Détails

  • Xcode 8.3.2, swift 3.1
  • Xcode 10.2 (10E125), Swift 5

Code

import UIKit

extension UITableView {
    func scrollToBottom(animated: Bool) {
        let y = contentSize.height - frame.size.height
        if y < 0 { return }
        setContentOffset(CGPoint(x: 0, y: y), animated: animated)
    }
}

Usage

tableView.scrollToBottom(animated: true)

Échantillon complet

N'oubliez pas de coller le code de la solution!

import UIKit

class ViewController: UIViewController {

    private weak var tableView: UITableView?
    private lazy var cellReuseIdentifier = "CellReuseIdentifier"

    override func viewDidLoad() {
        super.viewDidLoad()
        let tableView = UITableView(frame: view.frame)
        view.addSubview(tableView)
        tableView.register(UITableViewCell.self, forCellReuseIdentifier: cellReuseIdentifier)
        self.tableView = tableView
        tableView.dataSource = self
        tableView.performBatchUpdates(nil) { [weak self] result in
            if result { self?.tableView?.scrollToBottom(animated: true) }
        }
    }
}

extension ViewController: UITableViewDataSource {

    func numberOfSections(in tableView: UITableView) -> Int {
        return 1
    }

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return 100
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: cellReuseIdentifier, for: indexPath )
        cell.textLabel?.text = "\(indexPath)"
        return cell
    }
}

15

En fait, une façon plus rapide de le faire est:

var lastIndex = NSIndexPath(forRow: self.messages.count - 1, inSection: 0)
self.messageTableView.scrollToRowAtIndexPath(lastIndex, atScrollPosition: UITableViewScrollPosition.Bottom, animated: true)

travail parfait pour moi.


9

Je voulais que le tableau se charge avec la fin du tableau indiqué dans le cadre. J'ai trouvé en utilisant

NSIndexPath *scrollIndexPath = [NSIndexPath indexPathForRow:([self.tableView numberOfRowsInSection:0] - 1) inSection:0];
[[self tableView] scrollToRowAtIndexPath:scrollIndexPath atScrollPosition:UITableViewScrollPositionBottom animated:NO];

ne fonctionnait pas car cela donnait une erreur lorsque la hauteur de la table était inférieure à la hauteur du cadre. Notez que mon tableau n'a qu'une seule section.

La solution qui a fonctionné pour moi a été d'implémenter le code suivant dans viewWillAppear:

- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
// on the initial cell load scroll to the last row (ie the latest Note)
if (initialLoad==TRUE) {
    initialLoad=FALSE; 
    NSIndexPath *scrollIndexPath = [NSIndexPath indexPathForRow:([self.tableView numberOfRowsInSection:0] - 1) inSection:0];
    [[self tableView] scrollToRowAtIndexPath:scrollIndexPath atScrollPosition:UITableViewScrollPositionBottom animated:NO];
        CGPoint offset = CGPointMake(0, (1000000.0));
        [self.tableView setContentOffset:offset animated:NO];
    }
}

Le BOOL ivar initialLoad est défini sur TRUE dans viewDidLoad.


Avez-vous besoin d'appeler scrollToRowAtIndexPathdu tout? Vous appelez déjà setContentOffsetaprès, ce qui pourrait rendre ce premier appel inutile.
Carlos P

9

Pour Swift:

if tableView.contentSize.height > tableView.frame.size.height {
    let offset = CGPoint(x: 0, y: tableView.contentSize.height - tableView.frame.size.height)
    tableView.setContentOffset(offset, animated: false)
}


5

Pour Swift 3 (Xcode 8.1):

override func viewDidAppear(_ animated: Bool) {
    let numberOfSections = self.tableView.numberOfSections
    let numberOfRows = self.tableView.numberOfRows(inSection: numberOfSections-1)

    let indexPath = IndexPath(row: numberOfRows-1 , section: numberOfSections-1)
    self.tableView.scrollToRow(at: indexPath, at: UITableViewScrollPosition.middle, animated: true)
}

3
Cela ne répond pas à la question OP, c'est ce qui fonctionnait depuis le début. Vous devriez également appeler super.viewDidAppear
streem

4

Est bien sûr un bug. Probablement quelque part dans votre code que vous utilisez table.estimatedRowHeight = value(par exemple 100). Remplacez cette valeur par la valeur la plus élevée que vous pensez qu'une hauteur de ligne pourrait obtenir , par exemple 500 .. Cela devrait résoudre le problème en combinaison avec le code suivant:

//auto scroll down example
let delay = 0.1 * Double(NSEC_PER_SEC)
let time = dispatch_time(DISPATCH_TIME_NOW, Int64(delay))

dispatch_after(time, dispatch_get_main_queue(), {
    self.table.scrollToRowAtIndexPath(NSIndexPath(forRow: self.Messages.count - 1, inSection: 0), atScrollPosition: UITableViewScrollPosition.Bottom, animated: false)
})

4

Après beaucoup de bidouilles, voici ce qui a fonctionné pour moi:

var viewHasAppeared = false

override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()
    if !viewHasAppeared { goToBottom() }
}

override func viewDidAppear(animated: Bool) {
    super.viewDidAppear(animated)
    viewHasAppeared = true
}

private func goToBottom() {
    guard data.count > 0 else { return }
    let indexPath = NSIndexPath(forRow: data.count - 1, inSection: 0)
    tableView.scrollToRowAtIndexPath(indexPath, atScrollPosition: .Bottom, animated: false)
    tableView.layoutIfNeeded()
}

La clé s'est avérée ne pas s'enrouler à l' scrollToRowAtIndexPathintérieur de dispatch_asynccomme certains l'ont suggéré, mais simplement la suivre avec un appel àlayoutIfNeeded .

Ma compréhension de cela est que l'appel de la méthode scroll dans le thread actuel garantit que le décalage de défilement est défini immédiatement, avant l'affichage de la vue. Lors de l'envoi vers le thread principal, la vue s'affichait un instant avant que le défilement ne prenne effet.

(Notez également que vous avez besoin du viewHasAppeareddrapeau car vous ne le souhaitez pas à goToBottomchaque fois qu'il viewDidLayoutSubviewsest appelé. Il est appelé par exemple chaque fois que l'orientation change.)


3

En utilisant les solutions ci-dessus, cela défilera vers le bas de votre tableau (uniquement si le contenu du tableau est chargé en premier):

//Scroll to bottom of table
CGSize tableSize = myTableView.contentSize;
[myTableView setContentOffset:CGPointMake(0, tableSize.height)];

3

Dans Swift 3.0

self.tableViewFeeds.setContentOffset(CGPoint(x: 0, y: CGFLOAT_MAX), animated: true)

2

Si vous devez charger les données de manière asynchrone avant de faire défiler vers le bas, voici la solution possible:

tableView.alpha = 0 // We want animation!
lastMessageShown = false // This is ivar

viewModel.fetch { [unowned self] result in
    self.tableView.reloadData()

    if !self.lastMessageShown {
        dispatch_async(dispatch_get_main_queue()) { [unowned self] in
            if self.rowCount > 0 {
                self.tableView.scrollToRowAtIndexPath(NSIndexPath(forRow: self.rowCount, inSection: 0), atScrollPosition: .Bottom, animated: false)
            }

            UIView.animateWithDuration(0.1) {
                self.tableView.alpha = 1
                self.lastMessageShown = true // Do it once
            }
        }
    }
}

2

Fonction sur Swift 3 défiler vers le bas

 override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(false)
        //scroll down
        if lists.count > 2 {
            let numberOfSections = self.tableView.numberOfSections
            let numberOfRows = self.tableView.numberOfRows(inSection: numberOfSections-1)
            let indexPath = IndexPath(row: numberOfRows-1 , section: numberOfSections-1)
            self.tableView.scrollToRow(at: indexPath, at: UITableViewScrollPosition.middle, animated: true)
        }
    }

2
func scrollToBottom() {

    let sections = self.chatTableView.numberOfSections

    if sections > 0 {

        let rows = self.chatTableView.numberOfRows(inSection: sections - 1)

        let last = IndexPath(row: rows - 1, section: sections - 1)

        DispatchQueue.main.async {

            self.chatTableView.scrollToRow(at: last, at: .bottom, animated: false)
        }
    }
}

tu devrais ajouter

DispatchQueue.main.async {
            self.chatTableView.scrollToRow(at: last, at: .bottom, animated: false)
        }

ou il ne défilera pas vers le bas.


2

Utilisez ce code simple pour faire défiler le tableau

NSInteger rows = [tableName numberOfRowsInSection:0];
if(rows > 0) {
    [tableName scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:rows-1 inSection:0]
                     atScrollPosition:UITableViewScrollPositionBottom
                             animated:YES];
}

1
C'est essentiellement ce que l'OP a dit avoir déjà essayé. Cette réponse ne répond pas à la question du PO de savoir comment le faire fonctionner correctement dans viewWillAppear.
jk7

2

Merci Jacob pour la réponse. vraiment utile si quelqu'un d'intéressant avec la version monotouch c #

private void SetScrollPositionDown() {
    if (tblShoppingListItem.ContentSize.Height > tblShoppingListItem.Frame.Size.Height) {
        PointF offset = new PointF(0, tblShoppingListItem.ContentSize.Height - tblShoppingListItem.Frame.Size.Height);
        tblShoppingListItem.SetContentOffset(offset,true );
    }
}

1

Dans iOS, cela a bien fonctionné pour moi

CGFloat height = self.inputTableView.contentSize.height;
if (height > CGRectGetHeight(self.inputTableView.frame)) {
    height -= (CGRectGetHeight(self.inputTableView.frame) - CGRectGetHeight(self.navigationController.navigationBar.frame));
}
else {
    height = 0;
}
[self.inputTableView setContentOffset:CGPointMake(0, height) animated:animated];

Il doit être appelé de viewDidLayoutSubviews


1

[self.tableViewInfo scrollRectToVisible: CGRectMake (0, self.tableViewInfo.contentSize.height-self.tableViewInfo.height, self.tableViewInfo.width, self.tableViewInfo.height) animé: OUI];


1

La réponse acceptée ne fonctionnait pas avec ma table (milliers de lignes, chargement dynamique) mais le code ci-dessous fonctionne:

- (void)scrollToBottom:(id)sender {
    if ([self.sections count] > 0) {
        NSInteger idx = [self.sections count] - 1;
        CGRect sectionRect = [self.tableView rectForSection:idx];
        sectionRect.size.height = self.tableView.frame.size.height;
        [self.tableView scrollRectToVisible:sectionRect animated:NO];
    }
}

1

Pas besoin de défilement, vous pouvez simplement le faire en utilisant ce code:

[YOURTABLEVIEWNAME setContentOffset:CGPointMake(0, CGFLOAT_MAX)];

1

Si vous configurez frame pour tableview par programme, assurez-vous que vous définissez correctement frame.


0

Dans Swift, vous avez juste besoin

self.tableView.scrollToNearestSelectedRowAtScrollPosition(UITableViewScrollPosition.Bottom, animated: true)

pour le faire défiler automatiquement vers le bouton


0

Dans swift 3.0 Si vous voulez aller à n'importe quelle cellule particulière de tableview Changer l'index de cellule Valeur comme changer la valeur "self.yourArr.count".

self.yourTable.reloadData()
self.scrollToBottom() 
func scrollToBottom(){
    DispatchQueue.global(qos: .background).async {
        let indexPath = IndexPath(row: self.yourArr.count-1, section: 0)
        self.tblComment.scrollToRow(at: indexPath, at: .bottom, animated: true)
    }
}

0

Je pense que les anciennes solutions ne fonctionnent pas avec swift3.

Si vous connaissez le nombre de lignes dans le tableau, vous pouvez utiliser:

tableView.scrollToRow(
    at: IndexPath(item: listCountInSection-1, section: sectionCount - 1 ), 
    at: .top, 
    animated: true)
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.