Contenidos
- Requisitos previos
- Paso 1: Crear el directorio y la estructura del proyecto
- Paso 2: Configurar la identidad de la App
- Paso 3: Instalar el Plugin de Dispositivo (Device)
- Paso 4: Construir el Panel de Telemetría (HTML5, CSS3 y JS)
- Paso 5: Añadir la plataforma iOS y Sincronizar
- Paso 6: Evitar errores de vinculación (El truco del Modo Avión)
- Paso 7: Compilar y desplegar desde Xcode
- Paso 8: Configurar Confianza en el Perfil de iOS

Para cualquier desarrollador de aplicaciones híbridas con Ionic Capacitor, entender las barreras de seguridad de Apple es fundamental. El sistema operativo iOS aísla los recursos del teléfono en un contenedor seguro (Sandbox). Esto significa que tu App no puede espiar el carrete de fotos ni los sensores de movimiento sin el consentimiento explícito del usuario.
En este megapost construiremos un Cuadro de Mando Avanzado. Con él aprenderás a leer variables del procesador, auditar preferencias del usuario, estimar la ubicación por red IP y capturar dinámicamente Fuerzas G. Además, integraremos un selector de archivos multimedia compatible con las políticas de privacidad de Apple sin configurar permisos nativos globales (Info.plist).
Requisitos previos
Asegúrate de contar con lo siguiente en tu Mac:
- Node.js: El entorno de ejecución para instalar los módulos.
- Xcode: La herramienta oficial de desarrollo para empaquetar e instalar tu App en el iPhone.
Paso 1: Crear el directorio y la estructura del proyecto
Abre la Terminal de tu Mac y escribe estos comandos secuencialmente para configurar el espacio de trabajo:
# 1. Crear la carpeta raíz del proyecto unificado
mkdir megadashboard
# 2. Acceder al directorio
cd megadashboard
# 3. Inicializar el ecosistema Node.js
npm init -y
# 4. Instalar las dependencias estructurales de Capacitor
npm install @capacitor/core @capacitor/cli
Paso 2: Configurar la identidad de la App
Inicializa la configuración del contenedor nativo ingresando este comando en la terminal:
npx cap init "MegaDash" "com.tuusuario.megadash" --web-dir=www
Paso 3: Instalar el Plugin de Dispositivo (Device)
Para obtener la información real de la batería del hardware a través del puente de Capacitor, añade su plugin oficial:
npm install @capacitor/device
Paso 4: Construir el Panel de Telemetría (HTML5, CSS3 y JS)
Generamos la carpeta web donde alojaremos la interfaz del proyecto:
mkdir www
Crea un archivo llamado index.html dentro de la carpeta www recién creada y pega este código unificado completo:
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Mega Dashboard Telemetría</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background-color: #090d16;
color: #f3f4f6;
margin: 0;
padding: 15px;
}
h1 {
text-align: center;
color: #6366f1;
font-size: 24px;
margin-bottom: 5px;
text-transform: uppercase;
letter-spacing: 1px;
}
.subtitle {
text-align: center;
color: #6b7280;
font-size: 12px;
margin-bottom: 20px;
}
.btn-container {
text-align: center;
margin-bottom: 20px;
}
.btn-sensor {
background-color: #6366f1;
color: white;
border: none;
padding: 14px 24px;
font-size: 15px;
font-weight: bold;
border-radius: 12px;
cursor: pointer;
box-shadow: 0 4px 12px rgba(99, 102, 241, 0.4);
}
.grid {
display: grid;
grid-template-columns: 1fr;
gap: 12px;
width: 100%;
max-width: 480px;
margin: 0 auto;
}
@media(min-width: 400px) {
.grid { grid-template-columns: 1fr 1fr; }
.full-width { grid-column: span 2; }
}
.card {
background: #111827;
border: 1px solid #1f2937;
border-radius: 14px;
padding: 12px;
box-shadow: 0 4px 10px rgba(0,0,0,0.2);
}
.card-title {
font-size: 11px;
color: #9ca3af;
text-transform: uppercase;
font-weight: bold;
margin-bottom: 8px;
border-bottom: 1px solid #1f2937;
padding-bottom: 4px;
}
.sensor-data {
font-size: 14px;
font-family: monospace;
color: #10b981;
line-height: 1.4;
}
.highlight { color: #38bdf8; }
.error { color: #ef4444; }
/* Estilos del selector multimedia seguro */
.file-label {
background-color: #10b981;
color: white;
padding: 10px 16px;
border-radius: 8px;
display: inline-block;
cursor: pointer;
font-weight: bold;
font-family: sans-serif;
font-size: 13px;
margin-top: 5px;
}
#gallery-preview {
width: 100%;
max-width: 220px;
border-radius: 10px;
display: none;
margin-top: 12px;
border: 1px solid #1f2937;
}
</style>
</head>
<body>
<h1>🚨 Mega Dashboard</h1>
<div class="subtitle">Auditoría de Hardware, Sensores y Privacidad Integrada</div>
<div class="btn-container">
<button class="btn-sensor" onclick="desbloquearSensores()">🔓 DESBLOQUEAR KINETICS (WEBKIT)</button>
</div>
<div class="grid">
<div class="card">
<div class="card-title">🔋 Energía & Carga</div>
<div class="sensor-data">
Nivel: <span id="bat-level" class="highlight">--%</span><br>
Cargando: <span id="bat-charging">--</span>
</div>
</div>
<div class="card">
<div class="card-title">🧠 Procesador (Apple Silicon)</div>
<div class="sensor-data">
Núcleos CPU: <span id="cpu-cores" class="highlight">--</span> lógicos<br>
Frecuencia Web: <span id="web-hz">60Hz</span>
</div>
</div>
<div class="card full-width">
<div class="card-title">📍 Ubicación Pasiva (Vía Red IP)</div>
<div class="sensor-data">
Ciudad/País: <span id="ip-location" class="highlight">Cargando...</span><br>
Proveedor ISP: <span id="ip-isp">Detectando...</span>
</div>
</div>
<div class="card">
<div class="card-title">🚀 Acelerómetro Simples</div>
<div class="sensor-data">
X: <span id="acc-x">0.00</span> m/s²<br>
Y: <span id="acc-y">0.00</span> m/s²<br>
Z: <span id="acc-z">0.00</span> m/s²
</div>
</div>
<div class="card">
<div class="card-title">🌍 Fuerzas G e Intervalo</div>
<div class="sensor-data">
G-Eje Z: <span id="g-z">0.00</span> m/s²<br>
Frecuencia: <span id="srv-interval">0</span>ms
</div>
</div>
<div class="card">
<div class="card-title">🔄 Velocidad Rotación</div>
<div class="sensor-data">
Alpha: <span id="rot-alpha">0.0</span>°/s<br>
Beta: <span id="rot-beta">0.0</span>°/s<br>
Gamma: <span id="rot-gamma">0.0</span>°/s
</div>
</div>
<div class="card">
<div class="card-title">📐 Ángulo Espacial</div>
<div class="sensor-data">
Z (α): <span id="ang-alpha">0°</span><br>
X (β): <span id="ang-beta">0°</span><br>
Y (γ): <span id="ang-gamma">0°</span>
</div>
</div>
<div class="card">
<div class="card-title">📺 Panel & Display</div>
<div class="sensor-data">
Render: <span id="scr-res">0x0</span><br>
Factor Retina: <span id="scr-pixel">0</span>x
</div>
</div>
<div class="card full-width">
<div class="card-title">🖼️ Selector Multimedia Seguro (Sandbox)</div>
<div class="sensor-data" style="text-align: center;">
<label for="file-picker" class="file-label">📁 SELECCIONAR FOTO DEL CARRETE</label>
<input type="file" id="file-picker" accept="image/*" style="display: none;" onchange="cargarImagenDesdeGaleria(event)">
<br>
<img id="gallery-preview" src="" alt="Vista previa">
</div>
</div>
<div class="card full-width">
<div class="card-title">⚙️ Preferencias Internas del Sistema</div>
<div class="sensor-data">
Tema de Interfaz: <span id="pref-theme" class="highlight">--</span><br>
Reducir Movimiento: <span id="pref-motion">--</span><br>
Idioma / Zona Horaria: <span id="pref-lang" style="color:#38bdf8;">--</span>
</div>
</div>
</div>
<script>
// 1. TELEMETRÍA DE ENERGÍA (CAPACITOR)
async function actualizarBateria() {
try {
if (window.Capacitor && window.Capacitor.Plugins.Device) {
const info = await window.Capacitor.Plugins.Device.getBatteryInfo();
document.getElementById('bat-level').textContent = Math.round(info.batteryLevel * 100) + '%';
document.getElementById('bat-charging').textContent = info.isCharging ? "Sí ⚡" : "No";
}
} catch (e) { console.error(e); }
}
// 2. GEOLOCALIZACIÓN PASIVA POR IP (SIN PERMISOS DE GPS NATIVO)
async function rastreoPasivoIP() {
try {
const res = await fetch('https://ipapi.co/json/');
const data = await res.json();
document.getElementById('ip-location').textContent = ${data.city}, ${data.country_name} (${data.country_code});
document.getElementById('ip-isp').textContent = data.org;
} catch (err) {
document.getElementById('ip-location').textContent = "Offline o Modo Avión";
document.getElementById('ip-isp').textContent = "Red no disponible";
}
}
// 3. DESBLOQUEO DE SENSORES DE MOVIMIENTO (REQUERIDO POR INTERACCIÓN DEL USUARIO EN iOS)
async function desbloquearSensores() {
if (typeof DeviceOrientationEvent !== 'undefined' && typeof DeviceOrientationEvent.requestPermission === 'function') {
try {
const status = await DeviceOrientationEvent.requestPermission();
if (status === 'granted') {
vincularEventosMovimiento();
alert("✅ Sensores cinéticos en línea.");
} else {
alert("❌ Acceso denegado a los sensores.");
}
} catch (e) { alert("Error: Interactúa con la interfaz directamente."); }
} else {
vincularEventosMovimiento(); // Fallback para otros sistemas
}
}
function vincularEventosMovimiento() {
// Acelerómetro y Fuerza G
window.addEventListener('devicemotion', (e) => {
if (e.acceleration) {
document.getElementById('acc-x').textContent = (e.acceleration.x || 0).toFixed(2);
document.getElementById('acc-y').textContent = (e.acceleration.y || 0).toFixed(2);
document.getElementById('acc-z').textContent = (e.acceleration.z || 0).toFixed(2);
}
if (e.accelerationIncludingGravity) {
document.getElementById('g-z').textContent = (e.accelerationIncludingGravity.z || 0).toFixed(2);
}
document.getElementById('srv-interval').textContent = e.interval ? Math.round(e.interval) : 16;
if (e.rotationRate) {
document.getElementById('rot-alpha').textContent = (e.rotationRate.alpha || 0).toFixed(1);
document.getElementById('rot-beta').textContent = (e.rotationRate.beta || 0).toFixed(1);
document.getElementById('rot-gamma').textContent = (e.rotationRate.gamma || 0).toFixed(1);
}
});
// Giroscopio Orientación
window.addEventListener('deviceorientation', (e) => {
document.getElementById('ang-alpha').textContent = Math.round(e.alpha || 0) + '°';
document.getElementById('ang-beta').textContent = Math.round(e.beta || 0) + '°';
document.getElementById('ang-gamma').textContent = Math.round(e.gamma || 0) + '°';
});
}
// 4. ACCESO SEGURO AL CARRETE (EL USUARIO ELIGE QUÉ ARCHIVO COMPARTIR)
function cargarImagenDesdeGaleria(event) {
const archivo = event.target.files[0];
if (archivo) {
const vistaPrevia = document.getElementById('gallery-preview');
// Creamos un puntero de datos virtual para renderizar la foto seleccionada
vistaPrevia.src = URL.createObjectURL(archivo);
vistaPrevia.style.display = 'inline-block';
}
}
// 5. HARDWARE ESTÁTICO Y PERFILES DE USUARIO
function auditarHardwareYPreferencias() {
// Cores aproximados del microprocesador
document.getElementById('cpu-cores').textContent = navigator.hardwareConcurrency || "6 (A-Series)";
// Resolución física real de la pantalla
const w = window.screen.width * window.devicePixelRatio;
const h = window.screen.height * window.devicePixelRatio;
document.getElementById('scr-res').textContent = ${w}x${h};
document.getElementById('scr-pixel').textContent = window.devicePixelRatio;
// Preferencia de combinación de colores (Modo Oscuro vs Claro)
const isDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
document.getElementById('pref-theme').textContent = isDark ? "Oscuro (Dark Mode)" : "Claro (Light Mode)";
// Preferencia de accesibilidad (Reducir movimiento)
const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
document.getElementById('pref-motion').textContent = reduceMotion ? "Activado (Ahorro Visual)" : "Desactivado (Estándar)";
// Configuración regional de fecha y zona horaria
const lang = navigator.language || "es-ES";
const tz = Intl.DateTimeFormat().resolvedOptions().timeZone || "Desconocida";
document.getElementById('pref-lang').textContent = ${lang} / ${tz};
// Estimación de la Tasa de Refresco de Pantalla (ProMotion 60Hz - 120Hz)
let frames = 0;
let startTime = performance.now();
function checkFps() {
frames++;
let time = performance.now();
if (time >= startTime + 1000) {
document.getElementById('web-hz').textContent = frames + "Hz";
frames = 0;
startTime = time;
}
requestAnimationFrame(checkFps);
}
requestAnimationFrame(checkFps);
}
// INICIALIZACIÓN GENERAL DEL ENTORNO
window.addEventListener('DOMContentLoaded', () => {
auditarHardwareYPreferencias();
rastreoPasivoIP();
setInterval(actualizarBateria, 3000);
setTimeout(actualizarBateria, 500);
});
</script>
</body>
</html>
Paso 5: Añadir la plataforma iOS y Sincronizar
Volvemos a la Terminal para integrar la web compilada dentro del proyecto nativo de Xcode:
# 1. Instalar el paquete de despliegue para iOS
npm install @capacitor/ios
# 2. Agregar los recursos de compilación de Apple
npx cap add ios
# 3. Transferir el index.html y sincronizar los complementos nativos
npx cap sync ios
Paso 6: Evitar errores de vinculación (El truco del Modo Avión)
Para asegurar que Xcode envíe los paquetes binarios sin conflictos de red local:
- Desconecta tu iPhone del cable de la computadora.
- Abre el Centro de Control del iPhone y activa el Modo Avión (cerciórate de que Wi-Fi y Bluetooth queden completamente inhabilitados).
- Vuelve a conectar tu iPhone a la Mac utilizando el cable USB.
Abre el entorno nativo desde la terminal ejecutando:
npx cap open ios
Paso 7: Compilar y desplegar desde Xcode
- Configurar la Firma de Seguridad: Haz clic en la carpeta azul raíz llamada App ubicada en el panel de navegación de la izquierda. Dirígete a la pestaña central Signing & Capabilities. Abre el desplegable de Team y asocia tu Apple ID (esto te permitirá instalar aplicaciones de prueba de forma gratuita).
- Seleccionar Destino: En la barra de herramientas superior, busca el menú desplegable de dispositivos y selecciona tu iPhone físico conectado.
- Limpieza profunda de código: Ve al menú superior y selecciona Product -> Clean Build Folder.
- ¡Ejecutar!: Haz clic en el botón Play (el triángulo en la esquina superior izquierda).
Paso 8: Configurar Confianza en el Perfil de iOS
Cuando la App termine de transferirse, verás su icono en la pantalla del iPhone, pero al pulsarla se bloqueará indicando que el desarrollador no es confiable.
- En tu iPhone, dirígete a Ajustes -> General.
- Desplázate hacia abajo y accede a Administración de dispositivos y VPN.
- Selecciona tu correo electrónico y presiona en «Confiar».
¿Cómo probar las características del Dashboard?
- Al ingresar a la App, verás de inmediato las configuraciones del sistema (Modo Oscuro, idioma, zona horaria, resolución nativa y estimación de Hz).
- La geolocalización pasiva te indicará tu ubicación aproximada calculando tu salida de red a través de la IP sin usar el GPS nativo.
- Presiona el botón verde «SELECCIONAR FOTO DEL CARRETE»; iOS abrirá su gestor de Sandbox seguro para que selecciones una imagen y la proyectes en el Dashboard sin comprometer el resto de tus archivos privados.
- Finalmente, presiona el botón azul «DESBLOQUEAR KINETICS», acepta la alerta de movimiento de WebKit y agita el dispositivo para ver el flujo dinámico de las Fuerzas G en tiempo real. ¡Todo resuelto perfectamente!

