Skill · guia-ui-nativa-expo-router · v1.0.0

Cultiva Aprende
Guía UI Nativa con Expo Router

Arquitectura, patrones y código de referencia para construir la app de aprendizaje de IA de CULTIVA con calidad nativa iOS/Android.

iOS 26+ · Android
Expo Router · NativeTabs
Reanimated 3 · Liquid Glass
Avanzado · Premium
📱

Pantallas de la App

Arquitectura de 4 tabs: Home, Búsqueda, Favoritos, Perfil

① Home — lista
9:41
●●●
Skills
CULTIVA IA · 2.022 recursos
🤖
Copywriting con IA
Contenido · Avanzado
5€
📊
Analytics avanzado
Datos · Medio
5€
🎨
Brand Book IA
Marketing · Básico
5€
Automatización N8N
Agentes · Avanzado
5€
Skills
Buscar
Favoritos
Perfil
② Búsqueda nativa
9:41
●●●
Buscar
✓ Marketing
Contenido
Datos
📧
Email Sequence
Marketing · 3 resultados
🎯
Cold Email IA
Marketing · Avanzado
Skills
Buscar
Favoritos
Perfil
③ Filtros (Form Sheet)
9:41
●●●
Buscar
📧
Email Sequence
Marketing
Filtrar Skills
SERVICIO
✓ Marketing
Web
Datos
NIVEL
✓ Básico
Medio
Avanzado
Skills
Buscar
Favoritos
Perfil
④ Detalle (Zoom Transition)
9:41
●●●
← Skills
📧
Email Sequence
Marketing · Avanzado
5€
Precio único
★ 4.9
Valoración
Comprar skill
Skills
Buscar
Favoritos
Perfil
📦

Dependencias

Stack recomendado para Cultiva Aprende

Paquete Para qué Estado Nota
expo-router Navegación basada en archivos ● Estable Stack + NativeTabs incluidos
react-native-reanimated Animaciones declarativas ● Estable Entering/Exiting/LayoutAnim
expo-blur Fondos blur (iOS nativo) ● Estable BlurView con intensidad
expo-glass-effect Liquid Glass iOS 26+ ★ Nuevo Solo iOS 26, graceful fallback
expo-image Imágenes optimizadas + SF Symbols ● Estable source="sf:name" para SF Symbols
expo-haptics Feedback táctil iOS ● Estable Condicional solo en iOS
expo-router/unstable-native-tabs Tabs nativas iOS/Android ⚠ Unstable API puede cambiar, preferir sobre JS tabs
react-native-safe-area-context Safe area insets ● Estable NO usar SafeAreaView de RN
Expo Go primero: Prueba la app con npx expo start antes de crear builds personalizados. Todas estas dependencias funcionan en Expo Go sin configuración nativa adicional.
🗂

Estructura de Rutas

Convenciones de archivos y organización de carpetas

app/
├── _layout.tsx ← NativeTabs root
├── (index,search)/ ← grupo compartido (push screens comunes)
├── _layout.tsx ← Stack por tab
├── index.tsx ← lista de skills destacadas
├── search.tsx ← búsqueda con header search bar
└── i/[id].tsx ← detalle de skill (ruta dinámica)
├── (favorites)/
├── _layout.tsx ← Stack favoritos
└── index.tsx ← grid de favoritos guardados
├── (profile)/
├── _layout.tsx
└── index.tsx ← perfil y ajustes
└── modal.tsx ← modal de compra (presentation: "modal")

components/ ← NUNCA dentro de app/
├── skill-card.tsx
├── theme.tsx
└── filter-sheet.tsx
✓ Hacer
Rutas siempre en app/
Componentes en components/ (fuera de app)
Nombres kebab-case: skill-card.tsx
Alias en tsconfig: @components/
Siempre una ruta que coincida con /
✗ No hacer
Co-locar componentes dentro de app/
Caracteres especiales en nombres de archivo
Dejar rutas antiguas al reestructurar
Imports relativos largos (usar alias)
PascalCase o snake_case en archivos

NativeTabs Layout

Tabs nativas iOS (UITabBarController) y Android

📄 app/_layout.tsx
TSX
// app/_layout.tsx — root layout de Cultiva Aprende
import { NativeTabs, Icon, Label } from "expo-router/unstable-native-tabs";
import { Theme } from "@components/theme";

export default function Layout() {
  return (
    <Theme>
      <NativeTabs>
        {/* Tab 1: Skills destacadas */}
        <NativeTabs.Trigger name="(index)">
          <Icon sf="brain" />
          <Label>Skills</Label>
        </NativeTabs.Trigger>

        {/* Tab 2: Búsqueda con role search = barra búsqueda nativa iOS */}
        <NativeTabs.Trigger name="(search)" role="search" />

        {/* Tab 3: Favoritos */}
        <NativeTabs.Trigger name="(favorites)">
          <Icon sf="heart" />
          <Label>Favoritos</Label>
        </NativeTabs.Trigger>

        {/* Tab 4: Perfil */}
        <NativeTabs.Trigger name="(profile)">
          <Icon sf="person.circle" />
          <Label>Perfil</Label>
        </NativeTabs.Trigger>
      </NativeTabs>
    </Theme>
  );
}
📚

Stack Navigator

Headers nativos con large title, blur y botones

📄 app/(index,search)/_layout.tsx
TSX
import { Stack } from "expo-router/stack";
import { PlatformColor } from "react-native";

export default function Layout({ segment }: { segment: string }) {
  const screen = segment.match(/\((.*)\)/)?.[1]!;
  const titles: Record<string, string> = {
    index: "Skills",
    search: "Buscar",
  };

  return (
    <Stack
      screenOptions={{
        // Fondo transparente para que brille el liquid glass
        headerTransparent: true,
        headerShadowVisible: false,
        headerLargeTitleShadowVisible: false,
        headerLargeStyle: { backgroundColor: "transparent" },
        // Colores adaptados a dark/light mode automáticamente
        headerTitleStyle: { color: PlatformColor("label") },
        headerLargeTitle: true,
        headerBlurEffect: "none",
        headerBackButtonDisplayMode: "minimal",
      }}
    >
      {/* Pantalla principal del tab */}
      <Stack.Screen
        name={screen}
        options={{ title: titles[screen] }}
      />
      {/* Detalle de skill — compartido entre tabs */}
      <Stack.Screen
        name="i/[id]"
        options={{ headerLargeTitle: false }}
      />
    </Stack>
  );
}
🎨

Estilos y Convenciones

Apple HIG — sin CSS, sin Tailwind, sin StyleSheet innecesario

📄 components/skill-card.tsx — sombras, bordes, inline styles
TSX
import { View, Text, Pressable, PlatformColor } from "react-native";
import { Link } from "expo-router";
import { Image } from "expo-image";

export function SkillCard({ skill }: { skill: Skill }) {
  return (
    {/* Link con Zoom Transition hacia detalle */}
    <Link href={`/i/${skill.id}`}>
      <Link.Trigger>
        <Pressable>
          <View
            style={{
              backgroundColor: PlatformColor("secondarySystemBackground"),
              borderRadius: 14,
              padding: 14,
              gap: 10,
              /* CSS boxShadow — NUNCA elevation/shadowColor legacy */
              boxShadow: "0 2px 8px rgba(0,0,0,0.12), 0 0 0 1px rgba(255,255,255,0.06)",
              borderCurve: "continuous", /* esquinas suaves Apple-style */
            }}
          >
            <View style={{ flexDirection: "row", alignItems: "center", gap: 10 }}>
              {/* SF Symbol con expo-image */}
              <Image
                source={`sf:${skill.sfSymbol}`}
                style={{ width: 28, height: 28 }}
                tintColor={PlatformColor("systemIndigo")}
              />
              <View style={{ flex: 1 }}>
                {/* selectable para que el usuario pueda copiar el nombre */}
                <Text selectable style={{ fontWeight: "700", fontSize: 15 }}>
                  {skill.nombre}
                </Text>
                <Text style={{ color: PlatformColor("secondaryLabel"), fontSize: 13 }}>
                  {skill.servicio} · {skill.nivel}
                </Text>
              </View>
              <Text style={{ fontVariant: ["tabular-nums"], color: "#10b981", fontWeight: "700" }}>
                5</Text>
            </View>
          </View>
        </Pressable>
      </Link.Trigger>

      {/* Context menu de mantener presionado */}
      <Link.Menu>
        <Link.MenuAction
          title="Guardar en favoritos"
          icon="heart"
          onPress={() => addFavorite(skill.id)}
        />
        <Link.MenuAction
          title="Compartir"
          icon="square.and.arrow.up"
          onPress={() => shareSkill(skill)}
        />
        <Link.MenuAction
          title="Comprar"
          icon="cart"
          onPress={() => router.push("modal")}
        />
      </Link.Menu>
      <Link.Preview />  {/* preview en hover / 3D touch */}
    </Link>
  );
}

Animaciones con Reanimated

Entering/Exiting y animaciones de layout para las skill cards

1
Lista carga
FadeInDown + stagger 50ms
2
Filtrar
FadeOut → FadeInUp cards restantes
3
Press card
Scale 0.97 + haptic
4
Zoom Transition
Apple Zoom a detalle (iOS 18+)
📄 components/skill-list.tsx — animaciones de entrada
TSX
import Animated, { FadeInDown, FadeOut, LinearTransition } from "react-native-reanimated";
import * as Haptics from "expo-haptics";

export function SkillList({ skills }: { skills: Skill[] }) {
  return (
    <ScrollView
      {/* siempre contentInsetAdjustmentBehavior en el primer ScrollView de la ruta */}
      contentInsetAdjustmentBehavior="automatic"
      contentContainerStyle={{ padding: 16, gap: 12 }}
    >
      {skills.map((skill, i) => (
        <Animated.View
          key={skill.id}
          {/* Stagger de entrada — cada card entra 50ms después */}
          entering={FadeInDown.delay(i * 50).springify().damping(20)}
          exiting={FadeOut.duration(150)}
          layout={LinearTransition.springify()}
        >
          <SkillCard
            skill={skill}
            onPress={() => {
              /* Haptic feedback en iOS */
              if (process.env.EXPO_OS === "ios") {
                Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
              }
            }}
          />
        </Animated.View>
      ))}
    </ScrollView>
  );
}
🔮

Efectos Visuales iOS 26

Liquid Glass y Blur nativos

🫧

Liquid Glass

Header del Stack y fondo del Form Sheet con efecto liquid glass en iOS 26+. Fallback opaco en versiones anteriores.

expo-glass-effect iOS 26+
🌫

Blur Nativo

BlurView con intensidad adaptada para overlays, modals y fondos de tabs bar con material nativo.

expo-blur BlurView

Haptic Feedback

Impacto ligero en card press, mediano en favorito guardado. Solo en iOS con guard EXPO_OS.

expo-haptics Solo iOS
📄 Stack con liquid glass sheet
TSX
// Form Sheet con fondo transparente = liquid glass automático en iOS 26+
<Stack.Screen
  name="filter-sheet"
  options={{
    presentation: "formSheet",
    sheetGrabberVisible: true,
    sheetAllowedDetents: [0.4, 0.85],
    /* backgroundColor transparent activa liquid glass en iOS 26 */
    contentStyle: { backgroundColor: "transparent" },
    title: "Filtros",
  }}
/>
Liquid Glass es iOS 26+ only. Siempre proporciona un fallback: usa PlatformColor("systemBackground") como backgroundColor por defecto. En Android, el Sheet mostrará un fondo sólido.
🔭

Apple Zoom Transitions

Transiciones fluidas card → detalle (iOS 18+)

📋 Stack Screen con Zoom

<Stack.Screen
  name="i/[id]"
  options={{
    // presentation zoom activa Apple Zoom en iOS 18+
    presentation: "card",
    animation: "ios_from_right",
  }}
/>

La transición zoom conecta la card en la lista con la pantalla de detalle usando el mismo sharedTransitionTag.

🔗 Link con AppleZoom

<Link
  href={`/i/${skill.id}`}
  // AppleZoom disponible en iOS 18+
  asChild
>
  <Link.AppleZoom
    sourceId={`skill-${skill.id}`}
  >
    <SkillCard skill={skill} />
  </Link.AppleZoom>
</Link>

sourceId conecta la card con el destino para la animación de zoom fluido.

Best practice Cultiva Aprende: Añade zoom transitions en todas las cards de skills que navegan a detalle. En iOS < 18 y Android, la navegación cae back a la transición estándar de forma automática.
🚀

Capacidades por Categoría

Resumen de todo lo disponible en la guía

🎬

Animaciones

Entering/Exiting, layout animations, scroll-driven, gesture-based y Zoom transitions de Apple.

Reanimated FadeInDown LinearTransition
🍎

iOS Exclusivo

SF Symbols, Liquid Glass iOS 26, haptics, large titles, form sheets con blur y zoom transitions.

iOS 18+ iOS 26+ SF Symbols
💾

Storage

SQLite para catálogo de skills locales, SecureStore para tokens de acceso, AsyncStorage para preferencias.

SQLite SecureStore
🎮

Controles Nativos

Switch, Slider, SegmentedControl, DateTimePicker con haptics integrados. Nunca Picker legacy.

@react-native-community Haptics