Salta al contenuto principale

Come creare un barcode scanner Ionic con Capacitor

Crea uno scanner di codici a barre Ionic con Capacitor 8: configura l'app, aggiungi @capacitor/barcode-scanner, scansiona codici QR e codici a barre su iOS, Android e web.

Illustrazione di uno smartphone che scansiona un codice a barre all'interno di un'app mobile ibrida

Per creare uno scanner di codici a barre Ionic con Capacitor, crea un'app Ionic con l'integrazione Capacitor, installa il plugin ufficiale @capacitor/barcode-scanner plugin, aumenta il livello di Android minSdkVersion a 26, aggiungi la descrizione dell'uso della fotocamera per iOS e chiama CapacitorBarcodeScanner.scanBarcode() da un pulsante. Il plugin apre uno scanner nativo a schermo intero su iOS e Android, uno scanner nel browser su web e restituisce il testo decodificato e il formato.

Questo tutorial crea un'app scanner funzionante in Ionic Angular su Capacitor 8, poi copre il filtraggio dei formati, la gestione degli errori, i motori di scansione Android, la scansione continua, gli scanner hardware enterprise e i guasti comuni.

Cosa costruirai

Un'app Ionic single-page con un pulsante di scansione che:

  • apre lo scanner della fotocamera,
  • mostra ogni risultato con il relativo formato,
  • ti permette di copiare o aprire il valore scansionato,
  • handles cancel and permission errors cleanly.

Il code utilizza componenti Angular standalone. Le chiamate al plugin sono identiche in React o Vue, quindi cambia solo la sintassi del template.

Prerequisiti

  • Node.js LTS e Bun.
  • Per Android: Android Studio con un SDK e un dispositivo o emulatore con fotocamera.
  • Per iOS: un Mac con Xcode 26, richiesto da App Store Connect per gli upload da aprile 2026.
  • Un dispositivo fisico è l'ideale. Gli emulatori possono scansionare con una fotocamera virtuale, ma per i barcode contano luce e messa a fuoco reali.

Passaggio 1: Crea l'app Ionic

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

CLI di Ionic crea lo scaffold dell'app Angular con @capacitor/core e @capacitor/cli già installato. Controlla capacitor.config.ts: appId dovrebbe essere il tuo ID reverse-domain, ad esempio com.example.scanner, e webDir deve corrispondere all'output della build (www per Ionic Angular).

Esegui la build una volta, così le piattaforme native hanno qualcosa da copiare:

bun run build

Passaggio 2: Aggiungi Android e iOS

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

Capacitor 8 crea nuovi progetti iOS con Swift Package Manager. Se invece stai aggiornando un'app più vecchia, segui la guida all'aggiornamento a Capacitor 8 prima.

Passaggio 3: Installa il plugin barcode scanner

bun add @capacitor/barcode-scanner
bunx cap sync

@capacitor/barcode-scanner è lo scanner ufficiale del team di Capacitor. La versione 3.x supporta Capacitor 8.

Configurazione Android

Il plugin richiede Android API 26 o superiore. Capacitor 8 ha un valore predefinito di 24, quindi modifica android/variables.gradle:

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

Su Android puoi scegliere il motore di decodifica per ogni scansione: ZXing (predefinito, supporta tutti i formati elencati nel plugin) o Google ML Kit (veloce sui codici difficili, ma non supporta MaxiCode, RSS-14, RSS Expanded o estensione UPC/EAN).

Configurazione iOS

Add a camera usage description to ios/App/App/Info.plist:

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

Senza di essa, iOS termina l'app al primo accesso alla fotocamera. Scrivi la frase per l'utente, poiché App Review la legge anch'essa. iOS utilizza il framework Vision di Apple, che supporta tutti i formati elencati tranne MaxiCode e estensione UPC/EAN, e non distingue tra UPC-A ed EAN-13.

Passo 4: Scrivi il servizio scanner

Mantenere le chiamate al plugin in un servizio le rende facili da riutilizzare e testare.

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

Il plugin restituisce errori con codici della forma OS-PLUG-BARC-XXXX. Su iOS, 0006 indica che l'utente ha annullato e 0007 indica che l'accesso alla fotocamera è stato negato. Tratta l'annullamento come un esito normale, non come un errore. Registra il code grezzo su Android durante i test per confermare la stessa mappatura sui dispositivi target.

Il format nel risultato è un valore enum numerico. CapacitorBarcodeScannerTypeHint[format] lo converte di nuovo in un nome come QR_CODE oppure EAN_13.

Passo 5: Crea la pagina

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

Passo 6: Esegui sui dispositivi

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

Per la versione web, bun run start serve l'app su localhost, dove i browser permettono l'accesso alla camera. Su un IP LAN invece serve HTTPS.

Per iterare più velocemente, usa il live reload su un dispositivo: bunx cap run android --livereload --external ricarica l'app ogni volta che salvi.

Formati supportati

Formato ZXing su 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 e EAN-13 trattati allo stesso modo)
Code 128, Code 39, Code 93, Codabar, ITF Sì Sì Sì
RSS-14, RSS Expanded Sì No Sì
MaxiCode Sì No No
estensione UPC/EAN Sì No No

Se passi un formato non supportato come hint, il plugin passa alla scansione di qualsiasi formato.

Gestire i valori scansionati in sicurezza

Un codice a barre è un input non attendibile. Prima di usarlo:

  • Valida gli URL prima di aprirli e mostra prima il dominio all'utente.
  • Non eseguire le stringhe scansionate come code né iniettarle in HTML.
  • Per i codici prodotto, valida la check digit prima di chiamare il backend.
  • Limita le query al backend se gli utenti possono scansionare rapidamente.

Oltre lo scanner di base

scanBarcode è un flusso a schermo intero e istantaneo. Questo copre la maggior parte delle app. Per altri casi servono altri strumenti:

  • Scansione continua nella tua UI. Avvia un feed della fotocamera dietro la tua web UI con @capgo/camera-preview, cattura i frame con captureSample, e decodificali in JavaScript con una libreria come zxing-wasm.
  • Dispositivi enterprise rugged. I palmari Zebra hanno scanner hardware. @capgo/capacitor-zebra-datawedge gestisce i profili DataWedge e i trigger di scansione, risultando più veloce e affidabile della fotocamera su questi dispositivi.
  • Cattura documenti. Per ricevute e moduli cartacei, un plugin di scansione documenti è più adatto di un lettore di barcode. Vedi la directory dei plugin.

Vieni da Cordova? La nostra guida al barcode scanner per Cordova tratta i vecchi plugin e il percorso di migrazione.

Risoluzione problemi

L'app iOS si chiude all'avvio della scansione. NSCameraUsageDescription is missing from Info.plist.

La build Android fallisce con un errore di manifest merger o minSdk. minSdkVersion è ancora 24. Impostalo a 26 in android/variables.gradle e sincronizza.

Lo scanner si apre ma non legge mai il code. Prova l'altro motore Android, migliora l'illuminazione, usa la torcia e limita hint al formato atteso. I codici molto piccoli o danneggiati richiedono di avvicinare la fotocamera.

Permesso negato ogni volta. L'utente ha negato l'accesso alla fotocamera in precedenza. Mostra un messaggio che spiega come abilitarlo nelle impostazioni di sistema. Il sistema operativo non mostrerà più il prompt.

Lo scanner web non si avvia. La pagina non viene servita tramite HTTPS o localhost, oppure un'altra scheda sta usando la fotocamera.

Impostazione dell'orientamento ignorata sui tablet. A partire dalle app con target Android SDK 36, Android 16 ignora gli orientamenti fissi sui grandi schermi. Usa ADAPTIVE.

Rilascia fix senza attendere la revisione

Una volta che lo scanner è live, la maggior parte delle modifiche, come nuove regole di validazione, ritocchi alla UI o una diversa lookup sul backend, sono web code. Live updates di Capgo distribuiscili nelle app installate in pochi minuti, mentre gli aggiornamenti dei plugin e Info.plist le modifiche passano ancora tramite gli store. Quando ti serve un nuovo Native Build senza un Mac in locale, Build Capgo genera i binari firmati per iOS e Android nel cloud.

In sintesi

Un barcode scanner Ionic su Capacitor 8 richiede un plugin ufficiale, due modifiche alla config nativa (Android minSdkVersion 26 e la stringa di utilizzo della camera iOS) e una singola scanBarcode chiamata. Limita i formati quando puoi, tratta annullamento ed errori di permesso come esiti normali, valida tutto ciò che scansioni e ricorri a una camera preview o DataWedge quando il flusso one-shot non basta.

Live updates per le app Capacitor

Quando un bug nel web-layer è live, rilascia la fix tramite Capgo invece di aspettare giorni l'approvazione degli store. Gli utenti ricevono l'update in background, mentre le modifiche native restano nel normale processo di review.

Ultimi articoli dal blog

Capgo ti offre gli insight migliori che ti servono per creare un'app mobile davvero professionale.