Implementación de Carga Dinámica en UITableView con Patrones iOS

En el desarrollo de aplicaciones iOS, es común requerir que una lista se actualice dinámicamente cuando el usuario llega al final de la vista visible. Este patrón suele implementarse utilizando un pie de página personalizado (tableFooterView) dentro de un UITableView. Al hacer clic en un botón de carga, se simula una petición de red, se muestra un indicador de actividad y, tras un retardo, se añaden nuevos elementos a los datos subyacentes.

Estructura del Proyecto

Para mantener la arquitectura limpia, es fundamental separar la lógica de presentación de la lógica de control. A continuación, se presentan dos enfoques para gestionar la interacción entre la vista de carga inferiro y el controlador principal.

La vista de pie de página se configura como el tableFooterView del TableView:

self.tableView.tableFooterView = paginationFooter;El diseño se gestiona mediante archivos XIB para controlar adecuadamente los márgenes y el estado visual (botón vs indicador de carga).

Enfoque 1: Referencia Directa (Acoplamiento Fuerte)

Esta metodología implica asignar una propiedad en la vista que apuntaba directamente al controlador. Aunque funcional, genera dependencias circulares y dificulta la reutilización del componente.

Modelo de Datos (BusinessItem)

// BusinessItem.h
#import <Foundation/Foundation.h>

@interface BusinessItem : NSObject
@property (nonatomic, copy) NSString *itemIcon;
@property (nonatomic, copy) NSString *purchaseCount;
@property (nonatomic, copy) NSString *itemName;
@property (nonatomic, copy) NSString *costPrice;

// Constructor desde diccionario
- (instancetype)initWithDictionary:(NSDictionary *)dict;
+ (instancetype)itemWithDict:(NSDictionary *)dict;
@end

// BusinessItem.m
@implementation BusinessItem
- (instancetype)initWithDictionary:(NSDictionary *)dict {
    self = [super init];
    if (self) {
        [self setValuesForKeysWithDictionary:dict];
    }
    return self;
}
+ (instancetype)itemWithDict:(NSDictionary *)dict {
    return [[self alloc] initWithDictionary:dict];
}
@end

Vista de Pie de Página (PaginationFooterView)

En este enfoque, la vista mantiene una referencia directa al objeto controlador.

// PaginationFooterView.h
#import <UIKit/UIKit.h>
@class MainTableViewController;

@interface PaginationFooterView : UIView
// Referencia directa al controlador (No recomendado en proyectos escalables)
@property (nonatomic, strong) MainTableViewController *viewController;
@end
// PaginationFooterView.m
#import "PaginationFooterView.h"
// Se importa el encabezado del controlador para tipado directo
# import "MainTableViewController.h" 

@interface PaginationFooterView ()
@property (weak, nonatomic) IBOutlet UIActivityIndicatorView *spinner;
@property (weak, nonatomic) IBOutlet UIButton *actionButton;
@end

@implementation PaginationFooterView

- (IBAction)onLoadAction {
    // Ocultar botón y mostrar spinner
    self.actionButton.hidden = YES;
    self.spinner.hidden = NO;
    
    // Simular latencia de red (3 segundos)
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        // Llamada directa al método del controlador
        [self.viewController fetchMoreItems];
        
        // Restaurar estado de UI
        self.spinner.hidden = YES;
        self.actionButton.hidden = NO;
    });
}
@end

Enfoque 2: Patrón Delegado (Acoplamiento Débil)

La mejor práctica recomendada utiliza protocolos para desacoplar las vistas de los controladores. La vista de carga no necesita conocer la clase específica que la posee, solo saber quién cumple con el protocolo necesario.

Definición del Protocolo

Se define un protocolo que declara qué acción debe realizar el delegado cuando se solicita cargar más.

// PaginationFooterView.h
#import <UIKit/UIKit.h>

NS_ASSUME_NONNULL_BEGIN

@protocol PaginationDelegate <NSObject>
- (void)didRequestPaginatedContent;
@end

@interface PaginationFooterView : UIView
@property (nonatomic, weak) id<PaginationDelegate> _Nullable delegate;
@end

NS_ASSUME_NONNULL_END

Lógica de Actualización (PaginationFooterView.m)

En lugar de llamar a un método específico del controlador, se notifica al delegado asignado.

@implementation PaginationFooterView

- (IBAction)onLoadAction {
    self.actionButton.hidden = YES;
    self.spinner.hidden = NO;
    
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        // Comunicar al delegado que necesita más datos
        if ([self.delegate respondsToSelector:@selector(didRequestPaginatedContent)]) {
            [self.delegate didRequestPaginatedContent];
        }
        
        self.spinner.hidden = YES;
        self.actionButton.hidden = NO;
    });
}
@end

Implementación en el Controlador (MainTableViewController.m)

El controlador adopta el protocolo y gestiona la actualización del conjunto de datos.

@interface MainTableViewController () <UITableViewDataSource, UITableViewDelegate, PaginationDelegate>
@property (nonatomic, strong) UITableView *productTable;
@property (nonatomic, strong) NSMutableArray *itemsList;
@end

@implementation MainTableViewController

#pragma mark - Manejo de Paginación

- (void)didRequestPaginatedContent {
    // Añadir 3 ítems simulados
    for (int i = 0; i < 3; i++) {
        NSDictionary *data = @{
            @"name": [NSString stringWithFormat:@"Producto %lu", (unsigned long)i],
            @"icon": @"default_icon",
            @"price": @"50",
            @"count": @"15"
        };
        BusinessItem *newItem = [BusinessItem itemWithDict:data];
        [self.itemsList addObject:newItem];
    }
    
    // Actualizar interfaz
    [self.productTable reloadData];
}

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // Configurar altura de fila
    self.productTable.rowHeight = 80.0;
    
    // Instanciar el footer desde XIB
    UINib *nib = [UINib nibWithNibName:@"PaginationFooterView" bundle:nil];
    PaginationFooterView *footer = [[nib instantiateWithOwner:nil options:nil] firstObject];
    
    self.productTable.tableFooterView = footer;
    // Asignar al controlador como delegado
    footer.delegate = self;
}

#pragma mark - Carga Lazy de Datos

- (NSMutableArray *)itemsList {
    if (_itemsList == nil) {
        NSString *path = [[NSBundle mainBundle] pathForResource:@"products" ofType:@"plist"];
        NSArray *rawArray = [NSArray arrayWithContentsOfFile:path];
        NSMutableArray *mutableCopy = [NSMutableArray array];
        
        for (NSDictionary *dict in rawArray) {
            [mutableCopy addObject:[BusinessItem itemWithDict:dict]];
        }
        _itemsList = mutableCopy;
    }
    return _itemsList;
}

#pragma mark - DataSource del TableView

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    return self.itemsList.count;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    static NSString *cellID = @"ProductCell";
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellID];
    if (!cell) {
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:cellID];
    }
    
    BusinessItem *item = self.itemsList[indexPath.row];
    cell.textLabel.text = item.itemName;
    cell.detailTextLabel.text = item.costPrice;
    // Resto de configuración de UI...
    
    return cell;
}

@end

Etiquetas: iOS Objective-C UITableView DelegatePattern XIB

Publicado el 8-29 17:19