Pull-to-Refresh: conceptos básicos, RefreshControl y UIRefreshControl

Autor: IT Sectr Publicado: 2026-02-27 Tiempo de lectura: 8 min
Pull-to-Refresh es un patrón de interfaz móvil donde el usuario arrastra una lista hacia abajo con el dedo, iniciando la carga de datos nuevos. El gesto va acompañado de un indicador visual — un spinner giratorio o un icono animado — que desaparece cuando la carga se completa. Según el análisis de UX de Apple HIG, Pull-to-Refresh se ha convertido en el mecanismo estándar de actualización de contenido en feeds de noticias, redes sociales y clientes de correo desde su introducción en Tweetie (2008) y la posterior estandarización por Apple y Google.

Puntos clave

  • Pull-to-Refresh es un gesto de arrastrar la lista hacia abajo para actualizar los datos, acompañado de un indicador visual de carga.
  • En iOS se usa UIRefreshControl (iOS 6+), añadido a UITableViewController o UIScrollView mediante la propiedad refreshControl.
  • En Android se usa SwipeRefreshLayout (de Support Library) — un wrapper ViewGroup para RecyclerView o NestedScrollView.
  • Ambas API admiten personalización de colores, indicadores y callbacks mediante listener (iOS: UIRefreshControl.target-action, Android: setOnRefreshListener).
  • Pull-to-Refresh se bloquea automáticamente cuando la lista no está en la posición superior — el conflicto con el desplazamiento está excluido arquitectónicamente.

¿Qué es Pull-to-Refresh?

Pull-to-Refresh es un patrón de interfaz de usuario donde el usuario arrastra (pull down) una lista o área desplazable hacia abajo para actualizar el contenido. Visualmente, el gesto va acompañado de un indicador de carga (spinner) que aparece en la parte superior de la pantalla y desaparece tras recibir los datos. El patrón fue popularizado por la aplicación Tweetie para iPhone (2008) y posteriormente estandarizado por Apple (iOS 6 — UIRefreshControl) y Google (Android Support Library — SwipeRefreshLayout).

Desde un punto de vista técnico, Pull-to-Refresh es una combinación de paneo (seguimiento del desplazamiento del dedo) y un disparador al alcanzar un umbral. El usuario arrastra la lista hacia abajo, superando la resistencia (overscroll resistivo), y tras exceder el umbral (~80px en iOS, ~64dp en Android), se inicia la animación del indicador y la carga asíncrona. Si el usuario suelta el dedo antes del umbral — la lista vuelve a su posición original sin actualizarse.

Según las Guías de Material Design, Pull-to-Refresh no debe usarse para navegación o cambio de pestañas — su único propósito es la actualización de datos. En IT Sectr, usamos Pull-to-Refresh en feeds de noticias, listas de pedidos y chats donde la frescura de los datos es crítica para la experiencia del usuario.

Pull-to-Refresh en iOS: UIRefreshControl

UIRefreshControl es un control estándar de iOS para Pull-to-Refresh, disponible desde iOS 6. UIRefreshControl se añade a UITableViewController mediante la propiedad refreshControl (iOS 10+) o como subview de la tabla en versiones anteriores. Incluye un spinner integrado con color personalizable (tintColor), atributo title y una cadena atribuida con un título (por ejemplo, “Actualizando...”).

UIRefreshControl funciona mediante un mecanismo target-action: cuando se activa el gesto, se llama al método especificado (por ejemplo, refresh(_:)). Dentro del método se realiza la carga asíncrona de datos. Tras finalizar, se llama a endRefreshing(), que oculta el indicador con animación. UIRefreshControl gestiona automáticamente la sensibilidad del gesto — solo se activa cuando la tabla está en la posición superior (contentOffset.y <= 0).

La propiedad tintColor establece el color del spinner. Los atributos title permiten mostrar texto como “Actualizado hace 2 minutos” tras finalizar. Desde iOS 10, UIRefreshControl admite animaciones personalizadas mediante UIActivityIndicatorView o vistas personalizadas persistentes. En IT Sectr, configuramos tintColor según la marca y mostramos la hora de la última actualización mediante attributedTitle — esto aumenta la confianza del usuario en los datos.

Pull-to-Refresh en Android: SwipeRefreshLayout

SwipeRefreshLayout es un ViewGroup de Android Support Library (androidx.swiperefreshlayout) que envuelve contenido desplazable (RecyclerView, NestedScrollView, ListView) y añade funcionalidad Pull-to-Refresh. A diferencia de UIRefreshControl (que es un control, no un contenedor), SwipeRefreshLayout es un contenedor que intercepta los eventos táctiles del hijo y activa el indicador de actualización al superar el umbral.

SwipeRefreshLayout utiliza un indicador de progreso circular Material Design con personalización de color mediante setColorSchemeColors(). El método setOnRefreshListener establece el callback onRefresh(), en el que se realiza la carga asíncrona. Tras finalizar, se llama a setRefreshing(false) para ocultar el indicador. Importante: setRefreshing(true) vuelve a llamar a onRefresh() — por lo tanto, para la actualización programática, use una bandera o un método post.

La propiedad setProgressBackgroundColorSchemeResource cambia el fondo del indicador. setSize(SwipeRefreshLayout.LARGE) — tamaño del spinner. En el diseño XML, SwipeRefreshLayout envuelve RecyclerView: swipe_refresh_layout → recycler_view. Según Google I/O 2024, SwipeRefreshLayout se usa en el 85% de las aplicaciones Android con feeds de contenido. En IT Sectr, envolvemos todas las pantallas con listas cargadas asíncronamente en SwipeRefreshLayout — esto proporciona una UX consistente en todas las versiones de Android.

Material Pull-to-Refresh (Android 12+)

A partir de Android 12 (Material You), Google recomienda usar el nuevo Material Pull-to-Refresh de la librería material-1.6.0+ (androidx.compose.material3.pulltorefresh para Compose). La nueva API utiliza un indicador animado con soporte de animación spring y color adaptativo basado en el fondo de pantalla. SwipeRefreshLayout sigue siendo compatible para versiones inferiores a Android 12.

Mejores prácticas y errores comunes

Pull-to-Refresh es un patrón simple de implementar, pero contiene varios errores comunes que degradan la UX. Revisémoslos y las formas de evitarlos.

  • Actualización doble — el usuario puede tirar de la lista varias veces antes de que termine la carga. Solución: establezca un indicador isRefreshing al inicio y verifíquelo en onRefresh(). En iOS, endRefreshing() se llama solo después de completar; el bloqueo del gesto en UIRefreshControl está integrado.
  • Falta de retroalimentación — el indicador de carga debe aparecer estrictamente después de que el usuario haya superado el umbral. No muestre el indicador inmediatamente al tocar — es confuso. iOS y Android lo hacen automáticamente.
  • Ignorar el tiempo de actualización — si los datos se actualizan en 200 ms, el indicador debe mostrarse al menos 500 ms para que el usuario note la actualización. UIRefreshControl tiene un tiempo de animación mínimo; en Android, use Handler.postDelayed para el tiempo de visualización mínimo.
  • Conflicto con el teclado — con el teclado abierto, Pull-to-Refresh puede activarse accidentalmente. Oculte el teclado cuando comience el gesto mediante view.endEditing(true) en iOS y InputMethodManager.hideSoftInputFromWindow() en Android.
  • Usarlo no para actualizar — no use Pull-to-Refresh para navegación (cambio de pestañas, volver atrás). Esto viola las HIG de ambas plataformas y desorienta a los usuarios.

En IT Sectr, añadimos una comprobación isRefreshing en cada proyecto después de descubrir solicitudes duplicadas en los registros del servidor de prueba — resultó que los usuarios con dedos rápidos activaban la actualización hasta 3 veces seguidas.

Ejemplos de código en Swift y Kotlin

Ejemplo 1: UIRefreshControl en iOS (Swift)

Añade Pull-to-Refresh a UITableViewController con color de spinner personalizado y attributed title. Después de cargar los datos, el indicador se oculta.

swift
import UIKit

class FeedTableViewController: UITableViewController {

    private var items: [String] = []

    override func viewDidLoad() {
        super.viewDidLoad()

        tableView.refreshControl = UIRefreshControl()
        refreshControl?.tintColor = .systemBlue
        refreshControl?.attributedTitle = NSAttributedString(
            string: "Pull down to refresh"
        )
        refreshControl?.addTarget(
            self,
            action: #selector(refreshData),
            for: .valueChanged
        )
    }

    @objc private func refreshData() {
        DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) {
            self.items = FeedService().fetchLatest()
            self.tableView.reloadData()
            self.refreshControl?.endRefreshing()
        }
    }
}

La propiedad tableView.refreshControl (iOS 10+) establece UIRefreshControl. addTarget con el evento .valueChanged se activa cuando se activa el gesto. endRefreshing() es obligatorio — sin él, el indicador girará indefinidamente. La carga asíncrona se simula con DispatchQueue.main.asyncAfter — en un proyecto real, use URLSession o async/await.

Ejemplo 2: SwipeRefreshLayout en Android (Kotlin)

Envuelve RecyclerView en SwipeRefreshLayout con colores de indicador personalizados. onRefresh inicia la carga y oculta el indicador tras completar.

kotlin
class FeedFragment : Fragment() {

    private var _binding: FragmentFeedBinding? = null
    private val binding get() = _binding!!

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        _binding = FragmentFeedBinding.inflate(inflater, container, false)

        binding.swipeRefreshLayout.setColorSchemeColors(
            resources.getColor(R.color.brand_blue, null),
            resources.getColor(R.color.brand_green, null)
        )
        binding.swipeRefreshLayout.setOnRefreshListener {
            loadData()
        }
        return binding.root
    }

    private fun loadData() {
        viewModelScope.launch {
            try {
                val result = repository.getLatestFeed()
                adapter.submitList(result)
            } finally {
                binding.swipeRefreshLayout.isRefreshing = false
            }
        }
    }

    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
    }
}

setColorSchemeColors establece los colores del indicador giratorio Material Design. isRefreshing = false es obligatorio en finally para ocultar el indicador incluso en caso de error de carga. ViewModelScope.launch ejecuta una corrutina dentro del ciclo de vida del fragmento — cuando el fragmento se destruye, la corrutina se cancela automáticamente, evitando fugas de memoria.

Ejemplo 3: SwiftUI .refreshable (iOS 15+)

SwiftUI moderno proporciona el modificador .refreshable, que añade automáticamente Pull-to-Refresh a List o ScrollView.

swift
import SwiftUI

struct FeedView: View {

    @State private var items: [String] = []

    var body: some View {
        List(items, id: \.self) { item in
            Text(item)
        }
        .refreshable {
            items = await FeedService().fetchLatestAsync()
        }
    }
}

El modificador .refreshable acepta un async-closure que se ejecuta al hacer Pull-to-Refresh. SwiftUI muestra y oculta automáticamente el indicador de actualización, gestiona las condiciones de carrera (no reinicia la carga hasta que la actual termine) y adapta la animación a la plataforma. Para iOS 15+, esta es la forma preferida de implementar Pull-to-Refresh en SwiftUI.

Preguntas frecuentes

¿Funciona Pull-to-Refresh en SwiftUI?

Sí, SwiftUI proporciona el modificador .refreshable para List o ScrollView, disponible desde iOS 15. Dentro del closure se ejecuta código de carga asíncrona. SwiftUI gestiona automáticamente el indicador de actualización y bloquea los lanzamientos repetidos hasta que se complete la carga actual — este es el enfoque recomendado estándar para nuevos proyectos.

¿Cómo evitar la actualización doble?

Use el indicador isRefreshing: establézcalo en true cuando comience la carga y en false después de completar. En iOS, UIRefreshControl bloquea automáticamente las llamadas repetidas hasta que se llama a endRefreshing(). En Android, verifique SwipeRefreshLayout.isRefreshing al inicio de onRefresh(): si es true — return. Esto garantiza una solicitud por gesto.

¿Pull-to-Refresh entra en conflicto con el desplazamiento de la lista?

UIRefreshControl y SwipeRefreshLayout se activan solo cuando la lista está en la posición superior (contentOffset == 0). La arquitectura elimina el conflicto: mientras la lista esté desplazada aunque sea 1px, el gesto Pull-to-Refresh no se activa. Si se produce un conflicto — verifique nestedScrollingEnabled en Android o la presencia de GestureRecognizers personalizados que intercepten los toques.

Resumen

  • Pull-to-Refresh es un patrón de actualización de datos deslizando la lista hacia abajo, estandarizado por Apple y Google en todas las plataformas móviles.
  • UIRefreshControl en iOS — un control con target-action, tintColor, attributedTitle y endRefreshing() obligatorio.
  • SwipeRefreshLayout en Android — un contenedor ViewGroup con setOnRefreshListener, setColorSchemeColors e isRefreshing.
  • Material Pull-to-Refresh (Android 12+) — una nueva API con animación spring, recomendada para nuevos proyectos.
  • SwiftUI .refreshable — un modificador declarativo con async closure, disponible desde iOS 15.
  • El indicador isRefreshing evita la actualización doble — obligatorio en ambas plataformas.
  • Pull-to-Refresh no está diseñado para navegación — solo para actualización de contenido según Material Design y Apple HIG.

Desarrollaremos una aplicación móvil llave en mano

IT Sectr crea aplicaciones para iOS y Android para startups y empresas desde 2017. Le asesoraremos y le propondremos la mejor solución.

Discutir el proyecto

Lea también