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.

acqu13sce
la source

Réponses:

148

Je crois que l'appel

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

fera ce que vous voulez.

Jacob Relkin
la source
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)
}
Chamira Fernando
la source
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];
    }
}
Oussama F Elias
la source
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];
}
Hans One
la source
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)];
    }
}
RaffAl
la source
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.

ma11hew28
la source
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)
        }
    }
}
Ryan Herubin
la source
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
    }
}
Vasily Bodnarchuk
la source
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.

XcodeNOOB
la source
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.

TJ
la source
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)
}
Segev
la source
5

Vous devriez utiliser à la UITableViewScrollPositionBottomplace.

Erphan Rajput
la source
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)
}
Irshad Qureshi
la source
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)
})
Mohsen Karbassi
la source
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.)

skot
la source
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)];
JimmyJammed
la source
3

Dans Swift 3.0

self.tableViewFeeds.setContentOffset(CGPoint(x: 0, y: CGFLOAT_MAX), animated: true)
Amit Verma
la source
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
            }
        }
    }
}
SoftDesigner
la source
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)
        }
    }
Tarik
la source
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.


la source
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];
}
Bibin Joseph
la source
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 );
    }
}
Mahesh
la source
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

Josip B.
la source
1

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

Leetvin
la source
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];
    }
}
user3246173
la source
1

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

[YOURTABLEVIEWNAME setContentOffset:CGPointMake(0, CGFLOAT_MAX)];
Anuj Kumar Rai
la source
1

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

Narasimha Nallamsetty
la source
0

Dans Swift, vous avez juste besoin

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

pour le faire défiler automatiquement vers le bouton

Même Cheng
la source
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)
    }
}
Amit Verma
la source
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)
ymutlu
la source