ios - UITableView carga más cuando se desplaza hacia abajo como aplicación de Facebook
iphone pagination (12)
Rápido
Método 1: se desplazó hacia abajo
Aquí está la versión Swift de la respuesta de Pedro Romão . Cuando el usuario deja de desplazarse, comprueba si ha llegado al final.
func scrollViewDidEndDragging(scrollView: UIScrollView, willDecelerate decelerate: Bool) {
// UITableView only moves in one direction, y axis
let currentOffset = scrollView.contentOffset.y
let maximumOffset = scrollView.contentSize.height - scrollView.frame.size.height
// Change 10.0 to adjust the distance from bottom
if maximumOffset - currentOffset <= 10.0 {
self.loadMore()
}
}
Método 2: se alcanzó la última fila
Y aquí está la versión Swift de la respuesta de shinyuX . Comprueba si el usuario ha llegado a la última fila.
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
// set up cell
// ...
// Check if the last row number is the same as the last current data element
if indexPath.row == self.dataArray.count - 1 {
self.loadMore()
}
}
Ejemplo de un método loadMore()
Configuré estas tres variables de clase para buscar lotes de datos.
// number of items to be fetched each time (i.e., database LIMIT)
let itemsPerBatch = 50
// Where to start fetching items (database OFFSET)
var offset = 0
// a flag for when all database items have already been loaded
var reachedEndOfItems = false
Esta es la función para cargar más elementos de la base de datos en la vista de tabla.
func loadMore() {
// don''t bother doing another db query if already have everything
guard !self.reachedEndOfItems else {
return
}
// query the db on a background thread
DispatchQueue.global(qos: .background).async {
// determine the range of data items to fetch
var thisBatchOfItems: [MyObjects]?
let start = self.offset
let end = self.offset + self.itemsPerBatch
// query the database
do {
// SQLite.swift wrapper
thisBatchOfItems = try MyDataHelper.findRange(start..<end)
} catch _ {
print("query failed")
}
// update UITableView with new batch of items on main thread after query finishes
DispatchQueue.main.async {
if let newItems = thisBatchOfItems {
// append the new items to the data source for the table view
self.myObjectArray.appendContentsOf(newItems)
// reload the table view
self.tableView.reloadData()
// check if this was the last of the data
if newItems.count < self.itemsPerBatch {
self.reachedEndOfItems = true
print("reached end of data. Batch count: /(newItems.count)")
}
// reset the offset for the next data query
self.offset += self.itemsPerBatch
}
}
}
}
Estoy desarrollando una aplicación que usa SQLite. Quiero mostrar una lista de usuarios (UITableView) usando un mecanismo de paginación. ¿Podría alguien decirme cómo cargar más datos en mi lista cuando el usuario se desplaza hasta el final de la lista (como en la página de inicio de la aplicación de Facebook)?
Detalles
xCode 8.3.1, Swift 3.1
Solución
import UIKit
class LoadMoreActivityIndicator {
let spacingFromLastCell: CGFloat
let spacingFromLastCellWhenLoadMoreActionStart: CGFloat
let activityIndicatorView: UIActivityIndicatorView
weak var tableView: UITableView!
private var defaultY: CGFloat {
return tableView.contentSize.height + spacingFromLastCell
}
init (tableView: UITableView, spacingFromLastCell: CGFloat, spacingFromLastCellWhenLoadMoreActionStart: CGFloat) {
self.tableView = tableView
self.spacingFromLastCell = spacingFromLastCell
self.spacingFromLastCellWhenLoadMoreActionStart = spacingFromLastCellWhenLoadMoreActionStart
let size:CGFloat = 40
let frame = CGRect(x: (tableView.frame.width-size)/2, y: tableView.contentSize.height + spacingFromLastCell, width: size, height: size)
activityIndicatorView = UIActivityIndicatorView(frame: frame)
activityIndicatorView.color = .black
activityIndicatorView.isHidden = false
activityIndicatorView.autoresizingMask = [.flexibleLeftMargin, .flexibleRightMargin]
tableView.addSubview(activityIndicatorView)
activityIndicatorView.isHidden = isHidden
}
required init(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private var isHidden: Bool {
if tableView.contentSize.height < tableView.frame.size.height {
return true
} else {
return false
}
}
func scrollViewDidScroll(scrollView: UIScrollView, loadMoreAction: ()->()) {
let offsetY = scrollView.contentOffset.y
activityIndicatorView.isHidden = isHidden
if !isHidden && offsetY >= 0 {
let contentDelta = scrollView.contentSize.height - scrollView.frame.size.height
let offsetDelta = offsetY - contentDelta
let newY = defaultY-offsetDelta
if newY < tableView.frame.height {
activityIndicatorView.frame.origin.y = newY
} else {
if activityIndicatorView.frame.origin.y != defaultY {
activityIndicatorView.frame.origin.y = defaultY
}
}
if !activityIndicatorView.isAnimating {
if offsetY > contentDelta && offsetDelta >= spacingFromLastCellWhenLoadMoreActionStart && !activityIndicatorView.isAnimating {
activityIndicatorView.startAnimating()
loadMoreAction()
}
}
if scrollView.isDecelerating {
if activityIndicatorView.isAnimating && scrollView.contentInset.bottom == 0 {
UIView.animate(withDuration: 0.3) { [weak self] in
if let bottom = self?.spacingFromLastCellWhenLoadMoreActionStart {
scrollView.contentInset = UIEdgeInsetsMake(0, 0, bottom, 0)
}
}
}
}
}
}
func loadMoreActionFinshed(scrollView: UIScrollView) {
let contentDelta = scrollView.contentSize.height - scrollView.frame.size.height
let offsetDelta = scrollView.contentOffset.y - contentDelta
if offsetDelta >= 0 {
// Animate hiding when activity indicator displaying
UIView.animate(withDuration: 0.3) {
scrollView.contentInset = UIEdgeInsetsMake(0, 0, 0, 0)
}
} else {
// Hiding without animation when activity indicator displaying
scrollView.contentInset = UIEdgeInsetsMake(0, 0, 0, 0)
}
activityIndicatorView.stopAnimating()
activityIndicatorView.isHidden = false
}
}
Uso
extension ViewController: UITableViewDelegate {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
activityIndicator.scrollViewDidScroll(scrollView: scrollView) {
DispatchQueue.global(qos: .utility).async {
for i in 0..<3 {
print(i)
sleep(1)
}
DispatchQueue.main.async { [weak self] in
self?.activityIndicator.loadMoreActionFinshed(scrollView: scrollView)
}
}
}
}
}
Muestra completa
ViewController.swift
import UIKit
class ViewController: UIViewController {
var count = 30
@IBOutlet weak var tableView: UITableView!
fileprivate var activityIndicator: LoadMoreActivityIndicator!
override func viewDidLoad() {
super.viewDidLoad()
tableView.dataSource = self
tableView.delegate = self
tableView.tableFooterView = UIView()
activityIndicator = LoadMoreActivityIndicator(tableView: tableView, spacingFromLastCell: 10, spacingFromLastCellWhenLoadMoreActionStart: 60)
print(tableView.frame)
}
}
extension ViewController: UITableViewDataSource {
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = UITableViewCell()
cell.textLabel?.text = "/(indexPath)"
return cell
}
}
extension ViewController: UITableViewDelegate {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
activityIndicator.scrollViewDidScroll(scrollView: scrollView) {
DispatchQueue.global(qos: .utility).async {
for i in 0..<3 {
print("!!!!!!!!! /(i)")
sleep(1)
}
DispatchQueue.main.async { [weak self] in
self?.activityIndicator.loadMoreActionFinshed(scrollView: scrollView)
}
}
}
}
}
Main.storyboard
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12120" systemVersion="16E195" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
<device id="retina4_7" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12088"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="tne-QT-ifu">
<objects>
<viewController id="BYZ-38-t0r" customClass="ViewController" customModule="_20269474" customModuleProvider="target" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
<viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<tableView clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="default" rowHeight="44" sectionHeaderHeight="28" sectionFooterHeight="28" translatesAutoresizingMaskIntoConstraints="NO" id="6la-L6-Fo3">
<rect key="frame" x="0.0" y="20" width="375" height="647"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
</tableView>
</subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="6la-L6-Fo3" firstAttribute="bottom" secondItem="wfy-db-euE" secondAttribute="top" id="GvO-lp-VW8"/>
<constraint firstAttribute="trailing" secondItem="6la-L6-Fo3" secondAttribute="trailing" id="aGF-Ie-T6Y"/>
<constraint firstItem="6la-L6-Fo3" firstAttribute="leading" secondItem="8bC-Xf-vdC" secondAttribute="leading" id="obO-Sn-WYo"/>
<constraint firstItem="6la-L6-Fo3" firstAttribute="top" secondItem="y3c-jy-aDJ" secondAttribute="bottom" id="zp5-B1-sWe"/>
</constraints>
</view>
<connections>
<outlet property="tableView" destination="6la-L6-Fo3" id="4lR-IX-vtT"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="48.799999999999997" y="37.331334332833585"/>
</scene>
</scenes>
</document>
Resultado
A continuación, el enlace proporcionará un código de muestra. # Swift3
El usuario necesita desplegar la última celda de la vista de tabla, al menos la altura de 2 celdas para obtener más datos del servidor.
Encontrará la celda Proceso también para mostrar el proceso de carga como en la última celda.
Está en Swift3
Es mejor utilizar el método willDisplayCell
para verificar si se cargará la celda. Una vez que obtengamos el indexPath.row
actual. indexPath.row
es la última vez que podemos cargar más células. Esto cargará más celdas al desplazarse hacia abajo.
- (void)tableView:(UITableView *)tableView
willDisplayCell:(UITableViewCell *)cell
forRowAtIndexPath:(NSIndexPath *)indexPath
{
// check if indexPath.row is last row
// Perform operation to load new Cell''s.
}
Implementé una solución que encontré en , y funciona bien, pero creo que la solución de shinyuX es muy fácil de implementar y funciona bien para mi propuesta. Si alguien quiere una solución diferente, puede usar esta a continuación.
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate{
// UITableView only moves in one direction, y axis
CGFloat currentOffset = scrollView.contentOffset.y;
CGFloat maximumOffset = scrollView.contentSize.height - scrollView.frame.size.height;
//NSInteger result = maximumOffset - currentOffset;
// Change 10.0 to adjust the distance from bottom
if (maximumOffset - currentOffset <= 10.0) {
[self loadOneMorePage];
//[self methodThatAddsDataAndReloadsTableView];
}
}
Solo quiero compartir este enfoque:
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
NSLog(@"%@", [[YourTableView indexPathsForVisibleRows] lastObject]);
[self estimatedTotalData];
}
- (void)estimatedTotalData
{
long currentRow = ((NSIndexPath *)[[YourTableView indexPathsForVisibleRows] lastObject]).row;
long estimateDataCount = 25;
while (currentRow > estimateDataCount)
{
estimateDataCount+=25;
}
dataLimit = estimateDataCount;
if (dataLimit == currentRow+1)
{
dataLimit+=25;
}
NSLog(@"dataLimit :%ld", dataLimit);
[self requestForData];
// this answers the question..
//
if(YourDataSource.count-1 == currentRow)
{
NSLog(@"LAST ROW"); //loadMore data
}
}
NSLog(...);
salida sería algo así como:
<NSIndexPath: 0xc0000000002e0016> {length = 2, path = 0 - 92}
dataLimit :100
<NSIndexPath: 0xc000000000298016> {length = 2, path = 0 - 83}
dataLimit :100
<NSIndexPath: 0xc000000000278016> {length = 2, path = 0 - 79}
dataLimit :100
<NSIndexPath: 0xc000000000238016> {length = 2, path = 0 - 71}
dataLimit :75
<NSIndexPath: 0xc0000000001d8016> {length = 2, path = 0 - 59}
dataLimit :75
<NSIndexPath: 0xc0000000001c0016> {length = 2, path = 0 - 56}
dataLimit :75
<NSIndexPath: 0xc000000000138016> {length = 2, path = 0 - 39}
dataLimit :50
<NSIndexPath: 0xc000000000120016> {length = 2, path = 0 - 36}
dataLimit :50
<NSIndexPath: 0xc000000000008016> {length = 2, path = 0 - 1}
dataLimit :25
<NSIndexPath: 0xc000000000008016> {length = 2, path = 0 - 1}
dataLimit :25
Esto es bueno para mostrar datos almacenados localmente. Inicialmente declaro el dataLimit a 25, eso significa que uitableview tendrá 0-24 (inicialmente).
Si el usuario se desplazó hacia abajo y la última celda está visible, se agregará dataLimit
con 25 ...
Nota: Esto es más como una paginación de datos UITableView, :)
Una opción más para usar ( Swift 3 e iOS 10+):
class DocumentEventsTableViewController: UITableViewController, UITableViewDataSourcePrefetching {
var currentPage: Int = 1
override func viewDidLoad() {
super.viewDidLoad()
self.tableView.prefetchDataSource = self
}
func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
let upcomingRows = indexPaths.map { $0.row }
if let maxIndex = upcomingRows.max() {
let nextPage: Int = Int(ceil(Double(maxIndex) / Double(APICall.defaultPageSize))) + 1
if nextPage > currentPage {
// Your function, which attempts to load respective page from the local database
loadLocalData(page: nextPage)
// Your function, which makes a network request to fetch the respective page of data from the network
startLoadingDataFromNetwork(page: nextPage)
currentPage = nextPage
}
}
}
}
En el caso de páginas bastante pequeñas (~ 10 elementos), es posible que desee agregar datos manualmente para las páginas 1 y 2, ya que nextPage podría estar en algún lugar entre 1 y 2 hasta que la tabla tenga algunos elementos que se desplazarán bien. Pero funcionará de maravilla para todas las páginas siguientes.
Use el límite y la compensación en sus consultas y complete su vista de tabla con ese contenido. Cuando el usuario se desplaza hacia abajo, carga el siguiente desplazamiento.
Implemente el tableView:willDisplayCell:forRowAtIndexPath:
en su UITableViewDelegate
y verifique si es la última fila
Puede hacerlo agregando un control de dónde se encuentra en el método cellForRowAtIndexPath:
. Este método es fácil de entender y de implementar:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
// Classic start method
static NSString *cellIdentifier = @"MyCell";
MyCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier];
if (!cell)
{
cell = [[MyCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:MainMenuCellIdentifier];
}
MyData *data = [self.dataArray objectAtIndex:indexPath.row];
// Do your cell customisation
// cell.titleLabel.text = data.title;
BOOL lastItemReached = [data isEqual:[[self.dataArray] lastObject]];
if (!lastItemReached && indexPath.row == [self.dataArray count] - 1)
{
[self launchReload];
}
}
EDITAR: agregó una marca en el último elemento para evitar llamadas recurrentes. Tendrá que implementar el método que define si se ha alcanzado o no el último elemento.
EDIT2: explicó lastItemReached
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
if (news.count == 0) {
return 0;
} else {
return news.count + 1 ;
}
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
@try {
uint position = (uint) (indexPath.row);
NSUInteger row = [indexPath row];
NSUInteger count = [news count];
//show Load More
if (row == count) {
UITableViewCell *cell = nil;
static NSString *LoadMoreId = @"LoadMore";
cell = [tableView dequeueReusableCellWithIdentifier:LoadMoreId];
if (cell == nil) {
cell = [[UITableViewCell alloc]
initWithStyle:UITableViewCellStyleDefault
reuseIdentifier:LoadMoreId];
}
if (!hasMoreLoad) {
cell.hidden = true;
} else {
cell.textLabel.text = @"Load more items...";
cell.textLabel.textColor = [UIColor blueColor];
cell.textLabel.font = [UIFont boldSystemFontOfSize:14];
NSLog(@"Load more");
if (!isMoreLoaded) {
isMoreLoaded = true;
[self performSelector:@selector(loadMoreNews) withObject:nil afterDelay:0.1];
}
}
return cell;
} else {
NewsRow *cell = nil;
NewsObject *newsObject = news[position];
static NSString *CellIdentifier = @"NewsRow";
cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
// Load the top-level objects from the custom cell XIB.
NSArray *topLevelObjects = [[NSBundle mainBundle] loadNibNamed:CellIdentifier owner:self options:nil];
// Grab a pointer to the first object (presumably the custom cell, as that''s all the XIB should contain).
cell = topLevelObjects[0];
// Configure the cell...
}
cell.title.text = newsObject.title;
return cell;
}
}
@catch (NSException *exception) {
NSLog(@"Exception occurred: %@, %@", exception, [exception userInfo]);
}
return nil;
}
muy buena explicación en esta publicación.
http://useyourloaf.com/blog/2010/10/02/dynamically-loading-new-rows-into-a-table.html
simple, tiene que agregar la última fila y ocultarla, y cuando la fila de la tabla golpea la última fila, muestra la fila y carga más elementos.
- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath {
NSInteger lastSectionIndex = [tableView numberOfSections] - 1;
NSInteger lastRowIndex = [tableView numberOfRowsInSection:lastSectionIndex] - 1;
if ((indexPath.section == lastSectionIndex) && (indexPath.row == lastRowIndex)) {
// This is the last cell
[self loadMore];
}
}
Si está utilizando Core Data y NSFetchedResultsController
, loadMore
podría tener el siguiente aspecto:
// Load more
- (void)loadMore {
[self.fetchedResultsController.fetchRequest setFetchLimit:newFetchLimit];
[NSFetchedResultsController deleteCacheWithName:@"cache name"];
NSError *error;
if (![self.fetchedResultsController performFetch:&error]) {
// Update to handle the error appropriately.
NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
}
[self.tableView reloadData];
}
-(void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath {
NSInteger sectionsAmount = [tableView numberOfSections];
NSInteger rowsAmount = [tableView numberOfRowsInSection:[indexPath section]];
if ([indexPath section] == sectionsAmount - 1 && [indexPath row] == rowsAmount - 1) {
//get last row
if (!isSearchActive && !isFilterSearchActive) {
if (totalRecords % 8 == 0) {
int64_t delayInSeconds = 2.0;
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, delayInSeconds * NSEC_PER_SEC);
dispatch_after(popTime, dispatch_get_main_queue(), ^(void) {
[yourTableView beginUpdates];
[yourTableView insertRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationAutomatic];
[yourTableView endUpdates];
});
}
}
}
}