Pantallas de la App
Arquitectura de 4 tabs: Home, Búsqueda, 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 |
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
├── _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
app/components/ (fuera de app)skill-card.tsx@components//app/NativeTabs Layout
Tabs nativas iOS (UITabBarController) y Android
// 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
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
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
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.
Blur Nativo
BlurView con intensidad adaptada para overlays, modals y fondos de tabs bar con material nativo.
Haptic Feedback
Impacto ligero en card press, mediano en favorito guardado. Solo en iOS con guard EXPO_OS.
// 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", }} />
PlatformColor("systemBackground") como backgroundColor por defecto. En Android, el Sheet mostrará un fondo sólido.Búsqueda Nativa
headerSearchBarOptions — barra de búsqueda integrada en el header iOS
import { useState, useCallback } from "react"; import { Stack } from "expo-router"; import { FlatList } from "react-native"; import { SkillCard } from "@components/skill-card"; import { useSkills } from "@hooks/use-skills"; export default function SearchScreen() { const [query, setQuery] = useState(""); const skills = useSkills(query); return ( <> <Stack.Screen options={{ title: "Buscar", /* Barra de búsqueda nativa iOS integrada en el large header */ headerSearchBarOptions: { placeholder: "Buscar skills de IA...", onChangeText: (e) => setQuery(e.nativeEvent.text), onCancelButtonPress: () => setQuery(""), autoCapitalize: "none", }, }} /> <FlatList data={skills} keyExtractor={(item) => item.id} renderItem={({ item }) => <SkillCard skill={item} />} contentInsetAdjustmentBehavior="automatic" contentContainerStyle={{ padding: 16, gap: 12 }} /> > ); }
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.
Capacidades por Categoría
Resumen de todo lo disponible en la guía
Navegación
Stack, NativeTabs, Link, modals, sheets, rutas dinámicas, grupos, context menus y previews.
Animaciones
Entering/Exiting, layout animations, scroll-driven, gesture-based y Zoom transitions de Apple.
iOS Exclusivo
SF Symbols, Liquid Glass iOS 26, haptics, large titles, form sheets con blur y zoom transitions.
Búsqueda
headerSearchBarOptions integrado en el Stack header nativo, hook useSearch para filtrado reactivo.
Storage
SQLite para catálogo de skills locales, SecureStore para tokens de acceso, AsyncStorage para preferencias.
Controles Nativos
Switch, Slider, SegmentedControl, DateTimePicker con haptics integrados. Nunca Picker legacy.