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/appDespués, sincroniza con las plataformas nativas:
npx cap syncSi aún no tienes plataformas añadidas:
npx cap add android
npx cap add iosSiempre que instales un plugin nuevo o cambies configuración nativa, ejecuta npx cap sync y recompila.
- Escuche el audio con la pantalla apagada.
- Obtenga un certificado al finalizar.
- ¡Más de 5000 cursos para que explores!
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
| Estado | Qué mostrar | Acción |
|---|---|---|
| No solicitado | Botón “Activar cámara/ubicación” + texto breve | Solicitar permiso |
| Denegado | Mensaje “Sin permiso no podemos…” + alternativa | Reintentar o continuar sin esa función |
| Bloqueado en Ajustes | Mensaje + botón “Abrir Ajustes” | Deep link a ajustes |
| Concedido | UI normal | Ejecutar 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
Base64por simplicidad. Para fotos grandes, consideraCameraResultType.Uriy 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
timeouty captura excepciones engetCurrentPosition.
3) Problemas de memoria con Base64
- Síntoma: en dispositivos con poca RAM, fotos grandes pueden causar cierres o lentitud.
- Mitigación: usar
CameraResultType.Uriy 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.Dataes 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.