Funciones nativas en apps híbridas Ionic: cámara, archivos y geolocalización

Capítulo 8

Tiempo estimado de lectura: 10 minutos

+ Ejercicio

Capacidades nativas en Ionic con Capacitor

En una app híbrida, el HTML/JS no puede acceder directamente a funciones del dispositivo (cámara, GPS, almacenamiento) sin una “puerta de enlace” nativa. En Ionic, esa puerta suele ser Capacitor (recomendado) mediante plugins que exponen APIs TypeScript y ejecutan código nativo en Android/iOS. En este capítulo integrarás: permisos, cámara/galería, sistema de archivos y geolocalización, con un flujo completo: instalación, configuración, solicitud de permisos, manejo de fallos y UI adaptativa cuando el permiso está denegado.

Plugins que usaremos

  • @capacitor/camera: captura de foto o selección desde galería.
  • @capacitor/filesystem: guardar/leer archivos en almacenamiento de la app.
  • @capacitor/geolocation: obtener coordenadas (con permisos).
  • @capacitor/app: abrir ajustes del sistema cuando el usuario bloquea permisos.

Instalación y sincronización con plataformas

Instala los plugins en tu proyecto Ionic (Angular/React/Vue; el código TypeScript es equivalente):

npm i @capacitor/camera @capacitor/filesystem @capacitor/geolocation @capacitor/app

Después, sincroniza con las plataformas nativas:

npx cap sync

Si aún no tienes plataformas añadidas:

npx cap add android
npx cap add ios

Siempre que instales un plugin nuevo o cambies configuración nativa, ejecuta npx cap sync y recompila.

Continúa en nuestra aplicación.
  • Escuche el audio con la pantalla apagada.
  • Obtenga un certificado al finalizar.
  • ¡Más de 5000 cursos para que explores!
O continúa leyendo más abajo...
Download App

Descargar la aplicación

Permisos: modelo mental y estados

Los permisos no son un “sí/no” simple. En móvil verás estados típicos:

  • prompt: aún no se ha preguntado.
  • granted: concedido.
  • denied: denegado (a veces se puede volver a preguntar).
  • prompt-with-rationale (Android): el sistema sugiere explicar por qué lo necesitas.
  • denied permanently (varía por plataforma): el usuario marcó “No volver a preguntar” o bloqueó en Ajustes; debes guiarlo a Ajustes.

Buena práctica: pedir permisos justo antes de usarlos (no al abrir la app), explicar el beneficio y ofrecer una alternativa si el usuario no concede.

Patrón recomendado de UI para permisos

EstadoQué mostrarAcción
No solicitadoBotón “Activar cámara/ubicación” + texto breveSolicitar permiso
DenegadoMensaje “Sin permiso no podemos…” + alternativaReintentar o continuar sin esa función
Bloqueado en AjustesMensaje + botón “Abrir Ajustes”Deep link a ajustes
ConcedidoUI normalEjecutar funcionalidad

Configuración nativa mínima (iOS/Android)

iOS (Info.plist)

En iOS debes declarar por qué usas cada permiso. Capacitor suele requerir entradas en ios/App/App/Info.plist (o mediante Xcode). Añade descripciones claras (sin datos sensibles):

<key>NSCameraUsageDescription</key>
<string>Necesitamos la cámara para tomar fotos y guardarlas en tu dispositivo.</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>Necesitamos acceder a tu galería para seleccionar una imagen.</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>Necesitamos tu ubicación para asociar coordenadas a la foto.</string>

Android (AndroidManifest.xml)

En Android, los plugins suelen añadir permisos automáticamente, pero conviene verificar android/app/src/main/AndroidManifest.xml. Para geolocalización:

<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

En Android 13+ el acceso a fotos cambió (permisos de medios). El plugin de cámara gestiona gran parte, pero si notas fallos al leer galería, revisa la documentación del plugin y el nivel de API del dispositivo.

Funcionalidad práctica: tomar foto, guardar localmente y asociar coordenadas

Implementarás un flujo completo:

  • Solicitar permisos de cámara y ubicación.
  • Capturar una foto (o elegir de galería).
  • Obtener coordenadas actuales.
  • Guardar la imagen en el sistema de archivos de la app.
  • Guardar un “registro” con ruta + lat/lng + fecha.
  • Adaptar la UI para permisos denegados y fallos (sin bloquear la app).

Estructura de datos sugerida

Define un modelo simple para el registro de foto:

export interface PhotoRecord {
  id: string;
  fileName: string;
  filePath: string; // ruta interna (Directory.Data)
  createdAt: string;
  latitude?: number;
  longitude?: number;
  locationAccuracy?: number;
}

Servicio: cámara + archivos + geolocalización

Este servicio encapsula el flujo y centraliza el manejo de errores. Ajusta rutas/imports según tu framework.

import { Camera, CameraResultType, CameraSource, Photo } from '@capacitor/camera';
import { Filesystem, Directory } from '@capacitor/filesystem';
import { Geolocation, Position } from '@capacitor/geolocation';
import { App } from '@capacitor/app';

export class NativeCaptureService {
  async ensureCameraPermission(): Promise<boolean> {
    const perm = await Camera.checkPermissions();
    if (perm.camera === 'granted') return true;

    const req = await Camera.requestPermissions({ permissions: ['camera', 'photos'] });
    return req.camera === 'granted';
  }

  async ensureLocationPermission(): Promise<boolean> {
    const perm = await Geolocation.checkPermissions();
    if (perm.location === 'granted') return true;

    const req = await Geolocation.requestPermissions();
    return req.location === 'granted';
  }

  async openAppSettings(): Promise<void> {
    await App.openSettings();
  }

  async takeOrPickPhoto(source: CameraSource = CameraSource.Camera): Promise<Photo> {
    return Camera.getPhoto({
      quality: 80,
      allowEditing: false,
      resultType: CameraResultType.Base64,
      source
    });
  }

  async getCurrentPositionSafe(): Promise<Position | null> {
    try {
      return await Geolocation.getCurrentPosition({ enableHighAccuracy: true, timeout: 8000 });
    } catch (e) {
      // GPS apagado, timeout, permiso denegado, etc.
      return null;
    }
  }

  async saveBase64Image(base64: string, fileName: string): Promise<{ filePath: string }> {
    const res = await Filesystem.writeFile({
      path: fileName,
      data: base64,
      directory: Directory.Data
    });
    return { filePath: res.uri };
  }

  async captureAndSaveWithLocation(source: CameraSource): Promise<{
    fileName: string;
    filePath: string;
    latitude?: number;
    longitude?: number;
    accuracy?: number;
  }> {
    const cameraOk = await this.ensureCameraPermission();
    if (!cameraOk) {
      throw new Error('CAMERA_PERMISSION_DENIED');
    }

    // Ubicación es opcional: si no hay permiso o falla, seguimos sin coords.
    const locationOk = await this.ensureLocationPermission();
    const position = locationOk ? await this.getCurrentPositionSafe() : null;

    const photo = await this.takeOrPickPhoto(source);
    if (!photo.base64String) {
      throw new Error('PHOTO_NO_DATA');
    }

    const fileName = `photo_${Date.now()}.jpeg`;
    const saved = await this.saveBase64Image(photo.base64String, fileName);

    return {
      fileName,
      filePath: saved.filePath,
      latitude: position?.coords.latitude,
      longitude: position?.coords.longitude,
      accuracy: position?.coords.accuracy
    };
  }
}

Notas importantes:

  • Guardamos en Directory.Data: almacenamiento privado de la app (ideal para datos internos). Si necesitas que aparezca en la galería, el enfoque cambia (y requiere consideraciones extra).
  • La ubicación se trata como mejora opcional: si falla, la foto se guarda igual.
  • Usamos Base64 por simplicidad. Para fotos grandes, considera CameraResultType.Uri y copia el archivo para evitar consumo de memoria.

Página/Componente: UI con estados de permiso y fallos

La UI debe reflejar estados: cargando, permiso denegado, y resultado. Ejemplo de lógica (TypeScript):

import { CameraSource } from '@capacitor/camera';

type PermissionBlock = 'none' | 'camera' | 'location';

export class CapturePage {
  isBusy = false;
  lastError: string | null = null;
  permissionBlocked: PermissionBlock = 'none';

  lastSavedPath: string | null = null;
  lastCoords: { lat?: number; lng?: number; acc?: number } = {};

  constructor(private native: NativeCaptureService) {}

  async onCaptureFromCamera() {
    await this.runCapture(CameraSource.Camera);
  }

  async onPickFromGallery() {
    await this.runCapture(CameraSource.Photos);
  }

  private async runCapture(source: CameraSource) {
    this.isBusy = true;
    this.lastError = null;
    this.permissionBlocked = 'none';

    try {
      const res = await this.native.captureAndSaveWithLocation(source);
      this.lastSavedPath = res.filePath;
      this.lastCoords = { lat: res.latitude, lng: res.longitude, acc: res.accuracy };
    } catch (e: any) {
      const msg = String(e?.message || e);
      if (msg.includes('CAMERA_PERMISSION_DENIED')) {
        this.permissionBlocked = 'camera';
        this.lastError = 'No se concedió permiso de cámara.';
      } else {
        this.lastError = 'No se pudo completar la captura. Intenta de nuevo.';
      }
    } finally {
      this.isBusy = false;
    }
  }

  async openSettings() {
    await this.native.openAppSettings();
  }
}

Ejemplo de plantilla HTML (Ionic) para mostrar estados:

<ion-content class="ion-padding">
  <ion-card>
    <ion-card-header>
      <ion-card-title>Captura y guardado local</ion-card-title>
    </ion-card-header>

    <ion-card-content>
      <ion-button expand="block" (click)="onCaptureFromCamera()" [disabled]="isBusy">
        Tomar foto
      </ion-button>
      <ion-button expand="block" fill="outline" (click)="onPickFromGallery()" [disabled]="isBusy">
        Elegir de galería
      </ion-button>

      <ion-item *ngIf="isBusy">
        <ion-label>Procesando...</ion-label>
        <ion-spinner slot="end"></ion-spinner>
      </ion-item>

      <ion-item color="warning" *ngIf="permissionBlocked === 'camera'">
        <ion-label>
          La app no puede usar la cámara. Puedes habilitar el permiso en Ajustes.
        </ion-label>
        <ion-button slot="end" (click)="openSettings()">Abrir Ajustes</ion-button>
      </ion-item>

      <ion-text color="danger" *ngIf="lastError">
        <p>{{ lastError }}</p>
      </ion-text>

      <ion-list *ngIf="lastSavedPath">
        <ion-item>
          <ion-label>
            <h3>Guardado</h3>
            <p>Ruta: {{ lastSavedPath }}</p>
          </ion-label>
        </ion-item>
        <ion-item>
          <ion-label>
            <h3>Coordenadas</h3>
            <p>Lat: {{ lastCoords.lat ?? 'N/D' }}</p>
            <p>Lng: {{ lastCoords.lng ?? 'N/D' }}</p>
            <p>Precisión: {{ lastCoords.acc ?? 'N/D' }}</p>
          </ion-label>
        </ion-item>
      </ion-list>
    </ion-card-content>
  </ion-card>
</ion-content>

Observa que la UI:

  • Deshabilita botones mientras se procesa (isBusy).
  • Muestra un bloque específico cuando falta permiso de cámara.
  • Permite abrir Ajustes del sistema para recuperar permisos bloqueados.
  • Presenta resultados incluso si no hay ubicación (N/D).

Manejo de fallos comunes (y cómo responder en la app)

1) El usuario deniega permisos

  • Respuesta UX: explica el beneficio (“para adjuntar una foto”) y ofrece alternativa (“elige de galería” o “continúa sin ubicación”).
  • Acción técnica: si el permiso queda bloqueado, muestra botón “Abrir Ajustes” usando App.openSettings().

2) GPS desactivado o sin señal

  • Respuesta UX: no bloquees la captura; guarda la foto sin coordenadas y muestra un aviso “No se pudo obtener ubicación”.
  • Acción técnica: usa timeout y captura excepciones en getCurrentPosition.

3) Problemas de memoria con Base64

  • Síntoma: en dispositivos con poca RAM, fotos grandes pueden causar cierres o lentitud.
  • Mitigación: usar CameraResultType.Uri y luego copiar/leer el archivo por streaming (según plataforma) o reducir calidad/tamaño.

4) Rutas y visualización de archivos

Filesystem.writeFile devuelve una uri que no siempre es directamente renderizable en un <img>. Si necesitas previsualizar, suele ser mejor:

  • Guardar también la foto como data:image/jpeg;base64,... para preview rápida (si el tamaño lo permite), o
  • Usar utilidades de conversión de URI a URL segura según el framework (p. ej., WebView convertFileSrc en Ionic).

Privacidad y experiencia móvil (imprescindible)

Minimización de datos

  • Pide solo los permisos necesarios. Si la ubicación es opcional, no la hagas requisito para usar la cámara.
  • Guarda únicamente lo que necesitas: coordenadas aproximadas pueden ser suficientes en muchos casos.

Transparencia y control

  • Explica antes del prompt del sistema por qué se solicita el permiso (texto breve en pantalla).
  • Permite borrar registros/fotos desde la app si el caso de uso lo requiere.

Almacenamiento y seguridad

  • Directory.Data es privado para la app, pero el dispositivo puede estar rooteado/jailbreak. No almacenes información sensible sin cifrado.
  • Si sincronizas fotos a un backend, informa claramente y usa HTTPS; considera anonimizar coordenadas si no son críticas.

Buenas prácticas de interacción

  • No encadenes múltiples prompts de permisos al iniciar. Solicita cámara cuando el usuario pulsa “Tomar foto” y ubicación cuando vas a asociar coordenadas.
  • Si el usuario deniega, evita bucles de solicitud. Ofrece “Abrir Ajustes” y una alternativa funcional.

Ahora responde el ejercicio sobre el contenido:

En una app Ionic que usa Capacitor para cámara y geolocalización, ¿cuál es la estrategia recomendada para manejar permisos sin bloquear la funcionalidad principal?

¡Tienes razón! Felicitaciones, ahora pasa a la página siguiente.

¡Tú error! Inténtalo de nuevo.

La buena práctica es pedir permisos en el momento de uso, explicar el beneficio y mantener alternativas (por ejemplo, guardar la foto sin coordenadas). Si el permiso está bloqueado, se guía al usuario a Ajustes para reactivarlo.

Siguiente capítulo

Rendimiento y calidad en Ionic: optimización, accesibilidad y depuración

Arrow Right Icon
Portada de libro electrónico gratuitaIonic desde Cero: Crea Aplicaciones Híbridas con HTML, CSS y TypeScript
80%

Ionic desde Cero: Crea Aplicaciones Híbridas con HTML, CSS y TypeScript

Nuevo curso

10 páginas

Descarga la aplicación para obtener una certificación gratuita y escuchar cursos en segundo plano, incluso con la pantalla apagada.