Como rolar para a parte inferior de um UITableView no iPhone antes que a exibição apareça

136

Eu tenho um UITableViewque é preenchido com células de altura variável. Eu gostaria que a tabela rolasse para baixo quando a visualização fosse exibida.

Atualmente, tenho a seguinte função

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

log é uma matriz mutável que contém os objetos que compõem o conteúdo de cada célula.

O código acima funciona bem, no viewDidAppearentanto, isso tem o efeito colateral infeliz de exibir a parte superior da tabela quando a exibição aparece pela primeira vez e depois pular para o fundo. Eu preferiria se o table viewpudesse ser rolado para baixo antes de aparecer.

Eu tentei a rolagem viewWillAppeare, viewDidLoadem ambos os casos, os dados ainda não foram carregados na tabela e ambos lançam uma exceção.

Qualquer orientação seria muito apreciada, mesmo que seja apenas um caso de me dizer o que tenho é tudo o que é possível.

acqu13sce
fonte

Respostas:

148

Eu acredito que chamando

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

fará o que você quiser.

Jacob Relkin
fonte
14
Perfeito, obrigado. Criei um CGPoint com um valor Y suficientemente alto que o fará sempre exibir a parte inferior. Uma vez que o ponto de vista foi carregado I pode usar (self.table.contentSize.height - self.table.frame.size.height) para mover para o fundo com o mesmo método
acqu13sce
8
Embora esta seja a resposta perfeita, como não precisamos fazer o cálculo de quantas células, altura da tableview etc. MAS, eu indicaria que precisamos chamar isso antes de recarregar a tableview ... Não funcionará se escreva isto depois[table reloadData];
Fahim Parkar
4
não funciona no iOS 10-12 - mesa simplesmente desaparecer por primeira vez
Vyachaslav Gerchicov
2
Ou basta rolar para CGPoint(x: 0, y: tableView.contentSize.height)?
Amber K
5
Caramba!!! Faz a tabela desaparecer: -o. Melhor uso [self.table scrollToRowAtIndexPath: indexPath atScrollPosition: UITableViewScrollPositionBottom animated: NO];
Carl Hine
122

Eu acho que a maneira mais fácil é esta:

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

Versão Swift 3:

if messages.count > 0 {
    userDefinedOptionsTableView.scrollToRow(at: IndexPath(item:messages.count-1, section: 0), at: .bottom, animated: true)
}
Chamira Fernando
fonte
3
Ele não funciona se a célula é mais alto do que o UITableView
Olav Gausaker
3
A célula é mais alta que o UITableView? nunca ouvi esse caso de uso.
Chamira Fernando 15/09
@ChamiraFernando essa é a maneira mais fácil :) #
AITAALI_ABDERRAHMANE
Observe que pode fazer sentido substituir o messages.countpelo já implementado myTableView.dataSource!.tableView(myTableView, numberOfRowsInSection: 0). Sim, é mais longo, mas pode evitar a repetição de código. Você também precisa lidar com o opcional dataSource(não force a desembrulhar como nesta amostra).
Nikolay Suvandzhiev
@ChamiraFernando Eu sei que essa pergunta é antiga, mas só porque você nunca viu, isso não significa que isso não acontece. Para responder sua pergunta, aplicativos como o Foursquare podem ter essa situação, na qual o usuário escreve uma revisão. A altura da célula é maior que a altura da visualização da tabela. É uma situação perfeitamente boa.
Caio
121

Da resposta de Jacob , este é o código:

- (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];
    }
}
Osama F Elias
fonte
No iOS 11, você deve usar a altura do quadro de exibição de tabela ajustada:UIEdgeInsetsInsetRect(self.messagesTableView.frame, self.messagesTableView.safeAreaInsets).height
Slav
41

Se você precisar rolar para o final EXATO do conteúdo, poderá fazer o seguinte:

- (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
fonte
7
Funciona com a execução automática, mas é importante chamar esse método a partir de viewDidLayoutSubviews
Omaty
Poderia explicar por que precisamos fazer isso yOffset = self.tableView.contentSize.height - self.tableView.bounds.size.height; :? Obrigado.
Unheilig
3
@ Unheilig Se você rolar para self.tableView.contentSize.heighto conteúdo da exibição de tabela, pode não estar visível, porque você rolar abaixo do conteúdo. Portanto, você precisa rolar para uma "lacuna visível da visualização da tabela" acima do final da visualização da tabela.
Hans One
31

Estou usando o layout automático e nenhuma das respostas funcionou para mim. Aqui está minha solução que finalmente funcionou:

@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
fonte
1
Isso quase funciona para mim, mas recebo uma falha gráfica estranha enquanto meus dados da tabela são carregados de uma API externa. No meu caso, preciso ligar setContentOffsetem outro momento em que os dados foram buscados e a tableview recarregada?
jmoz
Tente definir o deslocamento em um manipulador de conclusão de sua solicitação.
RaffAl
2
Isso não funciona no ios 10 - simplesmente mostra uma tabela com um fundo preto #
RunLoop
2
Em vez de usar CGFLOAT_MAX, usei contentSize.height - frame.height + contentInset.bottomao definir o deslocamento do conteúdo inicial. Usar CGFLOAT_MAXparecia me atrapalhar.
Baza207
23

A solução aceita pelo @JacobRelkin não funcionou para mim no iOS 7.0 usando o Auto Layout.

Eu tenho uma subclasse personalizada de UIViewControllere adicionei uma variável de instância _tableViewcomo uma sub- visualização dele view. Posicionei _tableViewusando o Auto Layout. Tentei chamar esse método no final viewDidLoade até no viewWillAppear:. Nem funcionou.

Então, adicionei o seguinte método à minha subclasse personalizada 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];
    }
}

Ligando [self tableViewScrollToBottomAnimated:NO]no final dos viewDidLoadtrabalhos. Infelizmente, também faz tableView:heightForRowAtIndexPath:com que seja chamado três vezes para cada célula.

ma11hew28
fonte
23

Aqui está uma extensão que eu implementei no Swift 2.0. Essas funções devem ser chamadas após o tableviewcarregamento:

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
fonte
3
Isso é melhor do que usar o tamanho do conteúdo. Para Swift3. if self.numberOfRows (inSection: 0)> 0 {self.scrollToRow (em: IndexPath.init (linha: self.numberOfRows (inSection: 0) -1, seção: 0) -1, seção: 0), em: .bottom, animado: animado}}
Soohwan Park 23/03/19
se scrollToLastRow esse método não funcionar perfeitamente, basta adicionar self.layoutIfNeeded () e funcionar perfeitamente !!
Yogesh Patel
16

Detalhes

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

Código

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

Uso

tableView.scrollToBottom(animated: true)

Amostra completa

Não se esqueça de colar o código da solução!

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
fonte
15

Na verdade, uma maneira "mais rápida" de fazê-lo rapidamente é:

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

trabalho Perfeito para mim.

XcodeNOOB
fonte
9

Eu queria que a tabela carregasse com o final da tabela mostrada no quadro. Eu achei usando

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

não funcionou porque deu um erro quando a altura da tabela era menor que a altura do quadro. Observe que minha tabela possui apenas uma seção.

A solução que funcionou para mim foi implementar o seguinte código no 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];
    }
}

O BOOL ivar initialLoad é configurado como TRUE em viewDidLoad.

TJ
fonte
Você precisa chamar scrollToRowAtIndexPathem tudo? Você já está ligando setContentOffsetdepois, o que pode tornar a primeira chamada inútil.
Carlos P
9

Para 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
fonte
5

Você deve usar em seu UITableViewScrollPositionBottomlugar.

Erphan Rajput
fonte
5

Para 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
fonte
3
Isso não responde à pergunta do OP, é isso que estava funcionando desde o início. Além disso, você deve chamar super.viewDidAppear
streem
4

É claro que é um bug. Provavelmente em algum lugar do seu código que você usa table.estimatedRowHeight = value(por exemplo, 100). Substitua esse valor pelo valor mais alto que você acha que uma altura de linha poderia obter , por exemplo, 500. Isso deve resolver o problema em combinação com o seguinte código:

//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
fonte
4

Depois de muitas brincadeiras, foi isso que funcionou para mim:

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

A chave acabou por ser não embrulho scrollToRowAtIndexPathdentro de dispatch_asynccomo alguns sugeriram, mas simplesmente segui-lo com uma chamada paralayoutIfNeeded .

Pelo que entendi, chamar o método de rolagem no encadeamento atual garante que o deslocamento da rolagem seja definido imediatamente, antes da exibição da exibição. Quando eu estava despachando para o encadeamento principal, a exibição foi exibida por um instante antes do rolagem entrar em vigor.

(Além disso, você precisa da viewHasAppearedbandeira, porque não deseja que goToBottomtoda vez que ela viewDidLayoutSubviewsfor chamada. É chamada, por exemplo, sempre que a orientação muda.)

skot
fonte
3

Usando as soluções acima, isso rolará para a parte inferior da sua tabela (somente se o conteúdo da tabela for carregado primeiro):

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

No Swift 3.0

self.tableViewFeeds.setContentOffset(CGPoint(x: 0, y: CGFLOAT_MAX), animated: true)
Amit Verma
fonte
2

Se você precisar carregar os dados de forma assíncrona antes de rolar para baixo, aqui está a solução possível:

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
fonte
2

Função no swift 3, role para baixo

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

você deveria adicionar

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

ou não rolará para baixo.


fonte
2

Use este código simples para rolar a tabela

NSInteger rows = [tableName numberOfRowsInSection:0];
if(rows > 0) {
    [tableName scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:rows-1 inSection:0]
                     atScrollPosition:UITableViewScrollPositionBottom
                             animated:YES];
}
Bibin Joseph
fonte
1
Isso é basicamente o que o OP disse que já tentou. Esta resposta não aborda a questão do OP de como fazê-lo funcionar corretamente no viewWillAppear.
Jk7
2

Obrigado Jacob pela resposta. realmente útil se alguém interessante com monotouch c # versão

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
fonte
1

No iOS, isso funcionou bem para mim

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];

Ele precisa ser chamado de viewDidLayoutSubviews

Josip B.
fonte
1

[self.tableViewInfo scrollRectToVisible: CGRectMake (0, self.tableViewInfo.contentSize.height-self.tableViewInfo.height, self.tableViewInfo.width, self.tableViewInfo.height) animado: SIM];

Leetvin
fonte
1

A resposta aceita não funcionou com minha tabela (milhares de linhas, carregamento dinâmico), mas o código abaixo funciona:

- (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
fonte
1

Não há necessidade de rolagem; você pode fazer isso usando este código:

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

Se você estiver configurando o quadro para tableview programaticamente, verifique se está definindo o quadro corretamente.

Narasimha Nallamsetty
fonte
0

No Swift, você só precisa

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

para rolar automaticamente para o botão

Even Cheng
fonte
0

No swift 3.0 Se você deseja acessar qualquer célula específica da tableview, altere o valor do índice da célula, como o valor "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
fonte
0

Acredito que soluções antigas não funcionam com o swift3.

Se você souber o número de linhas na tabela, poderá usar:

tableView.scrollToRow(
    at: IndexPath(item: listCountInSection-1, section: sectionCount - 1 ), 
    at: .top, 
    animated: true)
ymutlu
fonte