Saltar al contenido principal

Cómo crear un escáner de códigos de barras de Ionic con Capacitor

Crea un barcode scanner en Ionic con Capacitor 8: configura la app, añade @capacitor/barcode-scanner y escanea códigos QR y de barras en iOS, Android y web.

Ilustración de un teléfono escaneando un código de barras dentro de una app móvil híbrida

Para crear un barcode scanner en Ionic con Capacitor, crea una app de Ionic con la integración de Capacitor, instala el @capacitor/barcode-scanner plugin, sube la versión de Android minSdkVersion a 26, añadir la descripción de uso de la cámara de iOS y llamar CapacitorBarcodeScanner.scanBarcode() desde un botón. El plugin abre un escáner nativo a pantalla completa en iOS y Android, un escáner del navegador en la web, y devuelve el texto decodificado y el formato.

Este tutorial crea una app de escáner funcional en Ionic Angular con Capacitor 8, y luego cubre filtrado de formatos, manejo de errores, motores de escaneo de Android, escaneo continuo, escáneres de hardware empresarial y fallos comunes.

Qué vas a construir

Una app Ionic de una sola página con un botón de escaneo que:

  • abre el escáner de la cámara,
  • muestra cada resultado con su formato,
  • te permite copiar o abrir el valor escaneado,
  • gestiona los errores de cancelación y permisos de forma limpia.

code utiliza componentes standalone de Angular. Las llamadas al plugin son idénticas en React o Vue, por lo que solo cambia la sintaxis de la plantilla.

Requisitos

  • Node.js LTS y Bun.
  • Para Android: Android Studio con un SDK y un dispositivo o emulador con cámara.
  • Para iOS: un Mac con Xcode 26, que App Store Connect exige para subir apps desde abril de 2026.
  • Lo ideal es un dispositivo físico. Los emuladores pueden escanear con una cámara virtual, pero la luz y el enfoque reales son clave para los códigos de barras.

Paso 1: Crear la app de Ionic

bunx @ionic/cli start barcode-scanner blank --type=angular-standalone --capacitor
cd barcode-scanner

El CLI de Ionic estructura la app de Angular con @capacitor/core y @capacitor/cli ya instalado. Revisa capacitor.config.ts: appId debe ser tu ID de dominio inverso, por ejemplo com.example.scanner, y webDir debe coincidir con la salida del build (www para Ionic Angular).

Construye una sola vez para que las plataformas nativas tengan algo que copiar:

bun run build

Paso 2: Añadir Android e iOS

bun add @capacitor/android @capacitor/ios
bunx cap add android
bunx cap add ios

Capacitor 8 crea nuevos proyectos de iOS con Swift Package Manager. Si en cambio estás actualizando una app más antigua, sigue la guía de actualización de Capacitor 8 primero.

Paso 3: Instala el plugin de barcode scanner

bun add @capacitor/barcode-scanner
bunx cap sync

@capacitor/barcode-scanner es el escáner oficial del equipo de Capacitor. La versión 3.x es compatible con Capacitor 8.

Configuración de Android

El plugin requiere Android API 26 o superior. Capacitor 8 usa 24 por defecto, así que edita android/variables.gradle:

ext {
    minSdkVersion = 26
    // keep the other values Capacitor generated
}

En Android puedes elegir el motor de decodificación en cada escaneo: ZXing (por defecto, soporta todos los formatos que lista el plugin) o Google ML Kit (rápido con códigos difíciles, pero sin MaxiCode, RSS-14, RSS Expanded ni extensión UPC/EAN).

Configuración de iOS

Añade una descripción de uso de la cámara en ios/App/App/Info.plist:

<key>NSCameraUsageDescription</key>
<string>The camera is used to scan barcodes and QR codes.</string>

Sin esto, iOS cierra la app al primer acceso a la cámara. Escribe el texto pensando en el usuario, ya que App Review también lo lee. iOS usa el framework Vision de Apple, que soporta todos los formatos listados excepto MaxiCode y la extensión UPC/EAN, y no distingue entre UPC-A y EAN-13.

Paso 4: Crea el servicio del scanner

Mantener las llamadas al plugin en un servicio facilita su reutilización y pruebas.

// src/app/services/scanner.service.ts
import { Injectable } from '@angular/core';
import { Capacitor } from '@capacitor/core';
import {
  CapacitorBarcodeScanner,
  CapacitorBarcodeScannerAndroidScanningLibrary,
  CapacitorBarcodeScannerCameraDirection,
  CapacitorBarcodeScannerScanOrientation,
  CapacitorBarcodeScannerTypeHint,
  CapacitorBarcodeScannerTypeHintALLOption,
} from '@capacitor/barcode-scanner';

export interface ScanResult {
  value: string;
  format: string;
  scannedAt: Date;
}

export type ScanOutcome =
  | { status: 'ok'; result: ScanResult }
  | { status: 'cancelled' }
  | { status: 'denied' }
  | { status: 'error'; message: string };

@Injectable({ providedIn: 'root' })
export class ScannerService {
  async scan(onlyQr = false): Promise<ScanOutcome> {
    try {
      const { ScanResult, format } = await CapacitorBarcodeScanner.scanBarcode({
        hint: onlyQr
          ? CapacitorBarcodeScannerTypeHint.QR_CODE
          : CapacitorBarcodeScannerTypeHintALLOption.ALL,
        scanInstructions: 'Point the camera at a barcode',
        cameraDirection: CapacitorBarcodeScannerCameraDirection.BACK,
        scanOrientation: CapacitorBarcodeScannerScanOrientation.ADAPTIVE,
        cancelButtonAccessibilityLabel: 'Cancel scanning',
        torchButtonOnAccessibilityLabel: 'Turn flashlight off',
        torchButtonOffAccessibilityLabel: 'Turn flashlight on',
        android: {
          scanningLibrary: CapacitorBarcodeScannerAndroidScanningLibrary.MLKIT,
        },
        web: {
          showCameraSelection: true,
        },
      });

      return {
        status: 'ok',
        result: {
          value: ScanResult,
          format: CapacitorBarcodeScannerTypeHint[format] ?? String(format),
          scannedAt: new Date(),
        },
      };
    } catch (err: any) {
      const code: string = err?.code ?? '';
      if (code.endsWith('0006')) return { status: 'cancelled' };
      if (code.endsWith('0007')) return { status: 'denied' };
      return { status: 'error', message: err?.message ?? 'Scan failed' };
    }
  }

  get platform() {
    return Capacitor.getPlatform();
  }
}

El plugin devuelve errores con códigos en el formato OS-PLUG-BARC-XXXX. En iOS, 0006 significa que el usuario canceló y 0007 significa que se denegó el acceso a la cámara. Trata la cancelación como un resultado normal, no como un error. Registra el code sin procesar en Android durante las pruebas para confirmar el mismo mapeo en tus dispositivos objetivo.

El format el resultado es un valor de enum numérico. CapacitorBarcodeScannerTypeHint[format] lo convierte de nuevo en un nombre como QR_CODE o EAN_13.

Paso 5: Construye la página

// src/app/home/home.page.ts
import { Component, inject, signal } from '@angular/core';
import { DatePipe } from '@angular/common';
import {
  IonHeader, IonToolbar, IonTitle, IonContent, IonList, IonItem, IonLabel,
  IonFab, IonFabButton, IonIcon, IonToggle, IonNote, ToastController,
} from '@ionic/angular/standalone';
import { addIcons } from 'ionicons';
import { scanOutline } from 'ionicons/icons';
import { ScannerService, ScanResult } from '../services/scanner.service';

@Component({
  selector: 'app-home',
  templateUrl: 'home.page.html',
  imports: [
    DatePipe, IonHeader, IonToolbar, IonTitle, IonContent, IonList, IonItem,
    IonLabel, IonFab, IonFabButton, IonIcon, IonToggle, IonNote,
  ],
})
export class HomePage {
  private scanner = inject(ScannerService);
  private toast = inject(ToastController);

  results = signal<ScanResult[]>([]);
  onlyQr = signal(false);
  scanning = signal(false);

  constructor() {
    addIcons({ scanOutline });
  }

  async scan() {
    if (this.scanning()) return;
    this.scanning.set(true);
    const outcome = await this.scanner.scan(this.onlyQr());
    this.scanning.set(false);

    if (outcome.status === 'ok') {
      this.results.update((list) => [outcome.result, ...list]);
    } else if (outcome.status === 'denied') {
      await this.show('Camera access is off. Enable it in Settings to scan.');
    } else if (outcome.status === 'error') {
      await this.show(outcome.message);
    }
  }

  private async show(message: string) {
    const t = await this.toast.create({ message, duration: 3000 });
    await t.present();
  }
}
<!-- src/app/home/home.page.html -->
<ion-header>
  <ion-toolbar>
    <ion-title>Barcode Scanner</ion-title>
  </ion-toolbar>
</ion-header>

<ion-content>
  <ion-list>
    <ion-item>
      <ion-toggle [checked]="onlyQr()" (ionChange)="onlyQr.set($event.detail.checked)">
        QR codes only
      </ion-toggle>
    </ion-item>

    @for (r of results(); track r.scannedAt) {
      <ion-item>
        <ion-label>
          <h2>{{ r.value }}</h2>
          <p>{{ r.format }} · {{ r.scannedAt | date: 'shortTime' }}</p>
        </ion-label>
      </ion-item>
    } @empty {
      <ion-item lines="none">
        <ion-note>No scans yet. Tap the button to scan.</ion-note>
      </ion-item>
    }
  </ion-list>

  <ion-fab slot="fixed" vertical="bottom" horizontal="end">
    <ion-fab-button (click)="scan()" [disabled]="scanning()" aria-label="Scan a barcode">
      <ion-icon name="scan-outline" aria-hidden="true"></ion-icon>
    </ion-fab-button>
  </ion-fab>
</ion-content>

Paso 6: Ejecuta en dispositivos

bun run build
bunx cap sync
bunx cap run android
bunx cap run ios

Para la versión web, bun run start sirve la app en localhost, donde los navegadores permiten el acceso a la cámara. Con una IP de LAN necesitas HTTPS.

Para iterar más rápido, usa live reload en un dispositivo: bunx cap run android --livereload --external recarga la app cada vez que guardas.

Formatos soportados

Formato ZXing para Android Android ML Kit iOS
QR Code, Aztec, Data Matrix, PDF417 Sí Sí Sí
EAN-13, EAN-8, UPC-A, UPC-E Sí Sí Sí (UPC-A y EAN-13 se tratan igual)
Code 128, Code 39, Code 93, Codabar, ITF Sí Sí Sí
RSS-14, RSS Expandido Sí No Sí
MaxiCode Sí No No
Extensión UPC/EAN Sí No No

Si pasas un formato no compatible como hint, el plugin vuelve a escanear cualquier formato.

Gestión segura de valores escaneados

Un código de barras es una entrada no confiable. Antes de procesarlo:

  • Valida las URLs antes de abrirlas y muestra primero el dominio al usuario.
  • No ejecutes los strings escaneados como code ni los inyectes en el HTML.
  • Para códigos de producto, valida el dígito de control antes de llamar a tu backend.
  • Limita las consultas al backend si los usuarios pueden escanear rápidamente.

Más allá del scanner básico

scanBarcode es un flujo de pantalla completa de un solo uso. Eso cubre la mayoría de las apps. Otros casos requieren otras herramientas:

  • Escaneo continuo en tu propia UI. Ejecuta un feed de cámara detrás de tu web UI con @capgo/camera-preview, captura frames con captureSample, y decodifícalos en JavaScript con una librería como zxing-wasm.
  • Dispositivos empresariales robustos. Los terminales Zebra tienen escáneres de hardware. @capgo/capacitor-zebra-datawedge gestiona los perfiles de DataWedge y los disparadores de escaneo, lo cual es más rápido y fiable que la cámara en esos dispositivos.
  • Captura de documentos. Para recibos y formularios en papel, un plugin de escáner de documentos encaja mejor que un lector de códigos de barras. Consulta el directorio de plugins.

¿Vienes de Cordova? Nuestra guía de escáner de código de barras de Cordova cubre los plugins antiguos y una ruta de migración.

Solución de problemas

La app de iOS se cierra al iniciar el escaneo. NSCameraUsageDescription is missing from Info.plist.

El build de Android falla con un error de manifest merger o minSdk. minSdkVersion sigue siendo 24. Cámbialo a 26 en android/variables.gradle y sincroniza.

El scanner abre pero nunca lee el code. Prueba con el otro motor de Android, mejora la iluminación, usa la linterna y limita hint al formato que esperas. Los códigos muy pequeños o dañados requieren acercar la cámara.

Permiso denegado cada vez. El usuario denegó el acceso a la cámara previamente. Muestra un mensaje explicando cómo activarlo en los ajustes del sistema. El SO no volverá a mostrar el prompt.

El scanner web no arranca. La página no se sirve vía HTTPS o localhost, o hay otra pestaña usando la cámara.

La configuración de orientación se ignora en tablets. A partir de las apps dirigidas a Android SDK 36, Android 16 ignora las orientaciones fijas en pantallas grandes. Usa ADAPTIVE.

Lanza fixes sin esperar la revisión

Una vez que el escáner está activo, la mayoría de los cambios, como nuevas reglas de validación, ajustes de UI o una consulta de backend distinta, son code web. actualizaciones Live Update de Capgo envíalos a las apps instaladas en minutos, mientras que las actualizaciones de plugins y Info.plist los cambios siguen pasando por las tiendas. Cuando necesites un nuevo Native Build sin un Mac local configurado, Build de Capgo genera binarios firmados para iOS y Android en la nube.

Resumen

Un escáner de códigos de barras de Ionic en Capacitor 8 requiere un plugin oficial, dos cambios de configuración nativa (Android minSdkVersion 26 y el string de uso de cámara en iOS) y una única scanBarcode llamada. Restringe los formatos cuando puedas, trata los errores de cancelación y de permisos como resultados normales, valida todo lo que escanees y recurre a una vista previa de cámara o a DataWedge cuando el flujo de un solo escaneo no sea suficiente.

Live updates para apps Capacitor

Cuando un bug de la capa web está en producción, publica el fix a través de Capgo en lugar de esperar días a la aprobación de la app store. Los usuarios reciben la actualización en segundo plano mientras los cambios nativos siguen el flujo de revisión habitual.

Lo último de nuestro blog

Capgo te da la información clave que necesitas para crear una app móvil realmente profesional.