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.

Respostas:


148

Eu acredito que chamando

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

fará o que você quiser.


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

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

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

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

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.


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

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

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.


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.


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


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

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

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


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

3

No Swift 3.0

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

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

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

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.


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

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

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


1

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


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

1

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

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

1

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


0

No Swift, você só precisa

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

para rolar automaticamente para o botão


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

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)
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.