<?xml version="1.0" encoding="utf-8" standalone="no"?>
<feed xmlns:media="http://search.yahoo.com/mrss/" xmlns="http://www.w3.org/2005/Atom">
  <id>https://jcalderita.com/es/blog/feed.xml</id>
  <title>Jorge Calderita's Blog</title>
  <author>
    <name>Jorge Calderita</name>
  </author>
  <link rel="self" href="https://jcalderita.com"></link>
  <updated>2026-07-22T07:45:19Z</updated>
  <entry>
    <id>https://jcalderita.com/es/blog/model-parts/</id>
    <title>Model Parts</title>
    <updated>2026-07-08T00:00:00Z</updated>
    <summary>Cómo centralizar índices, claves foráneas y restricciones únicas en el propio modelo para que las migraciones y los tests hablen el mismo idioma.</summary>
    <content type="html">&lt;hr /&gt;
&lt;h2&gt;Mi Problema 🤔&lt;/h2&gt;
&lt;p&gt;Cuando escribo migraciones con &lt;span class="high"&gt;Fluent&lt;/span&gt;, los índices, las claves foráneas y las restricciones únicas se definen dentro del método &lt;span class="high"&gt;prepare&lt;/span&gt; de la migración. Pertenecen a la migración, no al modelo.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-swift"&gt;func prepare(on db: Database) async throws {
    try await db.schema(&amp;quot;products&amp;quot;)
        .id()
        .field(.externalId, .string, .required)
        .field(.categoryId, .uuid, .references(&amp;quot;categories&amp;quot;, &amp;quot;id&amp;quot;, onDelete: .cascade, onUpdate: .cascade))
        .unique(on: .externalId)
        .create()
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;El problema es que esa información &lt;strong&gt;no es legible fuera de la migración&lt;/strong&gt;. Forma parte de un método que se ejecuta una vez y no expone nada al resto del código. Cuando quiero escribir un test de integración que verifique que la migración creó los índices correctos, ¿de dónde saco los nombres esperados? Los tengo que repetir como strings en el test. Y cuando quiero verificar que las claves foráneas existen, lo mismo: strings hardcodeados, acoplados a lo que esté dentro del &lt;span class="high"&gt;prepare&lt;/span&gt;.&lt;/p&gt;
&lt;p&gt;Mi modelo sabe qué tabla representa. Mi modelo sabe qué campos tiene. Pero no sabe nada de su propia estructura de base de datos: índices, referencias, unicidad. Esa información pertenece a la migración, y la única forma de acceder a ella es ejecutándola.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Mi Solución 🧩&lt;/h2&gt;
&lt;p&gt;Mi idea es convertir el modelo en la &lt;strong&gt;única fuente de verdad&lt;/strong&gt; para toda la metadata estructural de su tabla. Creé un protocolo &lt;span class="high"&gt;SchemaDescribable&lt;/span&gt; que extiende &lt;span class="high"&gt;Model&lt;/span&gt; de &lt;span class="high"&gt;Fluent&lt;/span&gt; con tres propiedades estáticas: índices, claves foráneas y restricciones únicas.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-swift"&gt;protocol SchemaDescribable: Model {
    static var indexes: [IndexDefinition] { get }
    static var foreignKeys: [ForeignKeyDefinition] { get }
    static var uniques: [UniqueDefinition] { get }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Cada tipo de constraint tiene su propio tipo con factory methods que hacen la declaración legible y segura. La conformancia la declaro en la capa de base de datos, no en el modelo compartido:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-swift"&gt;extension ProductModel: SchemaDescribable {
    static var indexes: [IndexDefinition] {
        [
            .btree(&amp;quot;idx_products_sku&amp;quot;, [.sku]),
            .btree(&amp;quot;idx_products_category_id&amp;quot;, [.categoryId]),
            .btree(&amp;quot;idx_products_brand_id&amp;quot;, [.brandId]),
            .btree(&amp;quot;idx_products_supplier_id&amp;quot;, [.supplierId]),
            .btree(&amp;quot;idx_products_warehouse_id&amp;quot;, [.warehouseId]),
            .btree(&amp;quot;idx_products_parent_id&amp;quot;, [.parentId]),
            .lower(&amp;quot;idx_products_name_prefix&amp;quot;, .name),
        ]
    }

    static var foreignKeys: [ForeignKeyDefinition] {
        [
            .references(.parentId, on: ProductModel.self),
            .references(.categoryId, on: CategoryModel.self),
            .references(.brandId, on: BrandModel.self),
            .references(.supplierId, on: SupplierModel.self),
            .references(.warehouseId, on: WarehouseModel.self),
        ]
    }

    static var uniques: [UniqueDefinition] { [.on(.externalId)] }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Cada modelo declara de forma explícita todos sus índices &lt;span class="high"&gt;B-Tree&lt;/span&gt;, sus claves foráneas y sus restricciones únicas. Los factory methods como &lt;span class="high"&gt;.btree&lt;/span&gt;, &lt;span class="high"&gt;.lower&lt;/span&gt;, &lt;span class="high"&gt;.references&lt;/span&gt; y &lt;span class="high"&gt;.on&lt;/span&gt; hacen que la declaración sea compacta y difícil de equivocar.&lt;/p&gt;
&lt;p&gt;Con las definiciones en el modelo, la migración se limita a aplicarlas. Un método &lt;span class="high"&gt;addConstraints(for:)&lt;/span&gt; sobre &lt;span class="high"&gt;SchemaBuilder&lt;/span&gt; añade las claves foráneas y las restricciones únicas, y &lt;span class="high"&gt;createIndexes&lt;/span&gt; genera los índices — todo a partir de lo que el modelo declara:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-swift"&gt;struct CreateProducts: AsyncMigration, MigrationProtocol {
    typealias ModelType = ProductModel
    let target: DeployTarget

    func prepare(on db: Database) async throws {
        try await db.schema(model)
            .id(default: true)
            .field(.externalId, .string, .required)
            .field(.name, .string, .required)
            .field(.sku, .string, .required)
            .field(.price, .double, .required)
            .field(.weight, .double)
            .field(.stock, .int)
            .field(.parentId, .uuid)
            .field(.categoryId, .uuid)
            .field(.brandId, .uuid)
            .field(.supplierId, .uuid)
            .field(.warehouseId, .uuid)
            .timestamps(default: true)
            .addConstraints(for: model)
            .create()

        try await createIndexes(on: db)
    }

    func revert(on db: Database) async throws {
        try await db.schema(model).delete()
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;La migración ya no contiene ninguna definición de constraint. Solo declara los campos y delega toda la estructura de índices, foreign keys y uniques al modelo a través de &lt;span class="high"&gt;addConstraints(for:)&lt;/span&gt; y &lt;span class="high"&gt;createIndexes&lt;/span&gt;. El protocolo &lt;span class="high"&gt;MigrationProtocol&lt;/span&gt; proporciona estos métodos gracias al &lt;span class="high"&gt;typealias ModelType&lt;/span&gt;, que conecta la migración con su modelo y permite acceder a las definiciones de &lt;span class="high"&gt;SchemaDescribable&lt;/span&gt;.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Mi Resultado 🎯&lt;/h2&gt;
&lt;p&gt;Ahora los índices, claves foráneas y restricciones únicas son &lt;strong&gt;propiedades del modelo&lt;/strong&gt;, no código enterrado en una migración. Cualquier parte de mi proyecto puede leerlas.&lt;/p&gt;
&lt;p&gt;El beneficio más inmediato son las migraciones limpias. Pero el que me compensa a futuro es que &lt;strong&gt;los tests pueden hablar directamente con el modelo&lt;/strong&gt; — en un próximo artículo veremos cómo aprovecho esto para escribir tests de integración que verifican la estructura de la base de datos sin un solo string hardcodeado.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Un solo sitio&lt;/strong&gt; donde leer toda la metadata estructural de cada modelo&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Migraciones sin repetición&lt;/strong&gt; — &lt;span class="high"&gt;addConstraints(for: model)&lt;/span&gt; aplica todo de un paso&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cambios localizados&lt;/strong&gt; — añadir un índice o FK solo toca la conformancia del modelo&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;La clave es reconocer que un modelo no es solo un contenedor de campos: es también la descripción completa de su tabla. &lt;span class="high"&gt;SchemaDescribable&lt;/span&gt; hace explícito lo que antes estaba implícito — y pertenecía solo a la migración.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Keep coding, keep running&lt;/strong&gt; 🏃‍♂️&lt;/p&gt;
&lt;hr /&gt;</content>
    <link href="https://jcalderita.com/es/blog/model-parts/" rel="alternate"></link>
    <media:content medium="image" url="https://jcalderita.com/static/blog/ModelParts.webp"></media:content>
  </entry>
  <entry>
    <id>https://jcalderita.com/es/blog/ai-status-bar/</id>
    <title>AI Status Bar</title>
    <updated>2026-07-01T00:00:00Z</updated>
    <summary>Cómo monté una status bar personalizada en Claude Code para tener siempre visible el contexto, tokens y límites de uso sin gastar ni un token extra.</summary>
    <content type="html">&lt;hr /&gt;
&lt;h2&gt;Mi Problema 🤔&lt;/h2&gt;
&lt;p&gt;Cuando trabajo con &lt;span class="high"&gt;Claude Code&lt;/span&gt; en la shell, hay información que necesito consultar constantemente: cuánto contexto llevo consumido, en qué porcentaje estoy de los límites de uso, qué rama de git tengo activa, con qué modelo estoy trabajando. Información que no cambia cada segundo, pero que sí necesito tener a mano para tomar decisiones.&lt;/p&gt;
&lt;p&gt;El problema es que obtenerla tiene fricción. Puedo pedírsela a la IA directamente, pero eso implica gastar tokens en una pregunta cuya respuesta es completamente determinista. También puedo abrir otra sesión de terminal, o lanzar un comando en background para consultarlo cada cierto tiempo. Pero todas esas opciones interrumpen el flujo de trabajo.&lt;/p&gt;
&lt;p&gt;Lo que en realidad necesitaba era algo más simple: &lt;strong&gt;una línea siempre visible&lt;/strong&gt; que me diera esa información sin que yo tuviera que pedirla, sin consumir contexto, y sin interrumpir lo que estaba haciendo.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Mi Solución 🧩&lt;/h2&gt;
&lt;p&gt;&lt;span class="high"&gt;Claude Code&lt;/span&gt; tiene una funcionalidad llamada &lt;span class="high"&gt;statusLine&lt;/span&gt; que permite definir un comando externo que se ejecuta cuando hay cambios en la sesión: tras cada respuesta del asistente, al terminar un &lt;span class="high"&gt;/compact&lt;/span&gt;, al cambiar el modo de permisos o al togglear el modo vim. La herramienta llama al script, le pasa información del estado actual de la sesión como JSON por &lt;span class="high"&gt;stdin&lt;/span&gt;, y muestra lo que el script devuelva por &lt;span class="high"&gt;stdout&lt;/span&gt; en la barra inferior de la interfaz.&lt;/p&gt;
&lt;p&gt;La configuración vive en &lt;span class="high"&gt;settings.json&lt;/span&gt; y es de una sola línea:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-json"&gt;{
  &amp;quot;statusLine&amp;quot;: {
    &amp;quot;type&amp;quot;: &amp;quot;command&amp;quot;,
    &amp;quot;command&amp;quot;: &amp;quot;zsh statusline-command.zsh&amp;quot;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;El JSON que recibe el script por &lt;span class="high"&gt;stdin&lt;/span&gt; contiene toda la información relevante de la sesión: modelo activo, directorio de trabajo, porcentaje de uso del contexto, tamaño total de la ventana, y límites de uso de los periodos de 5 horas y 7 días, cada uno con su porcentaje y timestamp de reset.&lt;/p&gt;
&lt;p&gt;El script se ejecuta en cada uno de esos eventos, con un debounce de 300 ms que agrupa cambios rápidos. Y aquí hay un detalle importante: si llega un nuevo evento mientras el script todavía corre, &lt;span class="high"&gt;Claude Code&lt;/span&gt; cancela la ejecución en curso y lanza una nueva — un script lento se interrumpiría a sí mismo. Por eso lo escribí buscando que fuera barato: &lt;span class="high"&gt;builtins&lt;/span&gt; de zsh en lugar de subprocesos, una única llamada a &lt;span class="high"&gt;jq&lt;/span&gt; en lugar de nueve, y helpers que devuelven el resultado en &lt;span class="high"&gt;REPLY&lt;/span&gt; para evitar capturarlo con &lt;span class="high"&gt;$(…)&lt;/span&gt; en el camino caliente.&lt;/p&gt;
&lt;p&gt;Lo primero es cargar el módulo de fecha de zsh y extraer los nueve campos del JSON en una sola pasada. La salida de &lt;span class="high"&gt;jq&lt;/span&gt; es una lista ordenada de valores que voy capturando posicionalmente con &lt;span class="high"&gt;read&lt;/span&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;#!/bin/zsh

zmodload zsh/datetime

MAGENTA=$'\e[35m'; CYAN=$'\e[36m'; YELLOW=$'\e[33m'; GREEN=$'\e[32m'
BLUE=$'\e[34m';    RED=$'\e[31m';  DIM=$'\e[2m';     RESET=$'\e[0m'

input=$(&amp;lt;/dev/stdin)
{
    read -r model_display_name
    read -r current_dir
    read -r used_percentage
    read -r context_window_size
    read -r five_hour_pct
    read -r five_hour_reset
    read -r seven_day_pct
    read -r seven_day_reset
    read -r effort_level
} &amp;lt; &amp;lt;(jq -r '
    .model.display_name // &amp;quot;&amp;quot;,
    .workspace.current_dir // &amp;quot;&amp;quot;,
    (.context_window.used_percentage // 0),
    (.context_window.context_window_size // 0),
    (.rate_limits.five_hour.used_percentage as $p | if $p then ($p | floor) else &amp;quot;&amp;quot; end),
    (.rate_limits.five_hour.resets_at // &amp;quot;&amp;quot;),
    (.rate_limits.seven_day.used_percentage as $p | if $p then ($p | floor) else &amp;quot;&amp;quot; end),
    (.rate_limits.seven_day.resets_at // &amp;quot;&amp;quot;),
    ((.effortLevel // .effort) as $e | if ($e | type) == &amp;quot;object&amp;quot; then $e.level else ($e // &amp;quot;&amp;quot;) end)
' &amp;lt;&amp;lt;&amp;lt;&amp;quot;$input&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Con eso tengo las nueve variables pobladas con un único &lt;span class="high"&gt;fork&lt;/span&gt; de &lt;span class="high"&gt;jq&lt;/span&gt;. Si el nivel de esfuerzo no venía en el JSON, lo busco primero en el &lt;span class="high"&gt;settings.json&lt;/span&gt; del proyecto y, si tampoco está, en el del usuario. Después calculo los derivados y construyo la barra de progreso con el left-padding nativo de zsh:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;extract_effort='(.effortLevel // .effort // empty) | if type == &amp;quot;object&amp;quot; then .level else . end // empty'
[[ -z &amp;quot;$effort_level&amp;quot; &amp;amp;&amp;amp; -f &amp;quot;$current_dir/.claude/settings.json&amp;quot; ]] &amp;amp;&amp;amp; \
    effort_level=$(jq -r &amp;quot;$extract_effort&amp;quot; &amp;quot;$current_dir/.claude/settings.json&amp;quot; 2&amp;gt;/dev/null)
[[ -z &amp;quot;$effort_level&amp;quot; &amp;amp;&amp;amp; -f &amp;quot;$HOME/.claude/settings.json&amp;quot; ]] &amp;amp;&amp;amp; \
    effort_level=$(jq -r &amp;quot;$extract_effort&amp;quot; &amp;quot;$HOME/.claude/settings.json&amp;quot; 2&amp;gt;/dev/null)

current_tokens=$(( used_percentage * context_window_size / 100 ))
current_k=$(( current_tokens / 1000 ))
max_k=$(( context_window_size / 1000 ))
project_name=${current_dir:t}
git_branch=$(git -C &amp;quot;$current_dir&amp;quot; -c core.fileMode=false rev-parse --abbrev-ref HEAD 2&amp;gt;/dev/null)

bar_width=20
filled=$(( used_percentage * bar_width / 100 ))
empty=$(( bar_width - filled ))
progress_bar=&amp;quot;${(l:filled::=:):-}${(l:empty:: :):-}&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Detalles que evitan procesos hijo: &lt;span class="high"&gt;${current_dir:t}&lt;/span&gt; es el equivalente nativo de &lt;span class="high"&gt;basename&lt;/span&gt;, y &lt;span class="high"&gt;${(l:filled::=:):-}&lt;/span&gt; construye una cadena rellenada por la izquierda con caracteres &lt;span class="high"&gt;=&lt;/span&gt; sin bucles ni concatenación.&lt;/p&gt;
&lt;p&gt;Luego vienen los umbrales de color. En la primera versión tenía dos funciones casi idénticas, una para porcentajes y otra para tokens absolutos. Las unifiqué en una sola que recibe el valor y los dos umbrales como argumentos, y aproveché para que devolviera el resultado en &lt;span class="high"&gt;REPLY&lt;/span&gt; en lugar de imprimirlo:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;threshold_color() {
    if (( $1 &amp;gt;= $2 )); then REPLY=$RED
    elif (( $1 &amp;gt;= $3 )); then REPLY=$YELLOW
    else REPLY=$GREEN
    fi
}

format_reset() {
    REPLY=
    [[ -z &amp;quot;$1&amp;quot; ]] &amp;amp;&amp;amp; return
    if (( $1 - EPOCHSECONDS &amp;lt; 86400 )); then
        strftime -s REPLY '%H:%M' $1
    else
        strftime -s REPLY '%a %H:%M' $1
    fi
}

build_rate_part() {
    local part
    if [[ -n &amp;quot;$2&amp;quot; ]]; then
        threshold_color $2 80 50
        part=&amp;quot;${MAGENTA}$1:${RESET} ${REPLY}$2%${RESET}&amp;quot;
    else
        part=&amp;quot;${MAGENTA}$1:${RESET} ${DIM}-${RESET}&amp;quot;
    fi
    format_reset $3
    [[ -n &amp;quot;$REPLY&amp;quot; ]] &amp;amp;&amp;amp; part+=&amp;quot; ${DIM}($REPLY)${RESET}&amp;quot;
    REPLY=$part
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Aplicando esos umbrales:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;span class="high-green"&gt;Contexto verde&lt;/span&gt; por debajo de 100k tokens — zona segura&lt;/li&gt;
&lt;li&gt;&lt;span class="high-yellow"&gt;Contexto amarillo&lt;/span&gt; entre 100k y 150k tokens — señal para considerar un &lt;span class="high"&gt;/clear&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span class="high-red"&gt;Contexto rojo&lt;/span&gt; por encima de 150k tokens — hay que actuar&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Lo mismo para los límites de uso de 5 horas y 7 días: verde por debajo del 50%, amarillo entre 50% y 79%, rojo a partir del 80%.&lt;/p&gt;
&lt;p&gt;Las tres funciones devuelven en &lt;span class="high"&gt;REPLY&lt;/span&gt;: así me ahorro el &lt;span class="high"&gt;$(…)&lt;/span&gt; y el subshell asociado en cada llamada, lo que reduce el riesgo de que una invocación se cancele a sí misma cuando llegan eventos seguidos. &lt;span class="high"&gt;format_reset&lt;/span&gt; tira del builtin &lt;span class="high"&gt;strftime&lt;/span&gt; y de &lt;span class="high"&gt;$EPOCHSECONDS&lt;/span&gt; del módulo &lt;span class="high"&gt;zsh/datetime&lt;/span&gt;, así que tampoco arranca un proceso &lt;span class="high"&gt;date&lt;/span&gt; cada vez.&lt;/p&gt;
&lt;p&gt;Finalmente la composición. La primera línea siempre lleva los mismos elementos: modelo, barra de progreso, porcentaje de contexto, tokens actuales sobre el máximo y nombre del proyecto. La segunda se construye solo con las partes que tengan datos — si no hay límite de 5h, no hay nivel de esfuerzo configurado o no hay rama de git, simplemente no aparecen y los separadores se ajustan:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;threshold_color $current_tokens 150000 100000
context_color=$REPLY
status_line=&amp;quot;${MAGENTA}${model_display_name}${RESET} ${CYAN}[${RESET}${progress_bar}${CYAN}]${RESET}&amp;quot;
status_line+=&amp;quot; ${context_color}${used_percentage}%${RESET} |&amp;quot;
status_line+=&amp;quot; ${context_color}${current_k}k/${max_k}k${RESET} |&amp;quot;
status_line+=&amp;quot; ${BLUE}${project_name}${RESET}&amp;quot;

typeset -a parts
build_rate_part 5h &amp;quot;$five_hour_pct&amp;quot; &amp;quot;$five_hour_reset&amp;quot;; parts+=(&amp;quot;$REPLY&amp;quot;)
build_rate_part 7d &amp;quot;$seven_day_pct&amp;quot; &amp;quot;$seven_day_reset&amp;quot;; parts+=(&amp;quot;$REPLY&amp;quot;)
[[ -n &amp;quot;$effort_level&amp;quot; ]] &amp;amp;&amp;amp; parts+=(&amp;quot;${MAGENTA}effort:${RESET} ${YELLOW}${effort_level}${RESET}&amp;quot;)
[[ -n &amp;quot;$git_branch&amp;quot; ]]   &amp;amp;&amp;amp; parts+=(&amp;quot;${GREEN}${git_branch}${RESET}&amp;quot;)

sep=&amp;quot; ${CYAN}|${RESET} &amp;quot;
(( ${#parts} )) &amp;amp;&amp;amp; status_line+=$'\n'&amp;quot;${(pj.$sep.)parts}&amp;quot;

print -rn -- &amp;quot;$status_line&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;La expansión &lt;span class="high"&gt;${(pj.$sep.)parts}&lt;/span&gt; une los elementos del array &lt;span class="high"&gt;parts&lt;/span&gt; con el separador &lt;span class="high"&gt;sep&lt;/span&gt; — un join nativo de zsh, sin bucles ni hacks con &lt;span class="high"&gt;IFS&lt;/span&gt;.&lt;/p&gt;
&lt;p&gt;Un detalle importante sobre la hora de reset: el script la formatea como &lt;span class="high"&gt;HH:MM&lt;/span&gt; si el reset ocurre dentro de las próximas 24 horas, y como &lt;span class="high"&gt;Día HH:MM&lt;/span&gt; si es más adelante. Así sé de un vistazo cuándo se libera el límite sin tener que hacer cálculos.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Mi Resultado 🎯&lt;/h2&gt;
&lt;p&gt;El resultado es exactamente lo que buscaba: información siempre presente sin interrumpir nada ni gastar un solo token en obtenerla.&lt;/p&gt;
&lt;p&gt;Lo que veo de un vistazo en cualquier momento:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Modelo activo&lt;/strong&gt; — qué versión de Claude está usando la sesión actual&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Barra de contexto&lt;/strong&gt; — representación visual de cuánto contexto he consumido&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tokens actuales sobre el máximo&lt;/strong&gt; — por ejemplo, &lt;span class="high"&gt;87k/200k&lt;/span&gt;, que me dice exactamente en qué punto estoy de la ventana&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Uso 5h y 7d con hora de reset&lt;/strong&gt; — para saber si me acerco a un límite de tasa y cuándo se libera&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Nivel de esfuerzo&lt;/strong&gt; — confirmación visual de que la sesión tiene el nivel correcto configurado&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Rama de git&lt;/strong&gt; — en qué rama estoy trabajando en el proyecto activo&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Lo que más valoro es que esta información &lt;strong&gt;no cuesta nada&lt;/strong&gt;. No es una pregunta a la IA, no es un comando que tengo que recordar lanzar, no es una ventana adicional que abrir. Es texto que aparece, se actualiza solo, y desaparece del foco en cuanto me pongo a trabajar.&lt;/p&gt;
&lt;p&gt;El enfoque también encaja bien con lo que exploraba en &lt;a href="/es/blog/back-to-shell/"&gt;Back to Shell&lt;/a&gt;: la shell como espacio de composición, donde los scripts hacen el trabajo determinista y la IA hace el trabajo contextual. La status bar es el ejemplo más visible de esa separación — toda la lógica de formateo y colores vive en el script, y la IA no sabe que existe.&lt;/p&gt;
&lt;p&gt;Si trabajas con &lt;span class="high"&gt;Claude Code&lt;/span&gt; y tienes información que consultas repetidamente, la &lt;span class="high"&gt;statusLine&lt;/span&gt; es la forma más eficiente de tenerla siempre a mano. El script puede mostrar lo que necesites: uso de APIs externas, estado de un servicio, variables de entorno, lo que quieras exponer desde un comando de shell.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Keep coding, keep running&lt;/strong&gt; 🏃‍♂️&lt;/p&gt;
&lt;hr /&gt;</content>
    <link href="https://jcalderita.com/es/blog/ai-status-bar/" rel="alternate"></link>
    <media:content medium="image" url="https://jcalderita.com/static/blog/AIStatusBar.webp"></media:content>
  </entry>
  <entry>
    <id>https://jcalderita.com/es/blog/back-to-shell/</id>
    <title>Back to Shell</title>
    <updated>2026-06-24T00:00:00Z</updated>
    <summary>Cómo la IA me devolvió a la shell tras años de GUIs. El ciclo completo del desarrollo que vuelve al texto, al comando y al punto de partida.</summary>
    <content type="html">&lt;hr /&gt;
&lt;h2&gt;Mi Problema 🤔&lt;/h2&gt;
&lt;p&gt;Empecé a programar cuando la &lt;span class="high"&gt;shell&lt;/span&gt; no era una opción, era &lt;strong&gt;el sitio&lt;/strong&gt;. No había ratón, no había ventanas. Solo un cursor parpadeando, esperando un comando. Aprendí a pensar en texto: entrada, salida, pipe, redirección.&lt;/p&gt;
&lt;p&gt;Después llegó el giro. La industria decidió que &lt;strong&gt;la productividad tenía cara de ventana&lt;/strong&gt;. Cada disciplina pasó a tener su propio programa, con su interfaz y su curva de aprendizaje:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Bases de datos&lt;/strong&gt; — su aplicación con su panel y sus pestañas&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Control de versiones&lt;/strong&gt; — otra ventana, otra forma de ver lo mismo&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Despliegues&lt;/strong&gt; — otro dashboard, otra cuenta, otros botones&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Observabilidad&lt;/strong&gt; — otro panel, otros gráficos, otra suscripción&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;La &lt;span class="high"&gt;shell&lt;/span&gt; quedó relegada a “eso que usan los que saben mucho” y a las tareas que las interfaces gráficas todavía no habían conquistado.&lt;/p&gt;
&lt;p&gt;Yo seguí la corriente. Fui acumulando aplicaciones, dashboards y ventanas hasta que dejé de visitar la &lt;span class="high"&gt;shell&lt;/span&gt;. La fui desempolvando cada vez con menos frecuencia, hasta que solo aparecía cuando no quedaba más remedio.&lt;/p&gt;
&lt;p&gt;El problema llegó al incorporar la IA a mi flujo de trabajo. No como buscador ni como generador de fragmentos, sino como agente que actúa sobre mi entorno. Y ese agente no tiene ratón. No abre ventanas. Trabaja exactamente como yo al principio: con texto, con comandos, con la &lt;span class="high"&gt;shell&lt;/span&gt;.&lt;/p&gt;
&lt;p&gt;La pregunta que me quedó flotando fue incómoda: ¿había pasado años aprendiendo interfaces gráficas para hacer lo mismo que ya hacía al empezar, pero con más pasos por el medio?&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Mi Solución 🧩&lt;/h2&gt;
&lt;p&gt;La solución no fue una ruptura. No cerré ventanas de golpe ni escribí un manifiesto anti-interfaz. Solo &lt;strong&gt;dejé de resistirme&lt;/strong&gt; a volver a la &lt;span class="high"&gt;shell&lt;/span&gt; cuando el trabajo me llevaba ahí de forma natural.&lt;/p&gt;
&lt;p&gt;El cambio empezó observando. Cuando un agente necesita inspeccionar algo, no busca una vista bonita — escribe un comando. Cuando quiere ver logs, los lee con un pipe. No le interesa qué aplicación prefiero yo. Solo le interesa el resultado.&lt;/p&gt;
&lt;p&gt;Mi primer instinto fue corregirlo: “Espera, para eso tengo una herramienta gráfica.” Pero al detenerme entendí que estaba metiendo fricción donde no había ninguna. El agente sabía qué ejecutar. Yo sabía leer el output. La interfaz no aportaba nada — solo añadía pasos: abrir, conectar, navegar, encontrar.&lt;/p&gt;
&lt;p&gt;Así que empecé a hacer lo mismo por mi cuenta. Donde antes abría una ventana, ahora abro una &lt;span class="high"&gt;shell&lt;/span&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Revisar un estado&lt;/strong&gt; — un comando directo en lugar de un panel que cargar&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Transformar datos&lt;/strong&gt; — encadeno operaciones en lugar de exportar, abrir y volver a guardar&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Automatizar una tarea&lt;/strong&gt; — la escribo una vez y la reutilizo en cualquier sitio&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Reproducir un flujo&lt;/strong&gt; — copio y pego texto en lugar de explicar clics&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;No por nostalgia, sino porque es &lt;strong&gt;más rápido y más composable&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Ese fue el verdadero giro mental: los comandos se componen de un modo que las interfaces gráficas nunca podrán igualar. Una ventana me ofrece lo que su diseñador imaginó que necesitaría. La &lt;span class="high"&gt;shell&lt;/span&gt; me da lo que yo decido que necesito.&lt;/p&gt;
&lt;p&gt;Y hubo algo que me sorprendió: el conocimiento seguía intacto. No había que estudiar otra vez. Solo había que &lt;strong&gt;desempolvar&lt;/strong&gt;.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Mi Resultado 🎯&lt;/h2&gt;
&lt;p&gt;El resultado no ha sido una ruptura con las interfaces gráficas. Sigo abriendo ventanas cuando aportan algo que el texto no puede ofrecer. Pero la &lt;span class="high"&gt;shell&lt;/span&gt; ha vuelto a ocupar &lt;strong&gt;el centro de gravedad&lt;/strong&gt; de mi flujo, en lugar de ser el último recurso.&lt;/p&gt;
&lt;p&gt;Lo que cambió concretamente:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Velocidad&lt;/strong&gt; — un comando es casi siempre más rápido que navegar hasta la opción equivalente en una GUI&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Composabilidad&lt;/strong&gt; — puedo encadenar operaciones que ninguna ventana contempla en combinación&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Consistencia con la IA&lt;/strong&gt; — hablamos el mismo idioma; no hay traducción entre lo que el agente ejecuta y lo que yo veo&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Portabilidad&lt;/strong&gt; — un comando funciona igual en local, en un servidor y en CI&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Pero lo que más me llamó la atención fue el aprendizaje que no tuve que hacer. Daba por hecho que volver a la &lt;span class="high"&gt;shell&lt;/span&gt; exigiría tiempo y práctica. Ese tiempo ya estaba invertido décadas atrás: los comandos, los pipes, la lógica de redirección — todo seguía ahí, esperando una excusa para volver a usarse.&lt;/p&gt;
&lt;p&gt;La IA no me enseñó a usar la &lt;span class="high"&gt;shell&lt;/span&gt;. &lt;strong&gt;Me recordó que ya sabía hacerlo.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;El ciclo que empezó con un triciclo, pasó por el cohete de las interfaces gráficas y vuelve a los fundamentos no es un retroceso. Es un círculo que cierra mejor de lo que abrió. A veces el futuro del desarrollo no consiste en aprender algo nuevo, sino en redescubrir lo que ya sabíamos hacer cuando empezamos.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Keep coding, keep running&lt;/strong&gt; 🏃‍♂️&lt;/p&gt;
&lt;hr /&gt;</content>
    <link href="https://jcalderita.com/es/blog/back-to-shell/" rel="alternate"></link>
    <media:content medium="image" url="https://jcalderita.com/static/blog/BackToShell.webp"></media:content>
  </entry>
  <entry>
    <id>https://jcalderita.com/es/blog/wwdc26-group-labs/</id>
    <title>WWDC26 Group Labs</title>
    <updated>2026-06-17T00:00:00Z</updated>
    <summary>Los Group Labs de la WWDC26 son sesiones de Q&amp;A con ingenieros de Apple. Te cuento mi descubrimiento y dónde encontrar mis notas transcritas.</summary>
    <content type="html">&lt;hr /&gt;
&lt;h2&gt;Mi Problema 🤔&lt;/h2&gt;
&lt;p&gt;Cuando publiqué mi &lt;a href="/es/blog/wwdc26-videos/"&gt;guía de videos de la WWDC26&lt;/a&gt;, dediqué el Bloque 17 a los &lt;span class="high"&gt;Group Labs&lt;/span&gt; con apenas una línea de contexto por sesión. La razón era sencilla: las descripciones oficiales de Apple son prácticamente idénticas entre sí —«Únete a nosotros para profundizar en WWDC26 con ingenieros de Apple»— y no dicen nada útil sobre lo que se habló dentro.&lt;/p&gt;
&lt;p&gt;Lo que no conté entonces es que, antes de escribir ese bloque, me había puesto a ver los Group Labs y me había llevado una sorpresa. No son sesiones pasivas. Son rondas de Q&amp;amp;A en directo entre desarrolladores reales e ingenieros del equipo de Apple, y la calidad de las preguntas y respuestas es muy distinta a la de las sesiones normales. En las sesiones ordinarias, Apple muestra lo que quiere mostrar. En los Group Labs, los desarrolladores preguntan lo que de verdad necesitan saber, y los ingenieros responden de forma directa.&lt;/p&gt;
&lt;p&gt;El problema es que ese contenido es prácticamente invisible. No hay transcripciones, no hay índice, no hay forma de buscar dentro del video. Si no te sientas a ver la sesión entera, no sabes qué cubre.&lt;/p&gt;
&lt;p&gt;Me puse a tomar notas. Una sesión, otra, y al final acabé con dieciocho.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Mi Solución 🧩&lt;/h2&gt;
&lt;p&gt;Decidí transcribir y estructurar el contenido de cada Group Lab: los temas preguntados, el timecode exacto, la respuesta resumida y qué ingenieros participaron. El objetivo era tener algo consultable, que me permitiera ir directamente al minuto que me interesa en lugar de buscar a ciegas en el video.&lt;/p&gt;
&lt;p&gt;Antes de la lista, tres ejemplos de lo que apareció dentro y que no encontrarías en ninguna sesión principal:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;En el &lt;span class="high"&gt;Swift Group Lab&lt;/span&gt;, el equipo del lenguaje explicó por qué habrían diseñado la concurrencia de forma diferente si empezaran hoy, y qué fue lo que cambió en Swift 6.2 para corregir el modelo.&lt;/li&gt;
&lt;li&gt;En el &lt;span class="high"&gt;SwiftUI Group Lab&lt;/span&gt;, los ingenieros desmontaron cuándo usar &lt;span class="high"&gt;AnyView&lt;/span&gt;, cómo funciona &lt;span class="high"&gt;DynamicProperty&lt;/span&gt; antes que &lt;span class="high"&gt;body&lt;/span&gt;, y por qué los condicionales dentro de contenedores lazy son un antipatrón.&lt;/li&gt;
&lt;li&gt;En el &lt;span class="high"&gt;Apple Intelligence Group Lab&lt;/span&gt;, el equipo explicó las diferencias reales entre &lt;span class="high"&gt;App Schemas&lt;/span&gt; e &lt;span class="high"&gt;IndexEntity&lt;/span&gt;, y los límites actuales del soporte de vídeo en Foundation Models.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Aquí están todas las sesiones disponibles, con enlace directo al video oficial:&lt;/p&gt;
&lt;h3&gt;Lenguaje y datos&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="/es/wwdc26/swift-group-lab/"&gt;Swift Group Lab&lt;/a&gt;&lt;/strong&gt; — Concurrencia, rendimiento, SwiftPM y características del lenguaje.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="/es/wwdc26/swiftdata-group-lab/"&gt;SwiftData Group Lab&lt;/a&gt;&lt;/strong&gt; — ResultsObserver, HistoryObserver, el nuevo Codable, CloudKit sync, migraciones y concurrencia.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;SwiftUI&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="/es/wwdc26/swiftui-group-lab-part-1/"&gt;SwiftUI Group Lab - Part 1&lt;/a&gt;&lt;/strong&gt; — Liquid Glass, flujo de datos, rendimiento de layouts y AnyView.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="/es/wwdc26/swiftui-group-lab-part-2/"&gt;SwiftUI Group Lab - Part 2&lt;/a&gt;&lt;/strong&gt; — ForEach con colecciones grandes, NavigationTransition, Liquid Glass en botones y layouts adaptativos.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="/es/wwdc26/swiftui-for-beginners-group-lab/"&gt;SwiftUI for Beginners Group Lab&lt;/a&gt;&lt;/strong&gt; — SwiftUI vs multiplataforma, @State por dentro y rutas de aprendizaje.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Herramientas y rendimiento&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="/es/wwdc26/xcode-tips-and-tricks-group-lab/"&gt;Xcode Tips and Tricks Group Lab&lt;/a&gt;&lt;/strong&gt; — Cómo sacarle el máximo a Xcode 27.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="/es/wwdc26/power-and-performance-group-lab/"&gt;Power and Performance Group Lab&lt;/a&gt;&lt;/strong&gt; — Instrumentación, MetricKit y rendimiento en producción.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Plataformas y web&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="/es/wwdc26/visionos-group-lab/"&gt;visionOS Group Lab&lt;/a&gt;&lt;/strong&gt; — Acceso a la cámara, depuración inmersiva, exportación USD y accesorios espaciales.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="/es/wwdc26/watchos-group-lab/"&gt;watchOS Group Lab&lt;/a&gt;&lt;/strong&gt; — Foundation Models en Watch, transición desde iOS y debugging en dispositivo.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="/es/wwdc26/safari-and-web-technologies-group-lab/"&gt;Safari and Web Technologies Group Lab&lt;/a&gt;&lt;/strong&gt; — WebKit, CSS Grid Lanes, Web Extensions y más.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Inteligencia, ML y AI&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="/es/wwdc26/apple-intelligence-group-lab/"&gt;Apple Intelligence Group Lab&lt;/a&gt;&lt;/strong&gt; — Foundation Models, App Schemas, Siri y Evaluations.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="/es/wwdc26/machine-learning-ai-group-lab/"&gt;Machine Learning &amp;amp; AI Group Lab&lt;/a&gt;&lt;/strong&gt; — Foundation Models, Private Cloud Compute, Core AI, MLX y evaluaciones.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="/es/wwdc26/coding-intelligence-for-beginners-group-lab/"&gt;Coding Intelligence for Beginners Group Lab&lt;/a&gt;&lt;/strong&gt; — Slash commands, Xcode frente a agentes externos y contexto entre proyectos.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="/es/wwdc26/coding-intelligence-machine-learning-ai-group-lab/"&gt;Coding Intelligence, Machine Learning &amp;amp; AI Group Lab&lt;/a&gt;&lt;/strong&gt; — Core AI vs Core ML vs MLX, ventanas de contexto e inferencia en background.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Calidad y distribución&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="/es/wwdc26/accessibility-technologies-group-lab/"&gt;Accessibility Technologies Group Lab&lt;/a&gt;&lt;/strong&gt; — VoiceOver, Dynamic Type y accesibilidad en controles personalizados.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="/es/wwdc26/privacy-and-security-group-lab/"&gt;Privacy and Security Group Lab&lt;/a&gt;&lt;/strong&gt; — App Attest, Trust Insights y diseño seguro de features agénticas.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="/es/wwdc26/camera-and-photo-technologies-group-lab/"&gt;Camera and Photo Technologies Group Lab&lt;/a&gt;&lt;/strong&gt; — AVFoundation, captura RAW y Center Stage.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="/es/wwdc26/app-store-connect-group-lab/"&gt;App Store Connect Group Lab&lt;/a&gt;&lt;/strong&gt; — In-App Purchase, suscripciones y StoreKit.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;Mi Resultado 🎯&lt;/h2&gt;
&lt;p&gt;El valor de los Group Labs no está en las descripciones oficiales —está dentro. Las preguntas de los desarrolladores sacan a la superficie los casos de uso que Apple no cubre en las sesiones principales: los bugs que el equipo reconoce, las decisiones de diseño que explican en off, los patrones que recomiendan en la práctica y no en el marketing.&lt;/p&gt;
&lt;p&gt;Si ya tienes mi &lt;a href="/es/blog/wwdc26-videos/"&gt;guía de videos&lt;/a&gt; como mapa del catálogo, los Group Labs son la capa que va encima: donde los ingenieros de Apple hablan de verdad sobre lo que construyeron.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Keep coding, keep running&lt;/strong&gt; 🏃‍♂️&lt;/p&gt;
&lt;hr /&gt;</content>
    <link href="https://jcalderita.com/es/blog/wwdc26-group-labs/" rel="alternate"></link>
    <media:content medium="image" url="https://jcalderita.com/static/blog/GroupLab26.webp"></media:content>
  </entry>
  <entry>
    <id>https://jcalderita.com/es/blog/wwdc26-videos/</id>
    <title>WWDC26 Videos</title>
    <updated>2026-06-11T00:00:00Z</updated>
    <summary>WWDC26 publica más de cien sesiones. Me he armado mi propia agrupación y checklist para verlas en mi orden y tachar bloques a lo largo del año.</summary>
    <content type="html">&lt;hr /&gt;
&lt;h2&gt;Mi Problema 🤔&lt;/h2&gt;
&lt;p&gt;Cada año el mismo ritual: el lunes después de la Keynote abro los videos de la &lt;a href="https://developer.apple.com/videos/all-videos/?collection=wwdc26"&gt;WWDC26&lt;/a&gt; y me encuentro con más de cien sesiones. Apple ofrece filtros por tema, plataforma y framework que se pueden ir acumulando hasta dejar el catálogo a tu medida — una base estupenda para arrancar.&lt;/p&gt;
&lt;p&gt;Lo que me pasa a mí es que, incluso con esos filtros activos, si no me marco un orden propio acabo saltando de video en video según el título que más me llame en ese momento. Veo algo de &lt;span class="high"&gt;SwiftUI&lt;/span&gt;, luego salto a &lt;span class="high"&gt;AI&lt;/span&gt;, luego me pongo con herramientas, y al final del día he consumido cinco sesiones inconexas que no se refuerzan entre sí. La comprensión es superficial porque cada bloque temático necesita contexto previo que no tengo.&lt;/p&gt;
&lt;p&gt;Llevo tiempo queriéndome armar mi propio esquema sobre lo que Apple ofrece. Un camino con sentido, que construya el conocimiento de manera acumulativa, que ponga primero lo que da contexto para todo lo demás — y que me sirva también como checklist para tachar bloques a medida que avanzo durante el año.&lt;/p&gt;
&lt;p&gt;Este año lo he hecho.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Mi Solución 🧩&lt;/h2&gt;
&lt;p&gt;Mi criterio es simple: &lt;strong&gt;primero la visión general, luego el núcleo del lenguaje, después las herramientas, y al final las áreas específicas según lo que más use en mi trabajo&lt;/strong&gt;. Cada bloque prepara el terreno para el siguiente.&lt;/p&gt;
&lt;p&gt;Dejo fuera solo las versiones en ASL y los recaps (Dub Dub Daily, recap del State of the Union). Todo lo demás entra — incluso los Group Labs, que aunque eran sesiones interactivas en directo Apple los mantiene en el catálogo y los dejo localizados al final por si necesitas referencia del tema.&lt;/p&gt;
&lt;p&gt;Lo que viene a continuación es la agrupación que uso yo, y que voy tachando bloque a bloque a lo largo del año. Si te sirve, hazla tuya: lo que importa es tener un camino que seguir, no que sea el mío.&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;Bloque 0 — Las presentaciones&lt;/h3&gt;
&lt;p&gt;Antes de cualquier sesión técnica, estos videos dan el marco del año. El teaser abre boca; Keynote y State of the Union son los que mandan.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://developer.apple.com/videos/play/wwdc2026/394/"&gt;Get ready for WWDC26&lt;/a&gt;&lt;/strong&gt;&lt;br /&gt;
El teaser pre-evento de la propia WWDC. Marginal una vez vista la Keynote pero está en el catálogo.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://developer.apple.com/videos/play/wwdc2026/101/"&gt;Keynote&lt;/a&gt;&lt;/strong&gt;&lt;br /&gt;
El gran anuncio del año. &lt;span class="high"&gt;Liquid Glass&lt;/span&gt;, &lt;span class="high"&gt;Apple Intelligence&lt;/span&gt; con la nueva &lt;span class="high"&gt;Siri&lt;/span&gt;, &lt;span class="high"&gt;Xcode 27&lt;/span&gt; con &lt;span class="high"&gt;agentes de código&lt;/span&gt;. No es técnico — es el por qué de todo lo demás.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://developer.apple.com/videos/play/wwdc2026/102/"&gt;Platforms State of the Union&lt;/a&gt;&lt;/strong&gt;&lt;br /&gt;
La Keynote para desarrolladores. Aquí ya aparece la capa técnica: qué cambia en las plataformas, qué prioridades tiene Apple este ciclo, y qué va a importar para el trabajo del día a día. Este video es el mapa del territorio.&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;Bloque 1 — Swift y herramientas&lt;/h3&gt;
&lt;p&gt;Con el contexto del año claro, entro en el núcleo. &lt;span class="high"&gt;Swift&lt;/span&gt; primero porque todo lo demás construye sobre el lenguaje.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://developer.apple.com/videos/play/wwdc2026/262/"&gt;What’s new in Swift&lt;/a&gt;&lt;/strong&gt;&lt;br /&gt;
Actualización del lenguaje: mejoras de ergonomía, &lt;span class="high"&gt;concurrencia&lt;/span&gt; mejorada, código de alto rendimiento más seguro, Embedded Swift e interoperabilidad con C y Java. El estado del lenguaje en 2026.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://developer.apple.com/videos/play/wwdc2026/258/"&gt;What’s new in Xcode 27&lt;/a&gt;&lt;/strong&gt;&lt;br /&gt;
Las novedades del entorno: &lt;span class="high"&gt;agentes de código&lt;/span&gt;, &lt;span class="high"&gt;Device Hub&lt;/span&gt;, &lt;span class="high"&gt;localización&lt;/span&gt;, &lt;span class="high"&gt;rendimiento&lt;/span&gt; y &lt;span class="high"&gt;herramientas de testing&lt;/span&gt;. Si voy a vivir aquí, necesito saber qué ha cambiado.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://developer.apple.com/videos/play/wwdc2026/260/"&gt;Get the most out of Device Hub&lt;/a&gt;&lt;/strong&gt;&lt;br /&gt;
Tour profundo de &lt;span class="high"&gt;Device Hub&lt;/span&gt;: cómo acelera el flujo de desarrollo y cómo diagnosticar y reproducir issues rápido con dispositivos y simuladores. El compañero natural del video anterior.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://developer.apple.com/videos/play/wwdc2026/259/"&gt;Xcode, agents, and you&lt;/a&gt;&lt;/strong&gt;&lt;br /&gt;
Cómo incorporar los &lt;span class="high"&gt;agentes de código de Xcode&lt;/span&gt; al flujo de trabajo real. Desde el prototipo inicial hasta pulir una app terminada. La parte práctica de lo que se presentó en el video anterior.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://developer.apple.com/videos/play/wwdc2026/227/"&gt;Create UI prototypes using agents in Xcode&lt;/a&gt;&lt;/strong&gt;&lt;br /&gt;
&lt;span class="high"&gt;Prototipado con agentes&lt;/span&gt;: técnicas para iterar sobre layouts, generar soluciones creativas y refinar las ideas hasta una experiencia pulida. La cara creativa de los agentes en Xcode.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://developer.apple.com/videos/play/wwdc2026/213/"&gt;Translate your app using agents in Xcode&lt;/a&gt;&lt;/strong&gt;&lt;br /&gt;
Cómo los &lt;span class="high"&gt;agentes traducen String Catalogs&lt;/span&gt; usando el contexto de la app. Estrategias para revisar el output y ajustar las localizaciones. La cara práctica para producto global.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://developer.apple.com/videos/play/wwdc2026/261/"&gt;Build, deliver, and automate with Xcode Cloud&lt;/a&gt;&lt;/strong&gt;&lt;br /&gt;
Las novedades de &lt;span class="high"&gt;Xcode Cloud&lt;/span&gt;: arranque simplificado, tests en la nube, webhooks y distribución. Fundamentos para cualquiera que lo use en CI.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://developer.apple.com/videos/play/wwdc2026/268/"&gt;Profile, fix, and verify: Improve app responsiveness with Instruments&lt;/a&gt;&lt;/strong&gt;&lt;br /&gt;
Un flujo completo para atacar problemas de rendimiento: &lt;span class="high"&gt;Swift Concurrency instrument&lt;/span&gt;, &lt;span class="high"&gt;Time Profiler&lt;/span&gt; y &lt;span class="high"&gt;System Trace&lt;/span&gt;. Encontrar el cuello de botella, medir la mejora, confirmar el fix.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://developer.apple.com/videos/play/wwdc2026/222/"&gt;Meet the new MetricKit&lt;/a&gt;&lt;/strong&gt;&lt;br /&gt;
&lt;span class="high"&gt;MetricKit&lt;/span&gt; renovado con métricas de rendimiento vitales y diagnósticos accionables. Cruzar métricas con estado de la app vía &lt;span class="high"&gt;StateReporting framework&lt;/span&gt; para tener el cuadro completo de optimización.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://developer.apple.com/videos/play/wwdc2026/267/"&gt;Migrate to Swift Testing&lt;/a&gt;&lt;/strong&gt;&lt;br /&gt;
Adoptar &lt;span class="high"&gt;Swift Testing&lt;/span&gt; junto a los XCTests existentes usando interoperabilidad entre frameworks. Patrones para introducir las nuevas capacidades de testing de manera incremental. Conecta con lo que cuento en &lt;a href="/es/blog/sigbus/"&gt;SIGBUS&lt;/a&gt;.&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;Bloque 2 — SwiftUI y persistencia&lt;/h3&gt;
&lt;p&gt;El stack de UI y datos. Lo veo junto porque en la práctica van de la mano.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://developer.apple.com/videos/play/wwdc2026/269/"&gt;What’s new in SwiftUI&lt;/a&gt;&lt;/strong&gt;&lt;br /&gt;
Nuevo protocolo &lt;span class="high"&gt;Document&lt;/span&gt; con acceso directo a disco, APIs de reordenación en listas y grids, mejoras en toolbars, nuevas presentaciones, &lt;span class="high"&gt;caché en AsyncImage&lt;/span&gt; y lazy state para &lt;span class="high"&gt;Observable&lt;/span&gt;. El overview de lo nuevo en el framework.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://developer.apple.com/videos/play/wwdc2026/274/"&gt;What’s new in SwiftData&lt;/a&gt;&lt;/strong&gt;&lt;br /&gt;
Persistencia de tipos externos con &lt;span class="high"&gt;Codable&lt;/span&gt;, agrupación de resultados en secciones para SwiftUI, &lt;span class="high"&gt;ModelResultsObserver&lt;/span&gt; e &lt;span class="high"&gt;HistoryObserver&lt;/span&gt; para observar cambios fuera de la vista. Las novedades del ORM de Apple.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://developer.apple.com/videos/play/wwdc2026/275/"&gt;Code-along: Add persistence with SwiftData&lt;/a&gt;&lt;/strong&gt;&lt;br /&gt;
Manos al teclado: añadir &lt;span class="high"&gt;persistencia&lt;/span&gt; a una app existente paso a paso. Definir modelos, integrar datos persistentes con SwiftUI, gestionar el estado de la app. El mejor sitio para que lo visto en el video anterior se consolide.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://developer.apple.com/videos/play/wwdc2026/271/"&gt;Code-along: Build powerful drag and drop in SwiftUI&lt;/a&gt;&lt;/strong&gt;&lt;br /&gt;
Construir Solitario para explorar las nuevas capacidades de drag and drop: &lt;span class="high"&gt;API de reordenación&lt;/span&gt;, drag containers para mover múltiples ítems, y ciclo de vida personalizado. Práctico y directo.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://developer.apple.com/videos/play/wwdc2026/321/"&gt;Dive into lazy stacks and scrolling with SwiftUI&lt;/a&gt;&lt;/strong&gt;&lt;br /&gt;
Rendimiento de listas y scroll en SwiftUI. Cuándo usar lazy stacks, cómo &lt;span class="high"&gt;optimizar el comportamiento de scroll&lt;/span&gt;, qué patrones evitar.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://developer.apple.com/videos/play/wwdc2026/322/"&gt;Compose advanced graphics effects with SwiftUI&lt;/a&gt;&lt;/strong&gt;&lt;br /&gt;
&lt;span class="high"&gt;Efectos gráficos&lt;/span&gt; avanzados con SwiftUI. Composición de capas, blending modes y efectos visuales que van más allá de lo básico.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://developer.apple.com/videos/play/wwdc2026/272/"&gt;Use SwiftUI with AppKit and UIKit&lt;/a&gt;&lt;/strong&gt;&lt;br /&gt;
Adopción incremental de SwiftUI en apps existentes de AppKit o UIKit: el framework &lt;span class="high"&gt;Observation&lt;/span&gt; para actualizar vistas, integrar componentes SwiftUI en jerarquías existentes, y añadir escenas completas SwiftUI sin cambiar la arquitectura.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://developer.apple.com/videos/play/wwdc2026/278/"&gt;Modernize your UIKit app&lt;/a&gt;&lt;/strong&gt;&lt;br /&gt;
Actualizar layouts de iPhone para funcionar bien con &lt;span class="high"&gt;iPhone Mirroring&lt;/span&gt; y en iPad. Nuevas APIs de tab y navigation bars, preparar la app para Apple Intelligence, y un skill para el agente de código que moderniza el codebase.&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;Bloque 3 — Apple Intelligence y Foundation Models&lt;/h3&gt;
&lt;p&gt;El bloque que más me interesa este año. Lo veo como una unidad porque cada video construye sobre el anterior.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://developer.apple.com/videos/play/wwdc2026/241/"&gt;What’s new in the Foundation Models framework&lt;/a&gt;&lt;/strong&gt;&lt;br /&gt;
Acceso a &lt;span class="high"&gt;Private Cloud Compute&lt;/span&gt;, integración de modelos de terceros y open source, capacidades de visión, APIs de gestión de contexto, &lt;span class="high"&gt;búsqueda semántica integrada&lt;/span&gt; y primitivas para experiencias agénticas. El overview del framework principal de AI en Apple.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://developer.apple.com/videos/play/wwdc2026/242/"&gt;Build agentic app experiences with the Foundation Models framework&lt;/a&gt;&lt;/strong&gt;&lt;br /&gt;
Perfiles dinámicos, instrucciones dinámicas, &lt;span class="high"&gt;gestión del contexto&lt;/span&gt; y patrones de orquestación entre modelos locales y de servidor. La capa avanzada sobre el video anterior.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://developer.apple.com/videos/play/wwdc2026/339/"&gt;Bring an LLM provider to the Foundation Models framework&lt;/a&gt;&lt;/strong&gt;&lt;br /&gt;
Extender el framework implementando un &lt;span class="high"&gt;LanguageModelExecutor&lt;/span&gt; para modelos nuevos. Interactuar con el transcript de la sesión, gestionar estado, optimizar el cache KV y soportar tipos de segmento personalizados. La puerta para integrar tu propio modelo en el stack de Apple.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://developer.apple.com/videos/play/wwdc2026/243/"&gt;Debug and profile agentic app experiences with Instruments&lt;/a&gt;&lt;/strong&gt;&lt;br /&gt;
Cómo depurar y perfilar experiencias agénticas con &lt;span class="high"&gt;Instruments&lt;/span&gt;. Porque cuando algo falla en un agente, necesitas saber dónde y por qué.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://developer.apple.com/videos/play/wwdc2026/298/"&gt;Meet the Evaluations framework&lt;/a&gt;&lt;/strong&gt;&lt;br /&gt;
En un mundo probabilístico, los tests unitarios no bastan. &lt;span class="high"&gt;Métricas cuantitativas y cualitativas&lt;/span&gt;, jueces de modelos, estadísticas agregadas para asegurar que las features de AI funcionan de manera fiable.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://developer.apple.com/videos/play/wwdc2026/299/"&gt;Create robust evaluations for agentic apps&lt;/a&gt;&lt;/strong&gt;&lt;br /&gt;
La &lt;span class="high"&gt;capa avanzada de evaluaciones&lt;/span&gt;: flujos con tool calling, condiciones dinámicas, generación de datos sintéticos, jueces y validación de datasets. El trabajo real detrás de que un agente se comporte correctamente.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://developer.apple.com/videos/play/wwdc2026/335/"&gt;Improve your prompts by hill-climbing with Evaluations&lt;/a&gt;&lt;/strong&gt;&lt;br /&gt;
Usar el ciclo de evaluación para mejorar prompts de manera iterativa. El cierre del bloque de evaluaciones.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://developer.apple.com/videos/play/wwdc2026/334/"&gt;Build AI-powered scripts with the fm CLI and Python SDK&lt;/a&gt;&lt;/strong&gt;&lt;br /&gt;
Scripts con el &lt;span class="high"&gt;CLI de Foundation Models&lt;/span&gt; y el SDK de Python. Automatización y prototipado fuera del entorno Swift.&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;Bloque 4 — Siri y App Intents&lt;/h3&gt;
&lt;p&gt;El puente entre la app y la inteligencia del sistema. Voy de lo conceptual a lo práctico.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://developer.apple.com/videos/play/wwdc2026/121/"&gt;Announcing Apple’s next big step for Siri and iPhone&lt;/a&gt;&lt;/strong&gt;&lt;br /&gt;
El gran anuncio del nuevo Siri. El contexto imprescindible antes de entrar en los videos técnicos de integración.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://developer.apple.com/videos/play/wwdc2026/345/"&gt;Discover new capabilities in the App Intents framework&lt;/a&gt;&lt;/strong&gt;&lt;br /&gt;
&lt;span class="high"&gt;ValueRepresentation&lt;/span&gt;, &lt;span class="high"&gt;RelevantEntities&lt;/span&gt;, &lt;span class="high"&gt;EntityCollection&lt;/span&gt;, &lt;span class="high"&gt;SyncableEntity&lt;/span&gt;, tipos de parámetro más ricos y long-running intents. Las novedades del framework base.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://developer.apple.com/videos/play/wwdc2026/343/"&gt;Explore advanced App Intents features for Siri and Apple Intelligence&lt;/a&gt;&lt;/strong&gt;&lt;br /&gt;
Técnicas avanzadas para que Siri interaccione con la app de manera natural: descubrimiento de contenido, &lt;span class="high"&gt;indexación semántica&lt;/span&gt;, búsqueda estructurada y conexión con notificaciones y Now Playing.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://developer.apple.com/videos/play/wwdc2026/240/"&gt;Build intelligent Siri experiences with App Schemas&lt;/a&gt;&lt;/strong&gt;&lt;br /&gt;
Adoptar &lt;span class="high"&gt;App Schemas&lt;/span&gt; para que la gente haga preguntas sobre datos de la app y tome acciones mediante lenguaje natural. Ejemplo práctico con eventos de calendario y Spotlight.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://developer.apple.com/videos/play/wwdc2026/344/"&gt;Code-along: Make your app available to Siri&lt;/a&gt;&lt;/strong&gt;&lt;br /&gt;
El code-along de todo lo anterior: integrar &lt;span class="high"&gt;App Intents&lt;/span&gt; en una app de calendario real, crear entidades, activar acciones de Siri y personalizar los snippets de respuesta.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://developer.apple.com/videos/play/wwdc2026/295/"&gt;Validate your App Intents adoption with AppIntentsTesting&lt;/a&gt;&lt;/strong&gt;&lt;br /&gt;
&lt;span class="high"&gt;AppIntentsTesting&lt;/span&gt;: validar los intents con la misma infraestructura que usan Siri, Shortcuts y Spotlight. Ejecutar intents, inspeccionar resultados, testear entities y queries sin automatización de UI. El cierre del bloque — probar lo que has integrado.&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;Bloque 5 — Core AI y ML&lt;/h3&gt;
&lt;p&gt;Para quien quiere ir más allá de los frameworks de alto nivel y entrar en la capa de modelos.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://developer.apple.com/videos/play/wwdc2026/324/"&gt;Meet Core AI&lt;/a&gt;&lt;/strong&gt;&lt;br /&gt;
El &lt;span class="high"&gt;nuevo framework&lt;/span&gt; para despliegue de modelos de AI en el dispositivo: ecosistema completo desde Python para conversión y optimización hasta la API Swift para inferencia. Integración profunda con Xcode y compilación ahead-of-time.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://developer.apple.com/videos/play/wwdc2026/326/"&gt;Integrate on-device AI models into your app using Core AI&lt;/a&gt;&lt;/strong&gt;&lt;br /&gt;
Colección de modelos open source optimizados para Apple silicon — &lt;span class="high"&gt;Qwen&lt;/span&gt;, &lt;span class="high"&gt;Mistral&lt;/span&gt;, &lt;span class="high"&gt;SAM3&lt;/span&gt; — y cómo integrarlos con pocas líneas de código. Compilación AOT y especialización on-device.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://developer.apple.com/videos/play/wwdc2026/325/"&gt;Dive into Core AI model authoring and optimization&lt;/a&gt;&lt;/strong&gt;&lt;br /&gt;
Autoría y optimización de modelos con herramientas Python de &lt;span class="high"&gt;Core AI&lt;/span&gt;. La capa profunda para quien quiere controlar el pipeline completo.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://developer.apple.com/videos/play/wwdc2026/330/"&gt;Optimize custom machine learning operations with Metal tensors&lt;/a&gt;&lt;/strong&gt;&lt;br /&gt;
La &lt;span class="high"&gt;Metal Tensor API&lt;/span&gt; y la librería &lt;span class="high"&gt;MPP Tensor Ops&lt;/span&gt;. Crear operaciones portátiles que aprovechen los Neural Accelerators de M5 y A19, construir kernels custom para apps Core AI y trabajar con formatos cuantizados. La capa baja para empujar el rendimiento al máximo.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://developer.apple.com/videos/play/wwdc2026/328/"&gt;Explore numerical computing in Swift with MLX&lt;/a&gt;&lt;/strong&gt;&lt;br /&gt;
MLX para &lt;span class="high"&gt;computación numérica&lt;/span&gt; en Swift. Las primitivas sobre las que se construye todo lo demás en ML.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://developer.apple.com/videos/play/wwdc2026/233/"&gt;Explore distributed inference and training with MLX&lt;/a&gt;&lt;/strong&gt;&lt;br /&gt;
Inferencia y entrenamiento distribuidos con &lt;span class="high"&gt;MLX&lt;/span&gt;. Escalar a múltiples Macs.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://developer.apple.com/videos/play/wwdc2026/232/"&gt;Run local agentic AI on the Mac using MLX&lt;/a&gt;&lt;/strong&gt;&lt;br /&gt;
Agentes de AI corriendo &lt;span class="high"&gt;localmente con privacidad&lt;/span&gt;, baja latencia y sin conexión. &lt;span class="high"&gt;OpenCode&lt;/span&gt;, integración con Xcode y técnicas para escalar entre Macs.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://developer.apple.com/videos/play/wwdc2026/319/"&gt;Build with the new Apple Foundation Model on Private Cloud Compute&lt;/a&gt;&lt;/strong&gt;&lt;br /&gt;
Usar el nuevo modelo de Apple en &lt;span class="high"&gt;Private Cloud Compute&lt;/span&gt;. El extremo del espectro: cuando el dispositivo no es suficiente pero la privacidad sigue siendo prioritaria.&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;Bloque 6 — Networking, servicios e infraestructura&lt;/h3&gt;
&lt;p&gt;Tres videos que cubren la capa de comunicación y runtime: del cliente al servicio, y de la infraestructura que los corre.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://developer.apple.com/videos/play/wwdc2026/265/"&gt;Build real-time apps and services with gRPC and Swift&lt;/a&gt;&lt;/strong&gt;&lt;br /&gt;
&lt;span class="high"&gt;gRPC Swift&lt;/span&gt; para experiencias en tiempo real: el framework RPC open source con streaming bidireccional, construido sobre Swift concurrency. Desde la definición del servicio en &lt;span class="high"&gt;Protobuf&lt;/span&gt; hasta el despliegue en producción. Los mensajes Protobuf son un 50% más pequeños que JSON y el runtime es moderno y seguro.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://developer.apple.com/videos/play/wwdc2026/389/"&gt;Discover container machines&lt;/a&gt;&lt;/strong&gt;&lt;br /&gt;
&lt;span class="high"&gt;Máquinas contenedor&lt;/span&gt; en Apple platforms. Infraestructura para desarrollo y despliegue de servicios Swift.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://developer.apple.com/videos/play/wwdc2026/224/"&gt;Expand the capabilities of your Virtualization app&lt;/a&gt;&lt;/strong&gt;&lt;br /&gt;
Capacidades nuevas en macOS 27 para apps de &lt;span class="high"&gt;Virtualization&lt;/span&gt;. Automatización del setup de huéspedes macOS en el primer arranque, passthrough de accesorios USB, topologías de red custom y port forwarding.&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;Bloque 7 — Diseño y accesibilidad&lt;/h3&gt;
&lt;p&gt;La capa que no se ve pero que define si una app es buena o no.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://developer.apple.com/videos/play/wwdc2026/250/"&gt;Principles of great design&lt;/a&gt;&lt;/strong&gt;&lt;br /&gt;
Los principios fundamentales del &lt;span class="high"&gt;diseño de apps&lt;/span&gt; Apple. El marco conceptual antes de entrar en APIs concretas.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://developer.apple.com/videos/play/wwdc2026/251/"&gt;Communicate your brand identity on iOS&lt;/a&gt;&lt;/strong&gt;&lt;br /&gt;
Expresar la &lt;span class="high"&gt;identidad de marca&lt;/span&gt; en iOS con las nuevas herramientas de personalización visual.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://developer.apple.com/videos/play/wwdc2026/292/"&gt;Design intuitive search experiences&lt;/a&gt;&lt;/strong&gt;&lt;br /&gt;
&lt;span class="high"&gt;Diseñar búsquedas&lt;/span&gt; que la gente entienda y use. Patrones y antipatrones.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://developer.apple.com/videos/play/wwdc2026/290/"&gt;Craft clear names for features and labels in your app&lt;/a&gt;&lt;/strong&gt;&lt;br /&gt;
&lt;span class="high"&gt;Nomenclatura de features y labels&lt;/span&gt;. Uno de esos videos que parece menor y que acaba siendo de los más prácticos.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://developer.apple.com/videos/play/wwdc2026/220/"&gt;Refine accessibility for custom controls&lt;/a&gt;&lt;/strong&gt;&lt;br /&gt;
&lt;span class="high"&gt;Accesibilidad en controles personalizados&lt;/span&gt;. Cómo hacer que lo que construyes funcione para todo el mundo.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://developer.apple.com/videos/play/wwdc2026/289/"&gt;Modernize your AppKit app&lt;/a&gt;&lt;/strong&gt;&lt;br /&gt;
Modernizar AppKit a las convenciones actuales de macOS. Input con &lt;span class="high"&gt;control events&lt;/span&gt; y gesture recognizers más allá de tracking loops, navegación por teclado, restauración de estado, y nuevas APIs de corner concentricity para encajar con la estética macOS.&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;Bloque 8 — Integración con el sistema&lt;/h3&gt;
&lt;p&gt;Las APIs que ponen mi app dentro del sistema: superficies como Live Activities, widgets, Lock Screen o CarPlay; integración con apps nativas (Workouts), accesorios Bluetooth, y atajos. Si la app vive más allá de su propia ventana, este bloque es relevante.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://developer.apple.com/videos/play/wwdc2026/223/"&gt;Live Activities essentials&lt;/a&gt;&lt;/strong&gt;&lt;br /&gt;
Fundamentos de &lt;span class="high"&gt;Live Activities&lt;/span&gt;: dónde aparecen, el nuevo estilo de &lt;span class="high"&gt;Dynamic Island&lt;/span&gt; en landscape, cómo estructurar contenido y datos, y cómo dispararlas en tiempo real con ActivityKit y push notifications. La base si voy a usar esta API.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://developer.apple.com/videos/play/wwdc2026/277/"&gt;WidgetKit foundations&lt;/a&gt;&lt;/strong&gt;&lt;br /&gt;
Los tipos de widget, qué los hace memorables, y cómo mantenerlos al día. &lt;span class="high"&gt;Personalización vía App Intents&lt;/span&gt; y estilos dinámicos. Si la app necesita vivir fuera de su icono, empieza aquí.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://developer.apple.com/videos/play/wwdc2026/226/"&gt;Create live communication experiences&lt;/a&gt;&lt;/strong&gt;&lt;br /&gt;
&lt;span class="high"&gt;LiveCommunicationKit&lt;/span&gt; transforma apps de comunicación real-time en experiencias integradas. UI de conversación nativa, presentación a pantalla completa en Lock Screen, multitasking con Dynamic Island. Conversaciones entrantes, salientes y de grupo.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://developer.apple.com/videos/play/wwdc2026/212/"&gt;Rev up your CarPlay app&lt;/a&gt;&lt;/strong&gt;&lt;br /&gt;
Nuevas features para apps de audio, navegación y conversación por voz en &lt;span class="high"&gt;CarPlay&lt;/span&gt;. Apps de video para reproducir contenido en vehículos compatibles cuando están aparcados. Thumbnails, info de media y controles por voz.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://developer.apple.com/videos/play/wwdc2026/207/"&gt;Deliver workout insights with HealthKit workout zones&lt;/a&gt;&lt;/strong&gt;&lt;br /&gt;
&lt;span class="high"&gt;HealthKit&lt;/span&gt; con workout insights —zonas de frecuencia cardiaca y de potencia en ciclismo. Zonas built-in o personalizadas, zona actual y tiempo en cada zona para guiar durante y después del entrenamiento.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://developer.apple.com/videos/play/wwdc2026/369/"&gt;Find your accessory with Bluetooth Channel Sounding&lt;/a&gt;&lt;/strong&gt;&lt;br /&gt;
&lt;span class="high"&gt;Channel Sounding&lt;/span&gt; añade distancia y dirección a accesorios Bluetooth. Nuevas APIs de Nearby Interaction y Core Bluetooth y los cambios necesarios en el accesorio. Consumo optimizado sin perder respuesta.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://developer.apple.com/videos/play/wwdc2026/310/"&gt;What’s new in Shortcuts&lt;/a&gt;&lt;/strong&gt;&lt;br /&gt;
Construir atajos con el contenido de la app, nuevas automatizaciones del sistema, y la feature &lt;span class="high"&gt;Use Model&lt;/span&gt; para refinar cómo se presenta una App Entity a los LLMs. Atajos que almacenan información rica sincronizada entre dispositivos.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://developer.apple.com/videos/play/wwdc2026/246/"&gt;LLM search using Core Spotlight&lt;/a&gt;&lt;/strong&gt;&lt;br /&gt;
Convertir la búsqueda básica en un sistema &lt;span class="high"&gt;RAG&lt;/span&gt; con &lt;span class="high"&gt;SpotlightSearchTool&lt;/span&gt; y LanguageModelSession. Integración con Core Spotlight, hidratación por delegate, PipelineStages para tareas como análisis de sentimiento. Búsqueda contextual real sobre el índice del sistema.&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;Bloque 9 — Seguridad y administración&lt;/h3&gt;
&lt;p&gt;Cinco videos que importan más que nunca este año. Con toda la AI añadida, dar por hecho que la integridad de la app, la inyección de prompts o la manipulación del usuario no son problema tuyo es un riesgo. Y para apps que viven en entornos gestionados —educación, empresa, flota— también la postura administrativa importa.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://developer.apple.com/videos/play/wwdc2026/347/"&gt;Secure your app: mitigate risks to agentic features&lt;/a&gt;&lt;/strong&gt;&lt;br /&gt;
Evaluar amenazas de &lt;span class="high"&gt;inyección indirecta de prompts&lt;/span&gt;: data exfiltration, acciones no deseadas. Salvaguardas del sistema y buenas prácticas con App Intents y Foundation Models —confirmaciones de usuario, diseño seguro de prompts, autenticación. Obligatorio si tengo features agénticas.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://developer.apple.com/videos/play/wwdc2026/201/"&gt;Secure your apps with App Attest&lt;/a&gt;&lt;/strong&gt;&lt;br /&gt;
&lt;span class="high"&gt;App Attest&lt;/span&gt; contra modificación no autorizada y fraude. Cómo los atacantes spoofean datos y saltan checks, y cómo defenderse. Generar y gestionar claves en el &lt;span class="high"&gt;Secure Enclave&lt;/span&gt;, validar attestations y assertions, y usar la fraud metric para detectar abuso.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://developer.apple.com/videos/play/wwdc2026/379/"&gt;Meet Trust Insights&lt;/a&gt;&lt;/strong&gt;&lt;br /&gt;
&lt;span class="high"&gt;Trust Insights&lt;/span&gt; usa ML preservando la privacidad para detectar cuándo alguien podría estar siendo manipulado a acciones de riesgo. Integración, interpretación de señales y diseño de intervenciones que protejan respetando la privacidad.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://developer.apple.com/videos/play/wwdc2026/206/"&gt;What’s new in managing Apple devices&lt;/a&gt;&lt;/strong&gt;&lt;br /&gt;
Updates a &lt;span class="high"&gt;declarative device management&lt;/span&gt;, Apple Business y Apple School Manager. Streamline de despliegue, refuerzo de seguridad y mejora de la experiencia de la flota gestionada.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://developer.apple.com/videos/play/wwdc2026/230/"&gt;What’s new in assessment on macOS&lt;/a&gt;&lt;/strong&gt;&lt;br /&gt;
&lt;span class="high"&gt;Automatic Assessment Configuration&lt;/span&gt; en macOS para apps de educación. APIs para crear entornos de examen seguros y configurables que incorporan más features del sistema. Prechecks automáticos y controles de accesibilidad para una experiencia de examen fiable.&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;Bloque 10 — visionOS, RealityKit y contenido espacial&lt;/h3&gt;
&lt;p&gt;El bloque más extenso del catálogo. Lo veo solo si trabajo con Vision Pro o si quiero experimentar con escenas espaciales. Si no es mi área, lo dejo para más adelante.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://developer.apple.com/videos/play/wwdc2026/287/"&gt;Build next-generation experiences with visionOS 27&lt;/a&gt;&lt;/strong&gt;&lt;br /&gt;
El &lt;span class="high"&gt;overview de visionOS 27&lt;/span&gt;: las distintas vías para construir experiencias —tools nativas de Apple, streaming inmersivo desde Mac o PC, third-party engines, port de apps iOS. Avances en creación de contenido 3D, immersive media y object tracking. El video con el que arrancar el bloque.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://developer.apple.com/videos/play/wwdc2026/279/"&gt;Explore advances in RealityKit&lt;/a&gt;&lt;/strong&gt;&lt;br /&gt;
Las novedades de &lt;span class="high"&gt;RealityKit&lt;/span&gt;: simulaciones interactivas de tela, &lt;span class="high"&gt;NavMesh&lt;/span&gt; pathfinding, iluminación mixed reality, reverb meshes personalizables para audio espacial. Mejor sombreado, character rendering y soporte para &lt;span class="high"&gt;Gaussian splatting&lt;/span&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://developer.apple.com/videos/play/wwdc2026/234/"&gt;Design immersive environments for visionOS apps and the spatial web&lt;/a&gt;&lt;/strong&gt;&lt;br /&gt;
Crear &lt;span class="high"&gt;entornos fotorrealistas&lt;/span&gt; para apps visionOS, webs y experiencias SharePlay. Principios de diseño, captura de referencias, preparación de assets CG de alta fidelidad y efectos en tiempo real como movimiento e iluminación.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://developer.apple.com/videos/play/wwdc2026/280/"&gt;Iterate your spatial scenes faster with Reality Composer Pro 3&lt;/a&gt;&lt;/strong&gt;&lt;br /&gt;
Las novedades de &lt;span class="high"&gt;Reality Composer Pro 3&lt;/span&gt;: contenido, VFX, iluminación e interactividad sin salir del editor. Features asistidas por AI integradas en el flujo. La herramienta donde vivirás si haces contenido espacial.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://developer.apple.com/videos/play/wwdc2026/281/"&gt;Extend Reality Composer Pro 3 functionality with Xcode&lt;/a&gt;&lt;/strong&gt;&lt;br /&gt;
&lt;span class="high"&gt;Plugins&lt;/span&gt; específicos del proyecto en Reality Composer Pro 3: componentes custom, sistemas custom, nodos propios de &lt;span class="high"&gt;ScriptGraph&lt;/span&gt;. Control completo del workflow de autoría espacial.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://developer.apple.com/videos/play/wwdc2026/282/"&gt;Discover the Spatial Preview framework&lt;/a&gt;&lt;/strong&gt;&lt;br /&gt;
El nuevo &lt;span class="high"&gt;Spatial Preview framework&lt;/span&gt; envía contenido desde Mac directo a visionOS. Live-syncing y edición bidireccional entre plataformas. La API SpatialPreview, device discovery, integración 2D/3D y nuevas capacidades de Quick Look.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://developer.apple.com/videos/play/wwdc2026/283/"&gt;Explore enhancements to visionOS object tracking&lt;/a&gt;&lt;/strong&gt;&lt;br /&gt;
Avances en &lt;span class="high"&gt;object tracking&lt;/span&gt; y accesorios espaciales. Trackear objetos en movimiento y handheld, nuevas clases de accesorios espaciales soportadas, y cómo construir accesorios custom para abrir modelos de interacción únicos.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://developer.apple.com/videos/play/wwdc2026/284/"&gt;Collaborate on structured 3D models in visionOS&lt;/a&gt;&lt;/strong&gt;&lt;br /&gt;
Modelos 3D estructurados en visionOS: preparación &lt;span class="high"&gt;USDZ&lt;/span&gt;, manipulación de entidades dentro de ensamblajes jerárquicos, inspección con un plano de corte transversal. Animaciones tipo exploded-view para diseño y colaboración en Vision Pro.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://developer.apple.com/videos/play/wwdc2026/285/"&gt;Discover USDKit and what’s new in OpenUSD&lt;/a&gt;&lt;/strong&gt;&lt;br /&gt;
&lt;span class="high"&gt;USDKit&lt;/span&gt; en Swift, la nueva API de preview espacial, web espacial mejorado. Updates al estándar &lt;span class="high"&gt;OpenUSD&lt;/span&gt;: accesibilidad, Gaussian splats, geometría comprimida. Tools de edición y rendering USD ampliadas en Preview para Mac.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://developer.apple.com/videos/play/wwdc2026/286/"&gt;Use foveated streaming to bring immersive content to visionOS&lt;/a&gt;&lt;/strong&gt;&lt;br /&gt;
&lt;span class="high"&gt;Foveated streaming&lt;/span&gt;: escenas renderizadas remotamente a Vision Pro en máxima fidelidad. Combina capacidades nativas con streaming third-party de forma inalámbrica, demostrado con una escena &lt;span class="high"&gt;OpenXR&lt;/span&gt; y NVIDIA CloudXR. Foveated dinámico sin comprometer la privacidad.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://developer.apple.com/videos/play/wwdc2026/320/"&gt;Explore immersive website environments in visionOS&lt;/a&gt;&lt;/strong&gt;&lt;br /&gt;
La nueva &lt;span class="high"&gt;Immersive API&lt;/span&gt; en JavaScript transporta visitantes de tu web a entornos virtuales en Vision Pro. Transiciones desde un model element inline, video docking, optimización a escala real. Pocas líneas de código corriendo en la web.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://developer.apple.com/videos/play/wwdc2026/393/"&gt;Supercharge your spatial workflows with Reality Composer Pro 3&lt;/a&gt;&lt;/strong&gt;&lt;br /&gt;
Interactividad rica y efectos visuales completos dentro de Reality Composer Pro: &lt;span class="high"&gt;Shader Graph&lt;/span&gt; para materiales, &lt;span class="high"&gt;Animation Graph&lt;/span&gt; para blending de animaciones esqueléticas, &lt;span class="high"&gt;Compute Graph&lt;/span&gt; para partículas. Script Graph para interactividad, Sequencer para eventos y Behavior Trees para NPCs.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://developer.apple.com/videos/play/wwdc2026/338/"&gt;Build live production tools for Apple Immersive Video&lt;/a&gt;&lt;/strong&gt;&lt;br /&gt;
Producción en directo de &lt;span class="high"&gt;Apple Immersive Video&lt;/span&gt;. Empaquetar video inmersivo, audio espacial y metadata de escena para transporte sobre IP con el estándar &lt;span class="high"&gt;SMPTE 2110&lt;/span&gt;. Immersive Media Support, Video Toolbox y AVFoundation para flujos en tiempo real.&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;Bloque 11 — Juegos y Metal&lt;/h3&gt;
&lt;p&gt;Si trabajo en un juego, este bloque es parada obligada. Si no, lo veo por curiosidad porque hay piezas de rendimiento que aplican también a apps no-juego.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://developer.apple.com/videos/play/wwdc2026/356/"&gt;Bringing Cyberpunk 2077 to Mac&lt;/a&gt;&lt;/strong&gt;&lt;br /&gt;
Cómo CD PROJEKT RED llevó &lt;span class="high"&gt;Cyberpunk 2077 a Mac&lt;/span&gt; aprovechando hardware, software y tools de Apple. Técnicas aplicables a otros juegos. El preset &lt;span class="high"&gt;For this Mac&lt;/span&gt; que optimiza settings automáticamente para balancear fidelidad visual y framerate en toda la línea Mac.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://developer.apple.com/videos/play/wwdc2026/357/"&gt;Speedrun your game port with agentic coding&lt;/a&gt;&lt;/strong&gt;&lt;br /&gt;
Las nuevas &lt;span class="high"&gt;skills agénticas&lt;/span&gt; del Game Porting Toolkit 4 aceleran el port. Adoptar &lt;span class="high"&gt;Metal 4&lt;/span&gt;, integrar &lt;span class="high"&gt;MetalFX&lt;/span&gt;, tune del juego para hardware Apple. Agentes que diagnostican issues de GPU rendering autónomamente.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://developer.apple.com/videos/play/wwdc2026/358/"&gt;Make your game great with touch&lt;/a&gt;&lt;/strong&gt;&lt;br /&gt;
Controles touch convincentes para juegos. Insights de devs indie a AAA, buenas prácticas, y cómo aprovechar el &lt;span class="high"&gt;Touch Controller framework&lt;/span&gt; y Metal para rendimiento óptimo.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://developer.apple.com/videos/play/wwdc2026/252/"&gt;Design no-code games with Reality Composer Pro 3&lt;/a&gt;&lt;/strong&gt;&lt;br /&gt;
&lt;span class="high"&gt;ScriptGraph&lt;/span&gt; en Reality Composer Pro 3 para contenido 3D no-code. Nodos visuales para animaciones, momentos interactivos, e integración con SwiftUI para añadir bocadillos y UI a la experiencia.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://developer.apple.com/videos/play/wwdc2026/359/"&gt;Build real-time neural rendering pipelines with Metal&lt;/a&gt;&lt;/strong&gt;&lt;br /&gt;
ML integrado en el pipeline de rendering con &lt;span class="high"&gt;Metal 4&lt;/span&gt;. &lt;span class="high"&gt;MetalFX neural denoising&lt;/span&gt; con insights de Maxon Redshift Live. Entrenar y desplegar un neural tone mapper inline con el ML command encoder, y la nueva tensor API para redes pequeñas dentro de tus shaders.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://developer.apple.com/videos/play/wwdc2026/388/"&gt;Find and fix performance issues in your Metal games&lt;/a&gt;&lt;/strong&gt;&lt;br /&gt;
Caza de problemas de rendimiento con tools de Metal: &lt;span class="high"&gt;Game Performance Overview&lt;/span&gt; en Instruments, traces en background con metalperftrace y Control Center, y la nueva &lt;span class="high"&gt;StateReporting API&lt;/span&gt; para correlar métricas con el estado runtime del juego. Horas de telemetría convertidas en insights accionables.&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;Bloque 12 — Cámara, fotografía e imagen&lt;/h3&gt;
&lt;p&gt;Para apps con cámara o procesamiento de imagen. Algunos videos son muy específicos (RAW, Center Stage), otros aplican incluso si solo despliego un preview.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://developer.apple.com/videos/play/wwdc2026/237/"&gt;What’s new in image understanding&lt;/a&gt;&lt;/strong&gt;&lt;br /&gt;
Vision framework y Foundation Models actualizados. La nueva request &lt;span class="high"&gt;tap-to-segment&lt;/span&gt; para segmentar imágenes, Vision en watchOS, y soporte de imágenes en el Apple Foundation Model combinado con OCR, escaneo de barcodes y tools propias.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://developer.apple.com/videos/play/wwdc2026/297/"&gt;Best practices for integrating visual intelligence in your app&lt;/a&gt;&lt;/strong&gt;&lt;br /&gt;
Cómo &lt;span class="high"&gt;Visual Intelligence&lt;/span&gt; transforma el descubrimiento de contenido. Definir entidades, procesar imágenes, manejar múltiples tipos de resultado. Optimización de velocidad y relevancia, e intents para acciones directas como abrir o reproducir contenido con un tap.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://developer.apple.com/videos/play/wwdc2026/303/"&gt;Build a responsive camera app that launches quickly&lt;/a&gt;&lt;/strong&gt;&lt;br /&gt;
Optimizar la secuencia completa de arranque de la cámara —desde el startup hasta el &lt;span class="high"&gt;primer frame de preview&lt;/span&gt;. Nuevas APIs para lanzamientos más rápidos, rendering fluido y rendimiento sostenido. Para que la gente nunca se pierda el momento.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://developer.apple.com/videos/play/wwdc2026/304/"&gt;Implement high resolution photo capture&lt;/a&gt;&lt;/strong&gt;&lt;br /&gt;
Captura de fotos en alta resolución con AVFoundation. Las tres opciones —&lt;span class="high"&gt;RAW&lt;/span&gt;, &lt;span class="high"&gt;exposure-bracketed&lt;/span&gt;, &lt;span class="high"&gt;fully processed&lt;/span&gt;— y cuándo usar cada una. Configurar captura de 24MP y 48MP en cámaras Main, Tele y Ultra Wide. Deferred photo processing para que la app responda incluso con disparos rápidos.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://developer.apple.com/videos/play/wwdc2026/305/"&gt;Enhance RAW image processing with Core Image&lt;/a&gt;&lt;/strong&gt;&lt;br /&gt;
La versión 9 de las APIs RAW de &lt;span class="high"&gt;Core Image&lt;/span&gt;: más nitidez, color más definido, Apple Neural Engine para rendimiento. &lt;span class="high"&gt;CIRAWFilter&lt;/span&gt; para editar exposición, ruido, sharpness y contraste. Nuevas CIImageProcessor APIs con control fino sobre tile sizing y buffers.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://developer.apple.com/videos/play/wwdc2026/341/"&gt;Support the Center Stage front camera in your iOS app&lt;/a&gt;&lt;/strong&gt;&lt;br /&gt;
&lt;span class="high"&gt;Center Stage&lt;/span&gt; con AVCapture APIs en la cámara frontal de iPhone 17, 17 Pro y Air. Zoom y rotate, framing flexible para selfies y videos, todos en el encuadre en fotos de grupo. Integración en videollamadas con auto-framing y estabilización en tiempo real.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://developer.apple.com/videos/play/wwdc2026/375/"&gt;Create high quality images using Image Playground&lt;/a&gt;&lt;/strong&gt;&lt;br /&gt;
Generación de imágenes con &lt;span class="high"&gt;Image Playground&lt;/span&gt;. Modelo generativo en &lt;span class="high"&gt;Private Cloud Compute&lt;/span&gt;, imágenes en casi cualquier estilo incluyendo fotorrealista, dimensiones específicas, y modificación mediante lenguaje natural y touch.&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;Bloque 13 — Web y Safari&lt;/h3&gt;
&lt;p&gt;Para web devs: las novedades de WebKit y nuevas piezas de CSS y HTML que Safari estrena este año.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://developer.apple.com/videos/play/wwdc2026/204/"&gt;What’s new in WebKit for Safari 27&lt;/a&gt;&lt;/strong&gt;&lt;br /&gt;
El overview de WebKit y &lt;span class="high"&gt;Safari 27&lt;/span&gt;: Grid Lanes, Customizable Select, HTML Model, Immersive Environments, lo último de Web Extensions. Más de 1.000 mejoras del engine para hacer la web más fiable.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://developer.apple.com/videos/play/wwdc2026/215/"&gt;Get started with the HTML Model Element&lt;/a&gt;&lt;/strong&gt;&lt;br /&gt;
El &lt;span class="high"&gt;model element&lt;/span&gt; trae contenido 3D interactivo a webs en iOS, iPadOS, macOS y visionOS. Tools para crear y optimizar assets 3D, features del model element, y hacia dónde van los web standards en 3D.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://developer.apple.com/videos/play/wwdc2026/216/"&gt;Create web extensions for Safari&lt;/a&gt;&lt;/strong&gt;&lt;br /&gt;
Construir y probar &lt;span class="high"&gt;web extensions&lt;/span&gt; de Safari desde cero —sin Xcode. Content blocking, modificación de páginas, native messaging y el permissions mode trabajando juntos para una experiencia potente y respetuosa con la privacidad.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://developer.apple.com/videos/play/wwdc2026/314/"&gt;Learn CSS Grid Lanes&lt;/a&gt;&lt;/strong&gt;&lt;br /&gt;
&lt;span class="high"&gt;Grid Lanes&lt;/span&gt; para layouts web adaptables con elementos de formas variadas. CSS limpio y flexible, y &lt;span class="high"&gt;flow-tolerance&lt;/span&gt; para refinar accesibilidad sin perder maleabilidad.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://developer.apple.com/videos/play/wwdc2026/315/"&gt;Rediscover the HTML select element&lt;/a&gt;&lt;/strong&gt;&lt;br /&gt;
Control total del estilo en select menus web. Nuevo valor CSS &lt;span class="high"&gt;appearance&lt;/span&gt; y nuevos pseudo-elementos, contenido rico dentro de options con nuevas posibilidades HTML. Selects a medida del design system sin renunciar a accesibilidad ni robustez.&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;Bloque 14 — StoreKit, App Store, suscripciones y Wallet&lt;/h3&gt;
&lt;p&gt;Para todo lo que toca monetización y App Store Connect. Si vendo algo en mi app, este bloque importa.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://developer.apple.com/videos/play/wwdc2026/210/"&gt;What’s new in Apple In-App Purchase&lt;/a&gt;&lt;/strong&gt;&lt;br /&gt;
Las nuevas &lt;span class="high"&gt;suscripciones mensuales con compromiso a 12 meses&lt;/span&gt; como opción más asequible. Configurar y testear con App Store Connect, StoreKit APIs y Xcode testing. Mejoras en redención de offer codes y en la experiencia de App Review.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://developer.apple.com/videos/play/wwdc2026/205/"&gt;Enhance your presence on the App Store&lt;/a&gt;&lt;/strong&gt;&lt;br /&gt;
Reimaginar el marketing en el App Store usando imágenes y videos en nuevos lugares. Placements visuales nuevos en product page, resultados de búsqueda y campañas de Apple Ads. El nuevo &lt;span class="high"&gt;Asset Library&lt;/span&gt; centraliza assets y un tool permite ver la product page antes de publicarla.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://developer.apple.com/videos/play/wwdc2026/209/"&gt;What’s new in Wallet&lt;/a&gt;&lt;/strong&gt;&lt;br /&gt;
Nuevos estilos de pase para diseños ricos. Nuevos formatos de barcode y una API flexible de pass actions. &lt;span class="high"&gt;Pass Designer&lt;/span&gt; y &lt;span class="high"&gt;Pass Builder&lt;/span&gt; simplifican diseñar, personalizar y distribuir pases a escala.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://developer.apple.com/videos/play/wwdc2026/309/"&gt;Explore Retention Messaging in App Store Connect&lt;/a&gt;&lt;/strong&gt;&lt;br /&gt;
&lt;span class="high"&gt;Retention Messaging&lt;/span&gt; para alcanzar suscriptores antes de que cancelen. Configurar en App Store Connect, añadir subscription offers, y usar la API para mensajería en tiempo real y alternativas que animen a quedarse.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://developer.apple.com/videos/play/wwdc2026/378/"&gt;Unlock in-game content with StoreKit and Background Assets&lt;/a&gt;&lt;/strong&gt;&lt;br /&gt;
In-App Purchases nativas en Unity con el nuevo &lt;span class="high"&gt;StoreKit plug-in&lt;/span&gt;. Reducción del tamaño de descarga con &lt;span class="high"&gt;Background Assets plug-in&lt;/span&gt; entregando packs por idioma. Y un Steam Asset Converter para migrar builds existentes.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://developer.apple.com/videos/play/wwdc2026/391/"&gt;Offer subscriptions to groups and organizations&lt;/a&gt;&lt;/strong&gt;&lt;br /&gt;
&lt;span class="high"&gt;Group Purchases&lt;/span&gt;: un suscriptor compra varios asientos e invita a otros desde la app. &lt;span class="high"&gt;Volume Purchasing&lt;/span&gt; vía Apple Business y Apple School Manager pone tus suscripciones delante de compradores enterprise y educación que ya compran apps a escala.&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;Bloque 15 — Música, audio y subtítulos&lt;/h3&gt;
&lt;p&gt;Para apps con contenido multimedia. Hay un framework nuevo este año (&lt;span class="high"&gt;Music Understanding&lt;/span&gt;) que merece atención incluso si no haces apps de música.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://developer.apple.com/videos/play/wwdc2026/253/"&gt;Meet the Music Understanding framework&lt;/a&gt;&lt;/strong&gt;&lt;br /&gt;
Nuevo framework &lt;span class="high"&gt;Music Understanding&lt;/span&gt; que analiza audio en seis dimensiones, on-device: clave, ritmo, estructura, pace, actividad instrumental y loudness. Y un sample app, Music Understanding Lab, para visualizar resultados.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://developer.apple.com/videos/play/wwdc2026/254/"&gt;Integrate MusicKit into your app&lt;/a&gt;&lt;/strong&gt;&lt;br /&gt;
Apple Music dentro de tu app con &lt;span class="high"&gt;MusicKit&lt;/span&gt;. Autorización, suscripción, selección de música, control de playback y compartir canciones entre storefronts. Nuevo &lt;span class="high"&gt;Music Picker&lt;/span&gt; para navegar el catálogo y librerías personales. Diferencias entre SystemMusicPlayer y ApplicationMusicPlayer.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://developer.apple.com/videos/play/wwdc2026/256/"&gt;Discover generated subtitles and subtitle styles&lt;/a&gt;&lt;/strong&gt;&lt;br /&gt;
&lt;span class="high"&gt;Subtítulos generados&lt;/span&gt; on-device que transcriben o traducen desde otro idioma. Caption style preview para personalizar y previsualizar durante el playback. Implementación con AVKit, AVPlayerLayer y Media Accessibility.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://developer.apple.com/videos/play/wwdc2026/312/"&gt;Meet the Now Playing framework&lt;/a&gt;&lt;/strong&gt;&lt;br /&gt;
Nuevo framework Swift &lt;span class="high"&gt;Now Playing&lt;/span&gt; que conecta el playback de tu app con las superficies del sistema: Lock Screen, Control Center, Dynamic Island, CarPlay. Publicar estado, responder a comandos vía API observable, y &lt;span class="high"&gt;remote playback sessions&lt;/span&gt; para representar media en dispositivos externos.&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;Bloque 16 — Accesibilidad, lectura y texto&lt;/h3&gt;
&lt;p&gt;La capa del texto y la lectura. Aquí no es solo APIs nuevas — es cómo encaja la app con las preferencias de cada persona.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://developer.apple.com/videos/play/wwdc2026/219/"&gt;Enhance the accessibility of your reading app&lt;/a&gt;&lt;/strong&gt;&lt;br /&gt;
Experiencias de lectura robustas para &lt;span class="high"&gt;VoiceOver&lt;/span&gt; y &lt;span class="high"&gt;Speak Screen&lt;/span&gt;. Selección de texto intuitiva, navegación clara entre líneas y párrafos, y lectura continua entre elementos y entre páginas.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://developer.apple.com/videos/play/wwdc2026/221/"&gt;Prepare your tvOS apps for Dynamic Type&lt;/a&gt;&lt;/strong&gt;&lt;br /&gt;
&lt;span class="high"&gt;Dynamic Type&lt;/span&gt; en tvOS: implementar escalado de fuentes, adaptar layouts para tamaños grandes, optimizar grids y carousels media-focused. Para que cualquier tamaño de texto siga siendo cómodo.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://developer.apple.com/videos/play/wwdc2026/370/"&gt;Elevate your app’s text experience with TextKit&lt;/a&gt;&lt;/strong&gt;&lt;br /&gt;
Combinar la conveniencia de los text views built-in con el control de &lt;span class="high"&gt;TextKit&lt;/span&gt;. Nuevas APIs para extender UITextView y NSTextView con comportamientos custom como números de línea o secciones colapsables. Arquitectura TextKit, caching y políticas de reuse para text attachments.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://developer.apple.com/videos/play/wwdc2026/372/"&gt;Unwrap PaperKit&lt;/a&gt;&lt;/strong&gt;&lt;br /&gt;
Aplicaciones canvas-based con &lt;span class="high"&gt;PaperKit&lt;/span&gt;. APIs nuevas del data model para acceder, crear y modificar elementos de markup. Controles custom, anotaciones, y buenas prácticas para integrar un canvas creativo completo.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://developer.apple.com/videos/play/wwdc2026/203/"&gt;Read between the strokes with PencilKit&lt;/a&gt;&lt;/strong&gt;&lt;br /&gt;
Reconocimiento de escritura a mano con la misma tecnología detrás de Freeform y Notes. Reconocimiento en muchos alfabetos e idiomas, y nuevas capacidades para integrar &lt;span class="high"&gt;PencilKit&lt;/span&gt; en más tipos de apps.&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;Bloque 17 — Group Labs&lt;/h3&gt;
&lt;p&gt;Las Group Labs eran sesiones de Q&amp;amp;A en directo con ingenieros y diseñadores de Apple durante la semana del evento. Apple las mantiene en el catálogo aunque la sesión interactiva ya pasara, así que sirven como referencia rápida del área en la que esté trabajando. Las descripciones oficiales son prácticamente la misma boilerplate, así que las agrupo por tema con una línea de contexto.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Núcleo del lenguaje y frameworks principales&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://developer.apple.com/videos/play/wwdc2026/8001/"&gt;Swift Group Lab&lt;/a&gt;&lt;/strong&gt; — Q&amp;amp;A sobre los anuncios de Swift de la semana.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://developer.apple.com/videos/play/wwdc2026/8006/"&gt;SwiftUI Group Lab&lt;/a&gt;&lt;/strong&gt; y &lt;strong&gt;&lt;a href="https://developer.apple.com/videos/play/wwdc2026/8120/"&gt;SwiftUI Group Lab (segunda tanda)&lt;/a&gt;&lt;/strong&gt; — Dos sesiones de Q&amp;amp;A sobre los anuncios de SwiftUI.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://developer.apple.com/videos/play/wwdc2026/8002/"&gt;SwiftUI for Beginners Group Lab&lt;/a&gt;&lt;/strong&gt; — Q&amp;amp;A para arrancar con SwiftUI desde cero.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://developer.apple.com/videos/play/wwdc2026/8017/"&gt;SwiftData Group Lab&lt;/a&gt;&lt;/strong&gt; — Q&amp;amp;A sobre los anuncios de SwiftData.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Xcode, rendimiento y plataformas&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://developer.apple.com/videos/play/wwdc2026/8013/"&gt;Xcode Tips and Tricks Group Lab&lt;/a&gt;&lt;/strong&gt; — Q&amp;amp;A para sacarle el máximo a Xcode.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://developer.apple.com/videos/play/wwdc2026/8003/"&gt;Power and Performance Group Lab&lt;/a&gt;&lt;/strong&gt; — Q&amp;amp;A sobre los anuncios de potencia y rendimiento.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://developer.apple.com/videos/play/wwdc2026/8004/"&gt;visionOS Group Lab&lt;/a&gt;&lt;/strong&gt; — Q&amp;amp;A sobre los anuncios de visionOS.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://developer.apple.com/videos/play/wwdc2026/8014/"&gt;watchOS Group Lab&lt;/a&gt;&lt;/strong&gt; — Q&amp;amp;A sobre los anuncios de watchOS.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://developer.apple.com/videos/play/wwdc2026/8015/"&gt;Safari and Web Technologies Group Lab&lt;/a&gt;&lt;/strong&gt; — Q&amp;amp;A sobre los anuncios de Safari y tecnologías web.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Inteligencia, ML y AI&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://developer.apple.com/videos/play/wwdc2026/8011/"&gt;Apple Intelligence Group Lab&lt;/a&gt;&lt;/strong&gt; — Q&amp;amp;A sobre los anuncios de Apple Intelligence.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://developer.apple.com/videos/play/wwdc2026/8016/"&gt;Machine Learning &amp;amp; AI Group Lab&lt;/a&gt;&lt;/strong&gt; — Q&amp;amp;A sobre los anuncios de ML y AI.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://developer.apple.com/videos/play/wwdc2026/8007/"&gt;Coding Intelligence for Beginners Group Lab&lt;/a&gt;&lt;/strong&gt; — Q&amp;amp;A para arrancar con coding intelligence.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://developer.apple.com/videos/play/wwdc2026/8121/"&gt;Coding Intelligence, Machine Learning &amp;amp; AI Group Lab&lt;/a&gt;&lt;/strong&gt; — Q&amp;amp;A combinado sobre coding intelligence, ML y AI.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Plataforma, distribución y diseño&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://developer.apple.com/videos/play/wwdc2026/8005/"&gt;Accessibility Technologies Group Lab&lt;/a&gt;&lt;/strong&gt; — Q&amp;amp;A sobre los anuncios de accesibilidad.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://developer.apple.com/videos/play/wwdc2026/8009/"&gt;Privacy and Security Group Lab&lt;/a&gt;&lt;/strong&gt; — Q&amp;amp;A sobre los anuncios de privacidad y seguridad.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://developer.apple.com/videos/play/wwdc2026/8010/"&gt;App Store Connect Group Lab&lt;/a&gt;&lt;/strong&gt; — Q&amp;amp;A sobre los anuncios de App Store Connect.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://developer.apple.com/videos/play/wwdc2026/8018/"&gt;Camera and Photo Technologies Group Lab&lt;/a&gt;&lt;/strong&gt; — Q&amp;amp;A sobre los anuncios de cámara y fotografía.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://developer.apple.com/videos/play/wwdc2026/8012/"&gt;Icon Composer for Beginners Group Lab&lt;/a&gt;&lt;/strong&gt; — Q&amp;amp;A para arrancar con Icon Composer.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;Mi Resultado 🎯&lt;/h2&gt;
&lt;p&gt;Con este orden me ahorro el caos del catálogo y empiezo a ver resultados desde la primera semana.&lt;/p&gt;
&lt;p&gt;Mi secuencia resumida:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Bloque 0&lt;/strong&gt; — Las presentaciones&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Bloque 1&lt;/strong&gt; — Swift y herramientas&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Bloque 2&lt;/strong&gt; — SwiftUI y persistencia&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Bloque 3&lt;/strong&gt; — Apple Intelligence y Foundation Models&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Bloque 4&lt;/strong&gt; — Siri y App Intents&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Bloque 5&lt;/strong&gt; — Core AI y ML&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Bloque 6&lt;/strong&gt; — Networking, servicios e infraestructura&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Bloque 7&lt;/strong&gt; — Diseño y accesibilidad&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Bloque 8&lt;/strong&gt; — Integración con el sistema&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Bloque 9&lt;/strong&gt; — Seguridad y administración&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Bloque 10&lt;/strong&gt; — visionOS, RealityKit y contenido espacial&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Bloque 11&lt;/strong&gt; — Juegos y Metal&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Bloque 12&lt;/strong&gt; — Cámara, fotografía e imagen&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Bloque 13&lt;/strong&gt; — Web y Safari&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Bloque 14&lt;/strong&gt; — StoreKit, App Store, suscripciones y Wallet&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Bloque 15&lt;/strong&gt; — Música, audio y subtítulos&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Bloque 16&lt;/strong&gt; — Accesibilidad, lectura y texto&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Bloque 17&lt;/strong&gt; — Group Labs&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Si llevas años viendo el &lt;span class="high"&gt;WWDC&lt;/span&gt; sabrás que el catálogo nunca caduca — los videos están disponibles todo el año en &lt;a href="https://developer.apple.com/videos/all-videos/?collection=wwdc26"&gt;developer.apple.com&lt;/a&gt;. Junio es para las novedades, el resto del año para profundizar. Como conté en &lt;a href="/es/blog/wwdc26/"&gt;WWDC 26&lt;/a&gt;, las sesiones técnicas son el plato fuerte — y ahora ya tengo mi propia checklist por la que ir tachando.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Keep coding, keep running&lt;/strong&gt; 🏃‍♂️&lt;/p&gt;
&lt;hr /&gt;</content>
    <link rel="alternate" href="https://jcalderita.com/es/blog/wwdc26-videos/"></link>
    <media:content medium="image" url="https://jcalderita.com/static/blog/WWDC26Videos.webp"></media:content>
  </entry>
  <entry>
    <id>https://jcalderita.com/es/blog/claude-voice/</id>
    <title>Claude Voice</title>
    <updated>2026-06-10T00:00:00Z</updated>
    <summary>Cómo un accidente en carrera de monte me obligó a comunicarme con la IA usando la voz y terminé aprendiendo una habilidad que no sabía que necesitaba.</summary>
    <content type="html">&lt;hr /&gt;
&lt;h2&gt;Mi Problema 🤔&lt;/h2&gt;
&lt;p&gt;Tengo un problema con la expresión oral que me ha acompañado toda la vida: &lt;strong&gt;cuando hablo, las ideas se me amontonan&lt;/strong&gt;. Mi cabeza va más rápido que mi voz. Empiezo a decir algo, aparece otra idea relacionada, luego otra, y antes de terminar la primera frase ya he perdido el hilo. El resultado es que me expreso peor de lo que pienso.&lt;/p&gt;
&lt;p&gt;Escribiendo, en cambio, tengo tiempo. Escribir es lento por naturaleza, y esa lentitud me obliga a ordenar. Mientras el dedo busca la letra, el pensamiento se asienta. Por eso desde que uso &lt;span class="high"&gt;Claude Code&lt;/span&gt; intensivamente, siempre he trabajado con texto. El teclado es mi capa de filtro natural.&lt;/p&gt;
&lt;p&gt;Eso funcionó bien hasta que dejó de funcionar.&lt;/p&gt;
&lt;p&gt;En una carrera de monte tuve un accidente. Caída tonta, consecuencias no tan tontas, semanas de recuperación, y lo que más me dolió en ese momento: &lt;strong&gt;poca movilidad para teclear con normalidad&lt;/strong&gt;. De golpe, mi método de trabajo preferido quedó bloqueado.&lt;/p&gt;
&lt;p&gt;Las opciones eran dos: o usaba la voz o asumía que iba a trabajar a un ritmo absurdamente lento. No era una elección filosófica sobre los modos de comunicación con la IA. Era pura necesidad.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Mi Solución 🧩&lt;/h2&gt;
&lt;p&gt;Empecé a usar la voz con &lt;span class="high"&gt;Claude Code&lt;/span&gt; a la fuerza, sin romantizar el proceso. Los primeros días fueron exactamente tan malos como esperaba. Frases cortadas, ideas que llegaban en el orden equivocado, contexto que se perdía por el camino. El modelo respondía bien, pero yo le estaba dando material mediocre.&lt;/p&gt;
&lt;p&gt;Lo que descubrí enseguida es que el problema no era la herramienta de transcripción, sino yo. No sabía estructurar un pensamiento para verbalizar antes de verbalizar. Con el teclado me corrijo sobre la marcha sin darme cuenta. Con la voz, el error sale y ya está.&lt;/p&gt;
&lt;p&gt;Así que cambié el enfoque. En lugar de intentar improvisar, empecé a &lt;strong&gt;preparar mentalmente lo que iba a decir&lt;/strong&gt; antes de hablar. No guiones, solo un segundo de silencio para ordenar:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;¿Cuál es el contexto que el modelo necesita?&lt;/li&gt;
&lt;li&gt;¿Qué quiero que haga exactamente?&lt;/li&gt;
&lt;li&gt;¿Qué restricciones hay que mencionar?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Ese segundo de pausa fue el cambio más grande. Es exactamente lo que hago con el teclado de forma inconsciente, pero con la voz tengo que hacerlo de forma consciente y explícita.&lt;/p&gt;
&lt;p&gt;La otra cosa que aprendí es que &lt;strong&gt;la voz favorece la brevedad&lt;/strong&gt;. Cuando escribo, tiendo a añadir matices. Cuando hablo, voy al grano porque mantener una frase larga en la cabeza es agotador. Y resulta que &lt;span class="high"&gt;Claude&lt;/span&gt; responde igual de bien a prompts concisos que a prompts detallados, siempre que el contexto esté claro.&lt;/p&gt;
&lt;p&gt;Lo que terminé haciendo fue una especie de protocolo informal para las sesiones de voz:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Empezar por el &lt;strong&gt;qué&lt;/strong&gt; antes del &lt;strong&gt;cómo&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Una idea por turno, sin encadenar peticiones&lt;/li&gt;
&lt;li&gt;Si la respuesta no es lo que esperaba, reformular desde cero en lugar de corregir encima&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Con ese esquema, las sesiones empezaron a funcionar. No perfectas, pero funcionales.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Mi Resultado 🎯&lt;/h2&gt;
&lt;p&gt;Hay situaciones en las que la voz gana con claridad. Cuando estoy pensando en voz alta sobre una arquitectura, cuando quiero explorar varias opciones antes de escribir código, cuando necesito describir un comportamiento que es más fácil de narrar que de especificar. El teclado sigue siendo mi modo principal, pero la voz ya no es el último recurso.&lt;/p&gt;
&lt;p&gt;Lo que cambió fue que aprendí a estructurar mejor antes de hablar. Y eso, curiosamente, mejoró también cómo escribo los prompts. El hábito de la pausa, de ordenar antes de emitir, se transfirió.&lt;/p&gt;
&lt;p&gt;El accidente me forzó a entrenar una habilidad que nunca habría entrenado voluntariamente. No porque no supiera que existía, sino porque tenía una alternativa cómoda que usaba siempre. Cuando la alternativa desaparece, te sorprendes de lo que eres capaz de aprender.&lt;/p&gt;
&lt;p&gt;Si trabajas con modelos de lenguaje de forma habitual y nunca has probado la voz, no hace falta que te rompas nada para intentarlo. Una semana con restricción voluntaria al teclado es suficiente para ver si hay algo ahí que vale la pena. A mí me costó un accidente descubrirlo. Tú tienes ventaja.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Keep coding, keep running&lt;/strong&gt; 🏃‍♂️&lt;/p&gt;
&lt;hr /&gt;</content>
    <link rel="alternate" href="https://jcalderita.com/es/blog/claude-voice/"></link>
    <media:content url="https://jcalderita.com/static/blog/ClaudeVoice.webp" medium="image"></media:content>
  </entry>
  <entry>
    <id>https://jcalderita.com/es/blog/wwdc26/</id>
    <title>WWDC 26</title>
    <updated>2026-06-03T00:00:00Z</updated>
    <summary>WWDC no es una conferencia más para mí. Es el evento del año que disfruto más que cualquier estreno de cine o serie, como un niño en Navidad.</summary>
    <content type="html">&lt;hr /&gt;
&lt;h2&gt;Mi Problema 🤔&lt;/h2&gt;
&lt;p&gt;Cada junio escucho a la gente hablar del gran estreno de la temporada. La peli del verano, la nueva temporada de tal serie, la secuela que llevaban años esperando. Y yo asiento y pienso en lo mío: dentro de unos días llega el &lt;span class="high"&gt;WWDC&lt;/span&gt;.&lt;/p&gt;
&lt;p&gt;Mi problema es que es muy difícil explicar esa sensación a alguien fuera del entorno de desarrollo de Apple. Decir que llevas semanas con el calendario marcado, que te tomas el día del Keynote como una festividad, que se te disparan los nervios según se acerca la hora — dicho sin contexto, suena raro. Para quien no programa en &lt;span class="high"&gt;Swift&lt;/span&gt;, una keynote de dos horas hablando de APIs es lo más alejado que existe de un estreno de cine.&lt;/p&gt;
&lt;p&gt;No es frikismo sin más. El &lt;span class="high"&gt;WWDC&lt;/span&gt; tiene algo que no tiene ningún estreno: lo que anuncian afecta directamente a mi trabajo, a las decisiones que tomé hace seis meses y que ahora van a tener respuesta oficial. Cuando se apagan las luces de mi sala particular, no soy un espectador más — soy el público objetivo.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Mi Solución 🧩&lt;/h2&gt;
&lt;p&gt;Dejé de intentar justificarlo. El &lt;span class="high"&gt;WWDC&lt;/span&gt; es mi estreno del año, y lo disfruto más que cualquier película o serie. Me lo paso como un niño.&lt;/p&gt;
&lt;h3&gt;La semana previa&lt;/h3&gt;
&lt;p&gt;Empieza antes del lunes. Los rumores están en máximos, la comunidad está en modo especulación total, y yo llevo días pensando en qué van a anunciar y cómo va a afectar a lo que tengo entre manos. Es la misma energía que la semana antes de un estreno muy esperado, con una diferencia fundamental: el resultado me importa de verdad — como alguien a quien lo anunciado le cambia el trabajo.&lt;/p&gt;
&lt;h3&gt;El Keynote como ritual&lt;/h3&gt;
&lt;p&gt;En España, el Keynote me toca a las 19:00. La hora perfecta — cierro el día de trabajo, cena pronto, y a partir de ahí solo existe el Keynote.&lt;/p&gt;
&lt;p&gt;Mi ritual incluye un montaje de pantallas que parecería absurdo desde fuera. En la principal, el stream oficial de Apple. En una segunda, un grupo de locos haciendo comentarios en directo — comentan cada slide, predicen lo siguiente, reaccionan en tiempo real. Y en una tercera, voy siguiendo X, blogs técnicos y los Slack y Discord de desarrolladores: matices que se escapan en los slides y primeros análisis sobre el impacto real.&lt;/p&gt;
&lt;p&gt;Lo que más me gusta es que el Keynote mezcla dos cosas que raramente van juntas: la emoción del espectáculo y la información técnica que me cambia el trabajo. Cada feature la proceso en dos niveles a la vez — espectador que disfruta y desarrollador que ya piensa en cómo integrarlo.&lt;/p&gt;
&lt;p&gt;Y luego están los momentos de “¡sí!” — cuando anuncian algo que llevabas tiempo pidiendo. El año pasado, con las mejoras en &lt;span class="high"&gt;Swift Concurrency&lt;/span&gt;, me levanté del sofá. Literalmente.&lt;/p&gt;
&lt;h3&gt;Las sesiones son el verdadero contenido&lt;/h3&gt;
&lt;p&gt;El Keynote es el aperitivo. Las &lt;span class="high"&gt;sesiones técnicas&lt;/span&gt; son el plato fuerte, y duran semanas.&lt;/p&gt;
&lt;p&gt;Reviso el catálogo como quien revisa la cartelera de un festival de cine. Con más de cien sesiones, seleccionar y priorizar es ya una actividad en sí misma. Y no caducan: Apple las mantiene en &lt;a href="https://developer.apple.com/videos/"&gt;developer.apple.com&lt;/a&gt; indefinidamente. Junio es para las novedades, y el resto del año para profundizar en las que dejé pendientes.&lt;/p&gt;
&lt;p&gt;Esto conecta con &lt;a href="/es/blog/swift-pills/"&gt;Swift Pills&lt;/a&gt;: conocimiento denso que merece consumirse con calma. Y con &lt;a href="/es/blog/apple-coding-speed/"&gt;Apple Coding Speed&lt;/a&gt;: la velocidad de comprensión que entrenas es lo que te permite sacar partido de cada sesión.&lt;/p&gt;
&lt;h3&gt;Las betas como parque de juegos&lt;/h3&gt;
&lt;p&gt;El día del Keynote se abren las betas. A partir de ahí empieza el juego: instalar, compilar contra el nuevo SDK, ver qué se rompe, explorar las APIs antes de que tengan documentación completa. Es acceso anticipado, pero el juego soy yo desarrollando — y lo que descubro puede cambiar decisiones de diseño que llevo meses madurando.&lt;/p&gt;
&lt;h3&gt;La comunidad en modo WWDC&lt;/h3&gt;
&lt;p&gt;La conversación técnica de desarrolladores de Apple se dispara. Todo el mundo tiene algo que comentar, algo que ha descubierto, algún detalle que ha cambiado cómo veía un problema.&lt;/p&gt;
&lt;p&gt;Las conferencias del ecosistema — como las que sigo en &lt;a href="/es/blog/conferences-25/"&gt;Conferences 25&lt;/a&gt; — tienen esa energía, pero el &lt;span class="high"&gt;WWDC&lt;/span&gt; la lleva al extremo: es el momento del año en que toda la comunidad mira lo mismo al mismo tiempo.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Mi Resultado 🎯&lt;/h2&gt;
&lt;p&gt;Ya no intento explicarlo. Acepto que el &lt;span class="high"&gt;WWDC&lt;/span&gt; es mi evento del año y lo disfruto sin complejos.&lt;/p&gt;
&lt;p&gt;Lo que me da cada junio:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Anticipación genuina&lt;/strong&gt; — el evento que afecta directamente a mi trabajo&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;El Keynote como espectáculo&lt;/strong&gt; — dos horas en modo doble: espectador y desarrollador&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Sesiones técnicas que duran meses&lt;/strong&gt; — material denso que consumo con calma el resto del año&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Las betas como laboratorio&lt;/strong&gt; — acceso anticipado a las nuevas APIs antes que nadie&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;La comunidad en su pico&lt;/strong&gt; — la conversación técnica más rica del año&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Respuestas a problemas que tienes encima de la mesa&lt;/strong&gt; — satisfacción sin equivalente&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Si tienes un evento del año que te hace sentir como un niño, no lo justifiques. Disfrútalo. El mío es el &lt;span class="high"&gt;WWDC&lt;/span&gt;, y ya tengo ganas del próximo.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Keep coding, keep running&lt;/strong&gt; 🏃‍♂️&lt;/p&gt;
&lt;hr /&gt;</content>
    <link rel="alternate" href="https://jcalderita.com/es/blog/wwdc26/"></link>
    <media:content url="https://jcalderita.com/static/blog/WWDC26.webp" medium="image"></media:content>
  </entry>
  <entry>
    <id>https://jcalderita.com/es/blog/swift-pills/</id>
    <title>Swift Pills</title>
    <updated>2026-05-27T00:00:00Z</updated>
    <summary>Píldoras de cinco minutos sobre Swift que te enseñan cosas que ni siquiera habías considerado. Conocimiento que entra sin pedir hueco en la agenda.</summary>
    <content type="html">&lt;hr /&gt;
&lt;h2&gt;Mi Problema 🤔&lt;/h2&gt;
&lt;p&gt;Estar al día con &lt;span class="high"&gt;Swift&lt;/span&gt; es una de esas cosas que siempre tengo en la lista y que casi nunca consigo tachar. Cada WWDC trae cosas nuevas, el lenguaje evoluciona, la comunidad publica sin parar, y yo mientras tanto estoy con el proyecto, con las reuniones, con la vida. La información existe — el problema es encontrar el hueco para entrarle.&lt;/p&gt;
&lt;p&gt;Y es que casi todo el contenido técnico viene en formatos grandes: vídeos de cincuenta minutos, sesiones enteras de WWDC, artículos kilométricos, libros. Todo eso pide tiempo y cabeza, y en mi día a día no siempre tengo ninguna de las dos. Lo que acaba pasando es que voy guardando enlaces “para cuando tenga un rato” — y ese rato no llega.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Mi Solución 🧩&lt;/h2&gt;
&lt;p&gt;El blog de &lt;a href="https://blog.arturorivas.com/"&gt;Arturo Rivas&lt;/a&gt; cambia la forma de consumir contenido sobre &lt;span class="high"&gt;Swift&lt;/span&gt;. Lo que él publica son píldoras de conocimiento: artículos cortos, de unos cinco minutos de lectura, donde te cuenta una cosa concreta y se va. Sin rodeos, sin relleno, sin “antes de empezar permítanme contextualizar”.&lt;/p&gt;
&lt;p&gt;Y lo que más me ha enganchado es que la mayoría de las veces son píldoras sobre cosas que ni siquiera te habías parado a considerar. Detalles del lenguaje, comportamientos sutiles, APIs que estaban ahí desde hace versiones y que nunca te habías cruzado con ellas. Cosas que cuando las lees piensas “anda, esto no lo sabía” — y a partir de ahí ya no se te olvidan.&lt;/p&gt;
&lt;h3&gt;Cinco minutos cambian la ecuación&lt;/h3&gt;
&lt;p&gt;Que sean píldoras de cinco minutos no es un detalle menor — es lo que hace que funcione. No necesito reservar un hueco, no necesito ponerme en modo estudio, no necesito ni siquiera estar en el ordenador. Las leo entre tareas, mientras espera un build, con el café de la mañana. El aprendizaje deja de competir con el trabajo y se cuela en los márgenes del día.&lt;/p&gt;
&lt;p&gt;Y como cada píldora va a una sola cosa, la retención es brutal. Cuando un artículo es largo necesito procesarlo a fondo, sentarme a estudiarlo y darle varias vueltas para que se quede. Cuando leo una píldora sobre, por ejemplo, cómo funciona &lt;span class="high"&gt;@discardableResult&lt;/span&gt; o un detalle de &lt;span class="high"&gt;Sendable&lt;/span&gt; que no había visto, ese concepto entra directamente. Era el único foco durante esos cinco minutos, y mi cabeza lo trata como tal.&lt;/p&gt;
&lt;h3&gt;Cosas que ni habías considerado&lt;/h3&gt;
&lt;p&gt;Esto para mí es lo más valioso del blog. Arturo no escribe sobre “qué es Swift” ni sobre cosas que ya están en cualquier tutorial. Escribe sobre rincones del lenguaje, sobre comportamientos que tienen su porqué pero que normalmente no te paras a investigar, sobre APIs que se quedan tapadas por las grandes novedades de cada año.&lt;/p&gt;
&lt;p&gt;Es ese tipo de conocimiento que no buscas activamente porque ni sabes que existe — pero que cuando lo tienes, te cambia la forma de leer y escribir código. Te das cuenta de que hay un patrón mejor, una herramienta que llevaba tiempo disponible, un detalle que explica por qué algo se comporta como se comporta.&lt;/p&gt;
&lt;h3&gt;Encaja en mi flujo sin pedir permiso&lt;/h3&gt;
&lt;p&gt;Empecé a tratar las píldoras como parte natural del día, no como tarea aparte. Cinco minutos antes de entrar en faena, otros cinco entre dos reuniones, mientras Xcode compila algo pesado. Sin la sensación de “ahora tengo que aprender” — simplemente dejando que entren cuando hay hueco.&lt;/p&gt;
&lt;p&gt;El contraste con los formatos largos es brutal. Para un vídeo de WWDC necesito planificarlo, sentarme, tener atención plena. Para una píldora, simplemente la abro cuando aparece la ocasión. Y resulta que las ocasiones son muchas más de las que pensaba.&lt;/p&gt;
&lt;p&gt;Esto conecta con algo que ya conté en &lt;a href="/es/blog/apple-coding-speed/"&gt;Apple Coding Speed&lt;/a&gt;: la velocidad de comprensión no se entrena solo en formaciones largas. También se entrena en la exposición constante a conceptos bien explicados — y las píldoras son justo eso, entrenamiento continuo que sucede entre sesiones más estructuradas.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Mi Resultado 🎯&lt;/h2&gt;
&lt;p&gt;Si trabajas con &lt;span class="high"&gt;Swift&lt;/span&gt; y se te están acumulando los enlaces “para cuando tenga un rato”, el problema probablemente no es la cantidad de contenido — es el formato. Pásate por &lt;a href="https://blog.arturorivas.com/"&gt;blog.arturorivas.com&lt;/a&gt; y prueba el modelo de las píldoras durante una semana.&lt;/p&gt;
&lt;p&gt;Lo que yo he sacado en limpio:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Aprendo cosas que ni buscaba&lt;/strong&gt; — la mayoría de las píldoras tocan detalles que no habría buscado por mi cuenta, y son las que más me han aportado&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;El conocimiento entra sin esfuerzo&lt;/strong&gt; — cinco minutos caben en cualquier hueco, así que el aprendizaje deja de depender de planificación&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Se me queda lo que leo&lt;/strong&gt; — un solo concepto por píldora hace que la retención sea mucho más alta que con artículos largos&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Reconozco patrones más rápido&lt;/strong&gt; — cuando me cruzo con una API o un patrón que vi en una píldora, lo identifico sin tener que buscarlo&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Se acabó la deuda de “tengo que leer esto”&lt;/strong&gt; — el contenido de bajo umbral se consume en el momento, no se aplaza&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Si quieres medicina de la nueva, la tienes en píldoras de conocimiento aquí. Cinco minutos, una idea, una cosa que probablemente no habías considerado. Difícilmente vas a encontrar mejor relación entre tiempo invertido y aprendizaje que se queda.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Keep coding, keep running&lt;/strong&gt; 🏃‍♂️&lt;/p&gt;
&lt;hr /&gt;</content>
    <link href="https://jcalderita.com/es/blog/swift-pills/" rel="alternate"></link>
    <media:content url="https://jcalderita.com/static/blog/SwiftPills.webp" medium="image"></media:content>
  </entry>
  <entry>
    <id>https://jcalderita.com/es/blog/apple-coding-speed/</id>
    <title>Apple Coding Speed</title>
    <updated>2026-05-20T00:00:00Z</updated>
    <summary>Qué entrenan las formaciones de Apple Coding Academy: no solo código Swift, sino velocidad de comprensión, análisis y desarrollo en el ecosistema Apple.</summary>
    <content type="html">&lt;hr /&gt;
&lt;h2&gt;Mi Problema 🤔&lt;/h2&gt;
&lt;p&gt;Cuando decidí especializarme en el ecosistema de Apple, buscaba una formación cercana con un perfil muy concreto: rigurosa, profesional, estructurada, y con una comunidad técnica activa detrás. No otra introducción a &lt;span class="high"&gt;SwiftUI&lt;/span&gt;, sino el ecosistema completo — &lt;span class="high"&gt;Swift Concurrency&lt;/span&gt;, &lt;span class="high"&gt;server-side&lt;/span&gt;, &lt;span class="high"&gt;accesibilidad&lt;/span&gt;, &lt;span class="high"&gt;agentes con IA&lt;/span&gt; — al nivel de la documentación oficial.&lt;/p&gt;
&lt;p&gt;Esa búsqueda me llevó a &lt;a href="https://acoding.academy/"&gt;Apple Coding Academy&lt;/a&gt;, un referente en formación especializada en desarrollo nativo con tecnologías de Apple. Y, una vez dentro, apareció una pregunta más interesante que la que me había llevado allí: ¿qué me da una formación estructurada de Apple Coding Academy que no me daría leer la documentación por mi cuenta?&lt;/p&gt;
&lt;p&gt;La respuesta tardó más de lo esperado en llegar, y cuando llegó no fue lo que anticipaba.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Mi Solución 🧩&lt;/h2&gt;
&lt;p&gt;Lo que me dieron las formaciones de Apple Coding Academy no fue el conocimiento. Fue la &lt;strong&gt;velocidad&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;No velocidad de ejecución — eso es un subproducto. Velocidad de comprensión. Velocidad de análisis. Velocidad para llegar al núcleo de un problema antes de que se expanda. Y esa velocidad, una vez que la tienes, se transfiere a todo.&lt;/p&gt;
&lt;h3&gt;Lo que realmente entrenas en Apple Coding Academy&lt;/h3&gt;
&lt;p&gt;Cuando entras en un programa estructurado de &lt;a href="https://acoding.academy/"&gt;Apple Coding Academy&lt;/a&gt; — ya sea el &lt;span class="high"&gt;Swift Developer Program&lt;/span&gt;, el &lt;span class="high"&gt;Swift Agentic Engineering Program&lt;/span&gt;, o cualquiera de sus formaciones — lo primero que notas es el ritmo. Es alto. Los conceptos se acumulan rápido porque hay una curva diseñada, un orden que maximiza la retención.&lt;/p&gt;
&lt;p&gt;El resultado es que después de varias formaciones, cuando encuentro una API nueva en &lt;span class="high"&gt;Swift&lt;/span&gt;, no la proceso igual que antes. El patrón de reconocimiento es más rápido. Veo la firma del método, identifico los protocolos que implementa, infiero el comportamiento antes de leer la documentación. No es intuición — es velocidad entrenada.&lt;/p&gt;
&lt;h3&gt;Velocidad de comprensión&lt;/h3&gt;
&lt;p&gt;Lo primero que cambia es cómo lees código. Antes de las formaciones, leer un archivo de &lt;span class="high"&gt;Swift&lt;/span&gt; desconocido era un proceso secuencial: línea a línea, buscando referencias, construyendo el modelo mental despacio.&lt;/p&gt;
&lt;p&gt;Después de suficientes horas de formación, la lectura se aproxima al escaneo. Identificas los patrones — &lt;span class="high"&gt;async/await&lt;/span&gt;, composición de protocolos, la estructura de un &lt;span class="high"&gt;actor&lt;/span&gt; — antes de procesar el detalle. El tiempo de comprensión se desploma.&lt;/p&gt;
&lt;p&gt;Eso importa más de lo que parece. Ahora, con el uso de la IA, paso más tiempo leyendo código que escribiéndolo. Si la velocidad de lectura y entendimiento sube, el tiempo de entrega baja sin que la calidad cambie.&lt;/p&gt;
&lt;h3&gt;Velocidad de análisis&lt;/h3&gt;
&lt;p&gt;Lo segundo que cambia es cómo analizo problemas. Las formaciones de Apple Coding Academy tienen una característica que al principio me resultó incómoda: los problemas no vienen bien definidos. Tienes un objetivo, un conjunto de herramientas, y la expectativa de que seas tú quien trace el camino.&lt;/p&gt;
&lt;p&gt;Eso entrena algo específico: separar lo que importa de lo que no importa antes de empezar a trabajar. Identificar el punto de fricción real, no el síntoma superficial.&lt;/p&gt;
&lt;p&gt;En mi día a día, ese entrenamiento se traduce en que cuando aparece un bug o una decisión de arquitectura, no salto directamente a resolver lo visible. Primero analizo. El análisis es más rápido porque tengo más contexto entrenado. La solución llega antes y con menos iteraciones.&lt;/p&gt;
&lt;h3&gt;Velocidad de desarrollo&lt;/h3&gt;
&lt;p&gt;Lo tercero que cambia es la ejecución. Cuando comprensión y análisis son más rápidos, el desarrollo lo es también — no porque escriba más rápido, sino porque, con el uso de la IA, escribo menos código a mano y dedico ese tiempo a revisar y dirigir lo que el modelo propone. Menos código de exploración, menos prototipos descartados, menos refactorizaciones de cosas que nunca deberían haberse escrito así.&lt;/p&gt;
&lt;p&gt;Las formaciones de Apple Coding Academy llevan esto en su ADN: la filosofía de diseño de Apple premia la claridad y la economía. Menos superficie para el mismo comportamiento. Eso lo asimilas aunque nadie te lo explique de forma explícita — lo absorbes del ecosistema.&lt;/p&gt;
&lt;h3&gt;Las formaciones que he hecho en Apple Coding Academy&lt;/h3&gt;
&lt;p&gt;He recorrido varias etapas dentro del catálogo de &lt;a href="https://acoding.academy/"&gt;Apple Coding Academy&lt;/a&gt;. El &lt;span class="high"&gt;Bootcamp&lt;/span&gt; me dio los fundamentos del ecosistema: tipos, gestión de memoria, &lt;span class="high"&gt;Swift Concurrency&lt;/span&gt;, composición de protocolos, integración con frameworks nativos. Cinco meses de inmersión que cubren el camino completo hasta un perfil profesional de desarrollo Apple.&lt;/p&gt;
&lt;p&gt;Después llegaron formaciones más específicas: módulos de &lt;span class="high"&gt;server-side&lt;/span&gt; con &lt;span class="high"&gt;Swift&lt;/span&gt;, de &lt;span class="high"&gt;accesibilidad&lt;/span&gt;, y del rediseño visual con &lt;span class="high"&gt;Liquid Glass&lt;/span&gt;. Cada módulo apunta a una capa distinta del stack y exige un nivel de profundidad que rara vez alcanzo solo leyendo.&lt;/p&gt;
&lt;p&gt;Más recientemente, el &lt;span class="high"&gt;Swift Agentic Engineering Program&lt;/span&gt; — sesenta horas centradas en desarrollo de &lt;span class="high"&gt;agentes con IA&lt;/span&gt;, &lt;span class="high"&gt;MCP&lt;/span&gt;, &lt;span class="high"&gt;Spec Driven Development&lt;/span&gt; y &lt;span class="high"&gt;Foundation Model Framework&lt;/span&gt;. Una formación que no existe en la documentación oficial porque combina piezas que aún se están definiendo en el ecosistema.&lt;/p&gt;
&lt;p&gt;Cada capa añadió algo diferente. Los fundamentos me dieron el vocabulario. Las formaciones de servidor me dieron la perspectiva de qué pasa cuando el código está en producción bajo carga real. Las de diseño de sistema me enseñaron a pensar en seams — los puntos donde el comportamiento puede variar sin que lo que está detrás cambie. Las de agentes me dieron el marco para integrar IA en flujos de desarrollo de forma rigurosa.&lt;/p&gt;
&lt;p&gt;Lo que tienen en común es que todas me forzaron a salir de mi zona de confort a una velocidad que yo solo no habría mantenido. Eso es lo que no consigo con la documentación autodidacta: el ritmo externo.&lt;/p&gt;
&lt;h3&gt;El efecto compuesto&lt;/h3&gt;
&lt;p&gt;Lo interesante de esta velocidad es que se compone. No sumas horas de formación de forma lineal — las capas se potencian entre sí.&lt;/p&gt;
&lt;p&gt;La comprensión rápida acelera el análisis porque procesas más contexto en menos tiempo. El análisis rápido acelera el desarrollo porque llegas al problema real antes. Y el desarrollo más limpio reduce el tiempo de mantenimiento futuro.&lt;/p&gt;
&lt;p&gt;El retorno no está en los primeros meses. Está en el año tres, cuando miras atrás y constatas que el nivel de decisiones que tomas en cinco minutos antes te llevaba una tarde.&lt;/p&gt;
&lt;p&gt;Esto conecta directamente con lo que exploro en &lt;a href="/es/blog/claude-code/"&gt;Claude Code&lt;/a&gt;: la IA amplifica lo que ya sabes. Si tu comprensión del ecosistema es profunda, las sugerencias del modelo son más útiles porque sabes evaluarlas. Si es superficial, el modelo te entrega código que compila pero no entiendes. La formación hace que la IA sea mejor herramienta, no al revés. Esa es justo la apuesta del &lt;span class="high"&gt;Swift Agentic Engineering Program&lt;/span&gt; de &lt;a href="https://acoding.academy/"&gt;Apple Coding Academy&lt;/a&gt;: entrenar el criterio antes de delegar en el modelo.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Mi Resultado 🎯&lt;/h2&gt;
&lt;p&gt;La respuesta a la pregunta que me hice — qué me da una formación de &lt;a href="https://acoding.academy/"&gt;Apple Coding Academy&lt;/a&gt; que no me daría la documentación sola — es esta: el ritmo. La presión diseñada. El entorno que fuerza la conexión rápida de piezas.&lt;/p&gt;
&lt;p&gt;La documentación te da el qué. La formación te entrena el cómo y, sobre todo, la velocidad con la que llegas al por qué.&lt;/p&gt;
&lt;p&gt;Lo que noto en mi trabajo ahora:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Diagnóstico más rápido&lt;/strong&gt; — identifico el problema real antes de tocar código&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Menos iteraciones&lt;/strong&gt; — las decisiones de diseño llegan antes de escribir la primera línea&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Lectura de código más eficiente&lt;/strong&gt; — proceso archivos desconocidos sin recorrer línea a línea&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Criterio más sólido&lt;/strong&gt; — sé cuándo un patrón es correcto para el contexto y cuándo es solo familiar&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Mejor uso de la IA&lt;/strong&gt; — evalúo las sugerencias del modelo porque entiendo el ecosistema con profundidad&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Keep coding, keep running&lt;/strong&gt; 🏃‍♂️&lt;/p&gt;
&lt;hr /&gt;</content>
    <link href="https://jcalderita.com/es/blog/apple-coding-speed/" rel="alternate"></link>
    <media:content url="https://jcalderita.com/static/blog/AppleCodingSpeed.webp" medium="image"></media:content>
  </entry>
  <entry>
    <id>https://jcalderita.com/es/blog/save-tokens/</id>
    <title>Save Tokens</title>
    <updated>2026-05-13T00:00:00Z</updated>
    <summary>Cómo reducir tokens en Claude Code moviendo la lógica bash de tus skills a scripts reutilizables que la IA invoca con una sola línea.</summary>
    <content type="html">&lt;hr /&gt;
&lt;h2&gt;Mi Problema 🤔&lt;/h2&gt;
&lt;p&gt;Trabajo con &lt;span class="high"&gt;Claude Code&lt;/span&gt; a diario y empecé a notar un patrón que me llamó la atención: cada vez que una skill o una conversación necesitaba ejecutar algo en el shell, la IA gastaba tokens antes de ejecutar nada. Razonaba sobre qué comando construir, lo redactaba, lo lanzaba, leía el output y luego lo interpretaba para seguir trabajando.&lt;/p&gt;
&lt;p&gt;En una operación aislada, ese coste se nota poco. El problema real aparece en cuanto hay &lt;strong&gt;repetición&lt;/strong&gt;. Una skill que recorre varios archivos, una conversación que reorganiza directorios, un flujo que necesita hacer la misma operación con argumentos distintos — todo eso multiplica el coste del razonamiento por cada iteración. Cada vuelta del bucle vuelve a construir un comando que, en el fondo, es idéntico al de la vuelta anterior salvo por un parámetro.&lt;/p&gt;
&lt;p&gt;Y aquí la clave: ese razonamiento es &lt;strong&gt;trabajo determinista&lt;/strong&gt; disfrazado de razonamiento. No estoy pidiéndole a la IA que decida nada — solo que vuelva a escribir un comando que ya escribió antes. Es exactamente el tipo de tarea que un script resuelve mejor que un modelo.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Mi Solución 🧩&lt;/h2&gt;
&lt;p&gt;La idea es sencilla: si una skill o un flujo necesita ejecutar lógica de shell, esa lógica no debería vivir en la cabeza de la IA — debería vivir en un &lt;span class="high"&gt;.sh&lt;/span&gt; que la IA invoca con una sola línea.&lt;/p&gt;
&lt;h3&gt;El cambio mental&lt;/h3&gt;
&lt;p&gt;El instinto natural cuando configuras una skill es describir paso a paso lo que la IA tiene que hacer:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Lista los archivos del directorio, filtra los que terminan en &lt;span class="high"&gt;.json&lt;/span&gt;, lee cada uno, extrae el campo &lt;span class="high"&gt;version&lt;/span&gt;, encuentra el mayor, y devuélvelo.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Esa descripción obliga a la IA a construir cada comando, ejecutarlo, interpretar la salida y encadenar el siguiente paso. En su lugar, la instrucción debería ser:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Ejecuta la script &lt;span class="high"&gt;latest-version&lt;/span&gt; /ruta.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;La diferencia es que todo el razonamiento sobre &lt;strong&gt;cómo&lt;/strong&gt; se hace la operación deja de ocurrir en cada invocación. Ocurre una sola vez, cuando escribes el script. A partir de ahí, la IA solo necesita saber &lt;strong&gt;qué&lt;/strong&gt; ejecutar y &lt;strong&gt;qué formato&lt;/strong&gt; recibir.&lt;/p&gt;
&lt;h3&gt;Por qué los bucles son el caso crítico&lt;/h3&gt;
&lt;p&gt;El ahorro se nota incluso en operaciones puntuales, pero se vuelve &lt;strong&gt;enorme&lt;/strong&gt; cuando la skill itera. Sin script, cada vuelta del bucle paga el coste de razonar y construir el comando. Con script, la IA lanza una sola invocación y recibe el agregado completo.&lt;/p&gt;
&lt;p&gt;La regla práctica que aplico: si la operación se puede agregar en un único paso de shell, debería hacerlo el script — no la IA iterando externamente.&lt;/p&gt;
&lt;h3&gt;Estructura que uso en mis skills&lt;/h3&gt;
&lt;p&gt;Cada skill mía sigue la misma forma: un &lt;span class="high"&gt;SKILL.md&lt;/span&gt; corto que describe el contrato, y una carpeta &lt;span class="high"&gt;scripts/&lt;/span&gt; con la lógica empaquetada.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;~/.claude/skills/
└── mi-skill/
    ├── SKILL.md          ← instrucciones para la IA (qué ejecutar, qué esperar)
    └── scripts/
        ├── operacion-a.zsh
        └── operacion-b.zsh
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Cada vez que la skill necesita una capacidad nueva, añado un script en lugar de añadir instrucciones bash inline. La skill gana funcionalidad sin engordar el contexto que la IA tiene que procesar en cada invocación.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Uso &lt;span class="high"&gt;.zsh&lt;/span&gt; porque trabajo en macOS, donde es el shell por defecto desde Catalina y puedo aprovechar features que bash no tiene. El patrón funciona exactamente igual con &lt;span class="high"&gt;.sh&lt;/span&gt; y bash. Elige la que case con tu entorno.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Y un detalle importante: los scripts los escribo con ayuda de la propia IA, una sola vez. Ese razonamiento — el de diseñar el comando correcto, manejar los edge cases, devolver el JSON adecuado — ocurre durante la creación del script. Después, ese trabajo queda &lt;strong&gt;cristalizado&lt;/strong&gt; en el archivo y no se vuelve a pagar.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Mi Resultado 🎯&lt;/h2&gt;
&lt;p&gt;El cambio es directamente observable en cuanto separas la lógica determinista del razonamiento. La IA pasa de construir comandos a consumirlos. De interpretar salidas variables a leer JSON estructurado. De iterar externamente a recibir el agregado completo en una llamada.&lt;/p&gt;
&lt;p&gt;Los beneficios concretos que estoy viendo:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Menos tokens por invocación&lt;/strong&gt; — el razonamiento sobre cómo hacer la operación ocurre una sola vez, al escribir el script, no cada vez que la skill se ejecuta&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Consistencia&lt;/strong&gt; — el script siempre devuelve el mismo formato; la IA no interpreta salidas variables en cada ejecución&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Mantenibilidad&lt;/strong&gt; — cuando cambia la lógica, edito el script en un sitio; el &lt;span class="high"&gt;SKILL.md&lt;/span&gt; no se toca&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Escalabilidad en bucles&lt;/strong&gt; — el script agrega antes de devolver; una llamada sustituye a N iteraciones&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Skills más simples&lt;/strong&gt; — el &lt;span class="high"&gt;SKILL.md&lt;/span&gt; describe &lt;strong&gt;qué&lt;/strong&gt; hacer, no &lt;strong&gt;cómo&lt;/strong&gt;; la complejidad operacional vive en los scripts&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;El patrón se aplica a cualquier skill o flujo que tenga que operar sobre el sistema de archivos, procesar texto estructurado o ejecutar lógica repetitiva. Si te encuentras describiendo en el &lt;span class="high"&gt;SKILL.md&lt;/span&gt; una secuencia de comandos bash que la IA debería construir y ejecutar, esa secuencia probablemente debería ser un script.&lt;/p&gt;
&lt;p&gt;Como exploré en el artículo sobre &lt;a href="/es/blog/claude-code/"&gt;Claude Code&lt;/a&gt;, la clave no es darle más capacidad de razonamiento a la IA para que resuelva más cosas — es estructurar el trabajo para que &lt;strong&gt;solo razone donde aporta valor&lt;/strong&gt;. Lo determinista lo resuelve un script. Lo creativo y contextual lo resuelve la IA. Cada uno en su sitio.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Keep coding, keep running&lt;/strong&gt; 🏃‍♂️&lt;/p&gt;
&lt;hr /&gt;</content>
    <link rel="alternate" href="https://jcalderita.com/es/blog/save-tokens/"></link>
    <media:content medium="image" url="https://jcalderita.com/static/blog/SaveTokens.webp"></media:content>
  </entry>
  <entry>
    <id>https://jcalderita.com/es/blog/uuid-v7/</id>
    <title>UUID v7</title>
    <updated>2026-05-06T00:00:00Z</updated>
    <summary>Swift usa UUID v4 por defecto, pero con una pequeña extensión puedes aprovechar todas las ventajas de UUID v7 sin sacrificar rendimiento.</summary>
    <content type="html">&lt;hr /&gt;
&lt;h2&gt;Mi Problema 🤔&lt;/h2&gt;
&lt;p&gt;En mi proyecto uso &lt;span class="high"&gt;PostgreSQL&lt;/span&gt; como base de datos y &lt;span class="high"&gt;UUID&lt;/span&gt; como identificador en todos mis modelos. Es el estándar habitual: universal, sin colisiones, seguro. Pero me encontré con un problema cuando mis tablas empezaron a crecer.&lt;/p&gt;
&lt;p&gt;&lt;span class="high"&gt;UUID v4&lt;/span&gt; es completamente aleatorio. Esto significa que cada nuevo registro se inserta en una posición aleatoria dentro del índice &lt;span class="high"&gt;B-Tree&lt;/span&gt;. El índice se fragmenta, las páginas se dividen, y el rendimiento de escritura degrada progresivamente. En tablas con millones de registros, el impacto es real.&lt;/p&gt;
&lt;p&gt;La solución existe desde hace tiempo en bases de datos como &lt;span class="high"&gt;MongoDB&lt;/span&gt; con &lt;span class="high"&gt;ObjectID&lt;/span&gt; o en sistemas que usan &lt;span class="high"&gt;ULID&lt;/span&gt;: embeber el timestamp en el identificador para que los registros nuevos se inserten siempre al final del índice.&lt;/p&gt;
&lt;p&gt;&lt;span class="high"&gt;UUID v7&lt;/span&gt;, definido en el &lt;span class="high"&gt;RFC 9562&lt;/span&gt;, trae exactamente eso al estándar UUID. &lt;span class="high"&gt;PostgreSQL 18&lt;/span&gt; ya incluye la función nativa &lt;span class="high"&gt;uuidv7()&lt;/span&gt;. Pero Swift — y por extensión &lt;span class="high"&gt;Fluent&lt;/span&gt; — sigue generando &lt;span class="high"&gt;UUID v4&lt;/span&gt; por defecto con &lt;span class="high"&gt;UUID()&lt;/span&gt;.&lt;/p&gt;
&lt;p&gt;Si el identificador lo genera mi aplicación antes de persistirlo, estoy perdiendo las ventajas de v7 aunque mi base de datos ya lo soporte.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Mi Solución 🧩&lt;/h2&gt;
&lt;p&gt;Decidí crear una extensión sobre &lt;span class="high"&gt;UUID&lt;/span&gt; que implementa la generación de &lt;span class="high"&gt;UUID v7&lt;/span&gt; directamente en Swift, siguiendo la especificación del &lt;span class="high"&gt;RFC 9562&lt;/span&gt;.&lt;/p&gt;
&lt;h3&gt;Estado monotónico&lt;/h3&gt;
&lt;p&gt;Lo primero que necesito es un estado compartido que garantice que dos UUIDs generados en el mismo milisegundo sean distintos y estén ordenados. Uso un &lt;span class="high"&gt;Mutex&lt;/span&gt; del framework &lt;span class="high"&gt;Synchronization&lt;/span&gt; para hacerlo thread-safe sin recurrir a &lt;span class="high"&gt;DispatchQueue&lt;/span&gt; ni &lt;span class="high"&gt;actor&lt;/span&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-swift"&gt;import Foundation
import Synchronization

private let v7State = Mutex(V7State())

private struct V7State: Sendable {
    var lastTimestamp: UInt64 = 0
    var sequence: UInt16 = 0
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;La generación del UUID&lt;/h3&gt;
&lt;p&gt;La extensión principal implementa el método estático &lt;span class="high"&gt;v7()&lt;/span&gt; que genera un UUID conforme al &lt;span class="high"&gt;RFC 9562&lt;/span&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-swift"&gt;extension UUID {
    public static func v7() -&amp;gt; UUID {
        var bytes = (
            UInt8(0), UInt8(0), UInt8(0), UInt8(0),
            UInt8(0), UInt8(0), UInt8(0), UInt8(0),
            UInt8(0), UInt8(0), UInt8(0), UInt8(0),
            UInt8(0), UInt8(0), UInt8(0), UInt8(0)
        )

        let (timestamp, seq) = v7State.withLock { state -&amp;gt; (UInt64, UInt16) in
            var now = UInt64(Date().timeIntervalSince1970 * 1000)

            if now == state.lastTimestamp {
                if state.sequence &amp;gt;= 0x0FFF {
                    while now == state.lastTimestamp {
                        now = UInt64(Date().timeIntervalSince1970 * 1000)
                    }
                    state.lastTimestamp = now
                    state.sequence = UInt16.random(in: 0...0x0FFF)
                } else {
                    state.sequence += 1
                }
            } else {
                state.lastTimestamp = now
                state.sequence = UInt16.random(in: 0...0x0FFF)
            }

            return (now, state.sequence)
        }

        // Bytes 0–5: 48-bit timestamp (big-endian)
        bytes.0 = UInt8(truncatingIfNeeded: timestamp &amp;gt;&amp;gt; 40)
        bytes.1 = UInt8(truncatingIfNeeded: timestamp &amp;gt;&amp;gt; 32)
        bytes.2 = UInt8(truncatingIfNeeded: timestamp &amp;gt;&amp;gt; 24)
        bytes.3 = UInt8(truncatingIfNeeded: timestamp &amp;gt;&amp;gt; 16)
        bytes.4 = UInt8(truncatingIfNeeded: timestamp &amp;gt;&amp;gt; 8)
        bytes.5 = UInt8(truncatingIfNeeded: timestamp)

        // Byte 6: version (0x7_) | top 4 bits of sequence
        bytes.6 = 0x70 | UInt8(seq &amp;gt;&amp;gt; 8)

        // Byte 7: lower 8 bits of sequence
        bytes.7 = UInt8(truncatingIfNeeded: seq)

        // Bytes 8–15: random, then set variant
        var random: UInt64 = 0
        withUnsafeMutableBytes(of: &amp;amp;random) { buf in
            _ = SecRandomCopyBytes(kSecRandomDefault, buf.count, buf.baseAddress!)
        }
        bytes.8  = UInt8(truncatingIfNeeded: random &amp;gt;&amp;gt; 56)
        bytes.9  = UInt8(truncatingIfNeeded: random &amp;gt;&amp;gt; 48)
        bytes.10 = UInt8(truncatingIfNeeded: random &amp;gt;&amp;gt; 40)
        bytes.11 = UInt8(truncatingIfNeeded: random &amp;gt;&amp;gt; 32)
        bytes.12 = UInt8(truncatingIfNeeded: random &amp;gt;&amp;gt; 24)
        bytes.13 = UInt8(truncatingIfNeeded: random &amp;gt;&amp;gt; 16)
        bytes.14 = UInt8(truncatingIfNeeded: random &amp;gt;&amp;gt; 8)
        bytes.15 = UInt8(truncatingIfNeeded: random)

        // Byte 8: variant 0b10xx_xxxx
        bytes.8 = (bytes.8 &amp;amp; 0x3F) | 0x80

        return UUID(uuid: bytes)
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;La estructura del UUID generado respeta la especificación del &lt;span class="high"&gt;RFC 9562&lt;/span&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Bytes 0–5&lt;/strong&gt; (48 bits): &lt;span class="high"&gt;timestamp&lt;/span&gt; en milisegundos desde epoch, en big-endian. Esto es lo que garantiza el orden cronológico.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Byte 6&lt;/strong&gt; (nibble alto): versión &lt;span class="high"&gt;0x7&lt;/span&gt; — identifica el UUID como v7.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Bytes 6–7&lt;/strong&gt; (12 bits inferiores): contador de secuencia. Garantiza monotonicidad dentro del mismo milisegundo.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Bytes 8–15&lt;/strong&gt; (64 bits): aleatorios generados con &lt;span class="high"&gt;SecRandomCopyBytes&lt;/span&gt;, con los dos bits superiores del byte 8 forzados a &lt;span class="high"&gt;0b10&lt;/span&gt; para marcar el variante &lt;span class="high"&gt;RFC 4122&lt;/span&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;La lógica monotónica dentro del &lt;span class="high"&gt;withLock&lt;/span&gt; funciona así: cuando dos llamadas ocurren en el mismo milisegundo, el contador se incrementa. Si el contador alcanza el máximo (&lt;span class="high"&gt;0x0FFF&lt;/span&gt;, es decir 4095 UUIDs en un solo milisegundo), la implementación espera activamente al siguiente milisegundo antes de continuar — nunca genera dos UUIDs idénticos.&lt;/p&gt;
&lt;h3&gt;Utilidades de inspección&lt;/h3&gt;
&lt;p&gt;Además de la generación, añadí dos propiedades para poder inspeccionar cualquier UUID: una que verifica si es v7 y otra que extrae el &lt;span class="high"&gt;timestamp&lt;/span&gt; embebido:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-swift"&gt;extension UUID {
    public var isV7: Bool {
        (uuid.6 &amp;gt;&amp;gt; 4) == 0x07 &amp;amp;&amp;amp; (uuid.8 &amp;gt;&amp;gt; 6) == 0x02
    }

    public var v7Timestamp: Date? {
        guard isV7 else { return nil }

        let ms = UInt64(uuid.0) &amp;lt;&amp;lt; 40
            | UInt64(uuid.1) &amp;lt;&amp;lt; 32
            | UInt64(uuid.2) &amp;lt;&amp;lt; 24
            | UInt64(uuid.3) &amp;lt;&amp;lt; 16
            | UInt64(uuid.4) &amp;lt;&amp;lt; 8
            | UInt64(uuid.5)

        return Date(timeIntervalSince1970: Double(ms) / 1000.0)
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;span class="high"&gt;isV7&lt;/span&gt; comprueba que el nibble alto del byte 6 sea &lt;span class="high"&gt;0x07&lt;/span&gt; (versión 7) y que los dos bits superiores del byte 8 sean &lt;span class="high"&gt;0b10&lt;/span&gt; (variante RFC 4122). &lt;span class="high"&gt;v7Timestamp&lt;/span&gt; reconstruye los 48 bits del timestamp desde los bytes 0–5 y los convierte en un &lt;span class="high"&gt;Date&lt;/span&gt;.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Mi Resultado 🎯&lt;/h2&gt;
&lt;p&gt;La adopción es inmediata. En cualquier modelo &lt;span class="high"&gt;Fluent&lt;/span&gt;, me basta con sustituir &lt;span class="high"&gt;UUID()&lt;/span&gt; por &lt;span class="high"&gt;UUID.v7()&lt;/span&gt; en el inicializador:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-swift"&gt;// Antes
self.id = UUID()

// Después
self.id = UUID.v7()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;El tipo en base de datos no cambia — sigue siendo &lt;span class="high"&gt;UUID&lt;/span&gt;. &lt;span class="high"&gt;Fluent&lt;/span&gt; no necesita ninguna modificación. El cambio es completamente transparente para el ORM.&lt;/p&gt;
&lt;p&gt;Los beneficios que he obtenido son directos:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Índices ordenados&lt;/strong&gt; — los nuevos registros se insertan siempre al final del &lt;span class="high"&gt;B-Tree&lt;/span&gt;, eliminando la fragmentación&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Ordenación cronológica nativa&lt;/strong&gt; — &lt;span class="high"&gt;ORDER BY id&lt;/span&gt; equivale a &lt;span class="high"&gt;ORDER BY created_at&lt;/span&gt; sin columna extra&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Timestamp embebido&lt;/strong&gt; — puedo extraer cuándo se creó un registro directamente del UUID con &lt;span class="high"&gt;v7Timestamp&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Compatible hacia atrás&lt;/strong&gt; — el formato sigue siendo UUID estándar; cualquier sistema que acepte &lt;span class="high"&gt;UUID v4&lt;/span&gt; acepta &lt;span class="high"&gt;v7&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Thread-safe&lt;/strong&gt; — funciona correctamente desde múltiples hilos concurrentes gracias al &lt;span class="high"&gt;Mutex&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class="language-swift"&gt;let id = UUID.v7()

print(id.isV7)          // true
print(id.v7Timestamp!)  // la fecha de creación embebida en el UUID
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Y si ya estás en &lt;span class="high"&gt;PostgreSQL 18&lt;/span&gt;, la función nativa &lt;span class="high"&gt;uuidv7()&lt;/span&gt; genera UUIDs con la misma estructura. Los UUIDs generados desde Swift y desde PostgreSQL son intercambiables y comparables entre sí.&lt;/p&gt;
&lt;p&gt;El cambio de &lt;span class="high"&gt;UUID()&lt;/span&gt; a &lt;span class="high"&gt;UUID.v7()&lt;/span&gt; es de una línea. Las ventajas, permanentes.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Keep coding, keep running&lt;/strong&gt; 🏃‍♂️&lt;/p&gt;
&lt;hr /&gt;</content>
    <link href="https://jcalderita.com/es/blog/uuid-v7/" rel="alternate"></link>
    <media:content medium="image" url="https://jcalderita.com/static/blog/UUID-V7.webp"></media:content>
  </entry>
  <entry>
    <id>https://jcalderita.com/es/blog/sigbus/</id>
    <title>SIGBUS</title>
    <updated>2026-04-29T00:00:00Z</updated>
    <summary>Cómo Swift Testing crashea con SIGBUS al formatear errores de enums con valores asociados, y cómo lo solucioné con tres helpers propios.</summary>
    <content type="html">&lt;hr /&gt;
&lt;h2&gt;Mi Problema 🤔&lt;/h2&gt;
&lt;p&gt;Estoy escribiendo tests con &lt;span class="high"&gt;Swift Testing&lt;/span&gt;, el stack nativo de Apple. Comparo dos modelos con &lt;span class="high"&gt;#expect(a == b)&lt;/span&gt; y verifico errores con &lt;span class="high"&gt;#expect(throws: MyError.self)&lt;/span&gt; — la API que la propia librería ofrece de serie. Lanzo los tests. Y mi binario muere con esto:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-text"&gt;*** Signal 10: Backtracing from 0x18a3c4e8c... done ***
Process terminated by signal 10 (SIGBUS)
Stack trace:
  ...
  _swift_buildDemanglingForMetadata
  ...
  swift_testing.Issue.record(...)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;span class="high"&gt;SIGBUS&lt;/span&gt;. No es un fallo de aserción. Es el &lt;strong&gt;runtime de Swift&lt;/strong&gt; rompiéndose dentro del propio test runner. Y solo pasa cuando un test &lt;strong&gt;falla&lt;/strong&gt; — si todos pasan, no me entero del problema.&lt;/p&gt;
&lt;p&gt;El patrón es muy concreto. El runtime crashea cuando uso &lt;span class="high"&gt;#expect(==)&lt;/span&gt; sobre &lt;span class="high"&gt;enums con valores asociados&lt;/span&gt;, o cuando uso &lt;span class="high"&gt;#expect(throws:)&lt;/span&gt; sobre un tipo de error que es un enum con valores asociados. En cuanto el test falla, swift-testing intenta construir el mensaje de error pidiéndole al runtime que reconstruya los metadatos de tipo de los valores que comparó. Y ahí truena: &lt;span class="high"&gt;_swift_buildDemanglingForMetadata&lt;/span&gt;, signal 10, fin.&lt;/p&gt;
&lt;p&gt;Es el bug &lt;a href="https://github.com/swiftlang/swift/issues/76608"&gt;swiftlang/swift#76608&lt;/a&gt;, abierto desde septiembre de 2024 y reproducible en Swift 6.3.1 sobre macOS 26 — tanto en debug como en release.&lt;/p&gt;
&lt;p&gt;Lo curioso es que el camino feliz no toca nunca este bug. Si la comparación pasa, no hay mensaje de error que formatear, no hay &lt;span class="high"&gt;Mirror&lt;/span&gt;, no hay SIGBUS. El bug solo aparece justo cuando más necesitas el output del test: cuando algo falla. Y no podía mover una pieza hasta resolverlo.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Mi Solución 🧩&lt;/h2&gt;
&lt;p&gt;La pista clave es &lt;strong&gt;dónde&lt;/strong&gt; crashea swift-testing: en el camino del &lt;em&gt;failure message&lt;/em&gt;, formateando los valores. Si yo hago la comparación por mi cuenta y le paso al reporter solamente un &lt;span class="high"&gt;Bool&lt;/span&gt;, swift-testing no tiene nada que reconstruir. No hay metadata que reflejar. El espejo no se rompe porque no se llega a usar.&lt;/p&gt;
&lt;p&gt;El truco que evita el SIGBUS cabe en dos líneas:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-swift"&gt;let isEqual = actual == expected
#expect(isEqual)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Esa es toda la idea. La igualdad la calculo yo, swift-testing solo recibe un &lt;span class="high"&gt;Bool&lt;/span&gt; ya resuelto. Sin valores que reflejar, no hay &lt;span class="high"&gt;Mirror&lt;/span&gt;, no hay SIGBUS. El precio es perder los valores en el mensaje de fallo — pero eso lo recupero formateando el texto a mano antes de pasarlo a &lt;span class="high"&gt;Issue.record&lt;/span&gt;.&lt;/p&gt;
&lt;p&gt;A partir de ahí monté un paquete &lt;span class="high"&gt;TestKit&lt;/span&gt; con tres helpers que aplican esa misma idea para los tres casos en los que tropezaba con el bug:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;span class="high"&gt;expectEqual(actual, expected)&lt;/span&gt; — comparación de cualquier &lt;span class="high"&gt;Equatable&lt;/span&gt;: structs, modelos, enums con valores asociados. Sustituye a &lt;span class="high"&gt;#expect(a == b)&lt;/span&gt; en todos los tests del monorepo.&lt;/li&gt;
&lt;li&gt;&lt;span class="high"&gt;expectThrows(E.self) { … }&lt;/span&gt; — verifica que un closure lanza un error del tipo esperado. Hace &lt;span class="high"&gt;do/catch&lt;/span&gt; a mano y comprueba con &lt;span class="high"&gt;catch is E&lt;/span&gt; — solo el tipo, nunca el valor. Cubre lo que &lt;span class="high"&gt;#expect(throws:)&lt;/span&gt; intentaba dar de serie. Tiene overload sync y async.&lt;/li&gt;
&lt;li&gt;&lt;span class="high"&gt;expectEqualLines(actual, expected)&lt;/span&gt; — diff línea a línea para verificar SQL generado y otros snapshots inline. La comparación también se reduce a &lt;span class="high"&gt;Bool&lt;/span&gt; antes de tocar el reporter.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Tres helpers, una sola idea: &lt;strong&gt;calcular el resultado fuera del macro, pasar solo el booleano&lt;/strong&gt;. Cuando algún día Swift 6.4 cierre &lt;a href="https://github.com/swiftlang/swift/issues/76608"&gt;el issue 76608&lt;/a&gt;, basta con sustituir el cuerpo de los helpers por &lt;span class="high"&gt;#expect&lt;/span&gt; directos y la suite ni se entera.&lt;/p&gt;
&lt;p&gt;Hay un detalle adicional para que los mensajes de fallo sigan siendo legibles: conformé los tipos de error y schema a &lt;span class="high"&gt;CustomStringConvertible&lt;/span&gt; con un &lt;span class="high"&gt;switch&lt;/span&gt; estático sobre los casos — &lt;strong&gt;nunca interpolando &lt;span class="high"&gt;(self)&lt;/span&gt;&lt;/strong&gt;, porque eso volvería a invocar &lt;span class="high"&gt;Mirror&lt;/span&gt; y reabriría la trampa.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Mi Resultado 🎯&lt;/h2&gt;
&lt;p&gt;Mi suite de tests vuelve a correr sin SIGBUS y con mensajes de error suficientemente legibles. La regla que aplico ahora en todo el monorepo:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;span class="high"&gt;expectEqual(a, b)&lt;/span&gt; para cualquier comparación entre structs, enums o modelos&lt;/li&gt;
&lt;li&gt;&lt;span class="high"&gt;expectThrows(E.self) { … }&lt;/span&gt; para verificar el tipo de error lanzado&lt;/li&gt;
&lt;li&gt;&lt;span class="high"&gt;#expect(…)&lt;/span&gt; directo solo cuando el argumento ya es &lt;span class="high"&gt;Bool&lt;/span&gt;, &lt;span class="high"&gt;nil&lt;/span&gt; o &lt;span class="high"&gt;contains&lt;/span&gt; — ahí no hay metadata de valores que reflejar y el bug no se dispara&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Los beneficios:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Suite que no crashea&lt;/strong&gt; — los fallos cuentan como fallos, no como abortos del proceso&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Stack mínimo&lt;/strong&gt; — solo dependo de &lt;span class="high"&gt;Testing&lt;/span&gt; y del propio toolchain de Swift&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Mensajes propios&lt;/strong&gt; — controlo cómo se imprime cada lado al fallar, gracias a &lt;span class="high"&gt;CustomStringConvertible&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Aislamiento del bug del runtime&lt;/strong&gt; — cuando Swift 6.4 lo solucione, basta con cambiar el cuerpo de los helpers y la suite ni se entera&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;La lección que me llevo: cuando una herramienta del runtime se rompe en el camino del error, la solución no es renunciar al lenguaje, es no dejar que la herramienta entre por ese camino. Hago yo la comparación, le paso un booleano al reporter, y el mirror se queda intacto.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Keep coding, keep running&lt;/strong&gt; 🏃‍♂️&lt;/p&gt;
&lt;hr /&gt;</content>
    <link rel="alternate" href="https://jcalderita.com/es/blog/sigbus/"></link>
    <media:content medium="image" url="https://jcalderita.com/static/blog/SIGBUS.webp"></media:content>
  </entry>
  <entry>
    <id>https://jcalderita.com/es/blog/two-bigs/</id>
    <title>Two Bigs</title>
    <updated>2026-04-22T00:00:00Z</updated>
    <summary>Casi todo el contenido técnico me aburre porque le falta pasión. Mis tres filtros para encontrar lo que merece mi tiempo y por qué Julio y Oliver siempre los pasan.</summary>
    <content type="html">&lt;hr /&gt;
&lt;h2&gt;Mi Problema 🤔&lt;/h2&gt;
&lt;p&gt;Consumo mucho contenido técnico. Podcasts, directos, newsletters, artículos. Y con el tiempo he desarrollado un filtro que no puedo desactivar: detecto en los primeros treinta segundos si la persona que habla siente pasión por lo que está contando o simplemente está repitiendo lo que ha leído.&lt;/p&gt;
&lt;p&gt;Y la mayoría de las veces es lo segundo.&lt;/p&gt;
&lt;p&gt;No hablo de calidad técnica. Hay contenido bien producido, bien estructurado, con buenos títulos, que me aburre profundamente. Porque detrás no hay nadie que se emocione con lo que dice. Es contenido fabricado para existir, no porque alguien necesitara contarlo.&lt;/p&gt;
&lt;p&gt;Cuando no hay pasión, pierdo el interés en menos de un minuto. Me da igual lo relevante que sea el tema. Si quien habla no vibra con lo que cuenta, yo tampoco. Y no es una decisión consciente — es una reacción física. Mi cerebro desconecta y ya no hay forma de volver.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Mi Solución 🧩&lt;/h2&gt;
&lt;p&gt;Siempre estoy en búsqueda activa de contenido dentro de mis temas de interés. Tecnología, desarrollo, IA, Apple, etc… Y siempre doy una oportunidad a lo que encuentro, porque sé lo que cuesta crear. Detrás de cada vídeo, cada podcast, cada artículo hay horas de trabajo, de preparación, de decisiones. Eso merece al menos un intento honesto por mi parte.&lt;/p&gt;
&lt;p&gt;Pero no tengo todo el tiempo del mundo, así que de manera natural he acabado aplicando tres filtros.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Pasión.&lt;/strong&gt; Si en los primeros segundos no noto que a quien habla le importa lo que está contando, mi atención se apaga sola. Sin pasión no hay atención.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Educación y respeto.&lt;/strong&gt; Quiero aprender de alguien que argumenta sin atacar y discrepa sin despreciar. La pasión sin respeto es ruido.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Satisfacción.&lt;/strong&gt; Que al terminar sienta que me llevo algo. Una idea nueva, una perspectiva que no tenía. Si no, no vuelvo.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;Mi Resultado 🎯&lt;/h2&gt;
&lt;p&gt;Estos filtros me han ido dejando una lista corta de favoritos. Y en esa lista están &lt;a href="https://www.linkedin.com/in/jcfmunoz/"&gt;Julio César Fernández&lt;/a&gt; y &lt;a href="https://www.linkedin.com/in/oliverac/"&gt;Oliver Nabani&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Cuando los astros se juntan y estos dos salen en un directo &lt;a href="https://www.youtube.com/watch?v=z6RIj_j6knE"&gt;Un futuro sin apps&lt;/a&gt;, me pasa algo que no me pasa con nadie más. Disfruto como un niño. Me río a carcajadas. Les hablo a la pantalla. Rebobino para apuntarme en papel algo que han dicho de pasada y que quiero investigar después. Me estoy meando pero no paro la reproducción porque no quiero perderme ni una frase.&lt;/p&gt;
&lt;p&gt;Y siempre, siempre, al terminar pienso lo mismo: “pero si solo han estado quince minutos, cada vez los hacen más cortos”. Miro el reloj. Casi tres horas. No me he enterado de que pasaba el tiempo.&lt;/p&gt;
&lt;p&gt;Eso es pasión. Eso es lo que busco. Eso es lo que estos dos transmiten cada vez que se ponen delante de una cámara juntos.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Dos grandes. Dos pasiones. Dos amigos. Y los demás somos sus invitados.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Keep coding, keep running&lt;/strong&gt; 🏃‍♂️&lt;/p&gt;
&lt;hr /&gt;</content>
    <link rel="alternate" href="https://jcalderita.com/es/blog/two-bigs/"></link>
    <media:content medium="image" url="https://jcalderita.com/static/blog/TwoBigs.webp"></media:content>
  </entry>
  <entry>
    <id>https://jcalderita.com/es/blog/astro-to-saga/</id>
    <title>Astro to Saga</title>
    <updated>2026-04-15T00:00:00Z</updated>
    <summary>Por qué migré mi portfolio de Astro a Saga, un generador de sitios estáticos en Swift, y cómo eliminé Node de mi stack para siempre.</summary>
    <content type="html">&lt;hr /&gt;
&lt;h2&gt;Mi Problema 🤔&lt;/h2&gt;
&lt;p&gt;Mi portfolio funcionaba sobre &lt;span class="high"&gt;Astro&lt;/span&gt; con &lt;span class="high"&gt;Bun&lt;/span&gt;. Todo iba bien. Rápido, cómodo, sin quejas técnicas.&lt;/p&gt;
&lt;p&gt;Pero había algo que no encajaba. Cada vez que abría el proyecto me encontraba con un &lt;span class="high"&gt;package.json&lt;/span&gt;, un &lt;span class="high"&gt;tailwind.config&lt;/span&gt;, un &lt;span class="high"&gt;astro.config&lt;/span&gt;, y una carpeta &lt;span class="high"&gt;node_modules&lt;/span&gt; con cientos de dependencias que ni sabía que existían. Esa sensación de perder el control sobre lo que hay dentro de tu propio proyecto me incomoda. Me gusta saber qué ejecuta mi código y por qué está ahí. Ya había eliminado Tailwind en una &lt;a href="/es/blog/tailwind-to-css/"&gt;migración previa a vanilla CSS&lt;/a&gt;, pero el resto del ecosistema JavaScript seguía ahí.&lt;/p&gt;
&lt;p&gt;Y al final, soy desarrollador Swift. Mi día a día es Swift. Y sin embargo, para generar mi propio portfolio dependía de un ecosistema completamente ajeno. Si alguien entraba en mi repositorio, no veía a un desarrollador Swift. Veía un proyecto JavaScript más.&lt;/p&gt;
&lt;p&gt;La pregunta era sencilla: si confío en Swift para todo lo demás, ¿por qué no confío en Swift para esto?&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Mi Solución 🧩&lt;/h2&gt;
&lt;p&gt;Me paré a pensar qué necesitaba realmente. Mi portfolio no es una aplicación web. No tiene estado ni interactividad compleja. Es un conjunto de páginas HTML estáticas generadas a partir de Markdown. No necesito React, ni Vue, ni hidratación. Necesito algo que lea Markdown, lo transforme en HTML y lo escriba en disco.&lt;/p&gt;
&lt;p&gt;Encontré &lt;a href="https://github.com/loopwerk/Saga"&gt;Saga&lt;/a&gt;, un generador de sitios estáticos en Swift. Deliberadamente minimalista: lee archivos, aplica transformaciones, escribe HTML. Lo que no incluye, lo decides tú. Además incluye hot reload para desarrollo, algo que no esperaba encontrar en un proyecto tan pequeño. Esa filosofía me convenció más que cualquier feature list.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Ventajas&lt;/th&gt;
&lt;th&gt;Desventajas&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Todo el stack en Swift — un solo lenguaje, sin cambio de contexto&lt;/td&gt;
&lt;td&gt;Ecosistema pequeño — si algo no existe, lo construyes tú&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HTML verificado por el compilador gracias al DSL tipado&lt;/td&gt;
&lt;td&gt;Curva de aprendizaje con la sintaxis del DSL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pipeline de imágenes nativo sin dependencias externas&lt;/td&gt;
&lt;td&gt;Pipeline de imágenes solo funciona en macOS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Node eliminado del entorno local&lt;/td&gt;
&lt;td&gt;Comunidad y documentación limitadas&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Control total sobre cada dependencia del proyecto&lt;/td&gt;
&lt;td&gt;Más trabajo inicial para funcionalidades que en otros frameworks vienen de serie&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;La reflexión de fondo&lt;/h3&gt;
&lt;p&gt;La decisión no fue técnica. Fue sobre coherencia. Quiero que si alguien visita mi repositorio, vea Swift. No digo que &lt;span class="high"&gt;Astro&lt;/span&gt; sea malo — es excelente. Pero mi portfolio es mi tarjeta de presentación, y esa tarjeta tiene que hablar de mí.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Mi Resultado 🎯&lt;/h2&gt;
&lt;p&gt;El sitio que lees ahora mismo está generado con &lt;span class="high"&gt;Saga&lt;/span&gt;, compilado con Swift, y desplegado sobre &lt;span class="high"&gt;Cloudflare Pages&lt;/span&gt; sin que Node haya intervenido en mi máquina.&lt;/p&gt;
&lt;p&gt;Lo que me sorprendió no fue el resultado técnico. Fue la sensación de coherencia. Abrir mi portfolio y ver que todo, desde la primera línea hasta el último deploy, es Swift me produce una tranquilidad difícil de explicar.&lt;/p&gt;
&lt;p&gt;Si eres desarrollador Swift y tu portfolio corre sobre Node, no digo que debas cambiar. Digo que merece la pena preguntarse por qué. Al final, la herramienta que eliges para presentarte dice algo sobre ti. Yo elegí la que me representa.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Keep coding, keep running&lt;/strong&gt; 🏃‍♂️&lt;/p&gt;
&lt;hr /&gt;</content>
    <link rel="alternate" href="https://jcalderita.com/es/blog/astro-to-saga/"></link>
    <media:content url="https://jcalderita.com/static/blog/AstroToSaga.webp" medium="image"></media:content>
  </entry>
  <entry>
    <id>https://jcalderita.com/es/blog/only-dns/</id>
    <title>OnlyDNS</title>
    <updated>2026-04-08T00:00:00Z</updated>
    <summary>Cómo desactivar el proxy de Cloudflare y pasar a modo Solo DNS resolvió en 5 minutos el bloqueo de La Liga que arrastraba desde hacía meses.</summary>
    <content type="html">&lt;hr /&gt;
&lt;h2&gt;Mi Problema 🤔&lt;/h2&gt;
&lt;p&gt;En el &lt;a href="/es/blog/la-liga-thanks/"&gt;artículo anterior&lt;/a&gt; hablé de cómo &lt;span class="high"&gt;La Liga&lt;/span&gt; bloqueaba mi web durante los partidos. Gente intentando visitar mi portfolio y mi blog se encontraba con que la página no cargaba, simplemente porque mi dominio pasaba por la infraestructura de &lt;span class="high"&gt;Cloudflare&lt;/span&gt;, y &lt;span class="high"&gt;La Liga&lt;/span&gt; bloqueaba rangos enteros de IPs de &lt;span class="high"&gt;Cloudflare&lt;/span&gt; para frenar las retransmisiones ilegales.&lt;/p&gt;
&lt;p&gt;Lo sabía. Lo documenté. Y me quedé ahí.&lt;/p&gt;
&lt;p&gt;Durante meses no hice nada para solucionarlo. No por pereza ni por pensar que fuese complicado — sino por enfado. Mi web funcionaba perfectamente, era &lt;span class="high"&gt;La Liga&lt;/span&gt; la que aplicaba bloqueos indiscriminados y la dejaba inaccesible. No era culpa mía. Así que me planté: no tenía por qué ser yo quien moviese ficha.&lt;/p&gt;
&lt;p&gt;Así que no hice nada. Durante meses.&lt;/p&gt;
&lt;p&gt;Lo que pasa es que tenía el proxy de &lt;span class="high"&gt;Cloudflare&lt;/span&gt; activo — la famosa nube naranja. Mi web está alojada en &lt;span class="high"&gt;Cloudflare Pages&lt;/span&gt;, así que todo está dentro del ecosistema de &lt;span class="high"&gt;Cloudflare&lt;/span&gt;. Pero con el proxy activado, el tráfico pasa por la capa de CDN/proxy de &lt;span class="high"&gt;Cloudflare&lt;/span&gt;, que usa unos rangos de IPs compartidas. Y esos rangos son precisamente los que &lt;span class="high"&gt;La Liga&lt;/span&gt; bloqueaba.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Mi Solución 🧩&lt;/h2&gt;
&lt;p&gt;La solución es desactivar el proxy de &lt;span class="high"&gt;Cloudflare&lt;/span&gt; y pasar al modo &lt;span class="high"&gt;Solo DNS&lt;/span&gt; — la nube gris.&lt;/p&gt;
&lt;p&gt;Con la nube naranja activa, &lt;span class="high"&gt;Cloudflare&lt;/span&gt; es un “centro comercial”: todos los visitantes entran por la puerta de su proxy/CDN, que usa rangos de IPs compartidas con miles de sitios. Son esas IPs las que &lt;span class="high"&gt;La Liga&lt;/span&gt; bloquea en masa para frenar las retransmisiones ilegales — y mi web queda como daño colateral.&lt;/p&gt;
&lt;p&gt;Con la nube gris, &lt;span class="high"&gt;Cloudflare&lt;/span&gt; pasa a ser solo un “director de tráfico”: resuelve el DNS y apunta directamente a &lt;span class="high"&gt;Cloudflare Pages&lt;/span&gt;. Mi web sigue alojada en &lt;span class="high"&gt;Cloudflare&lt;/span&gt;, pero el tráfico llega a través de los rangos de IPs de &lt;span class="high"&gt;Pages&lt;/span&gt; — que son distintos a los del proxy y que &lt;span class="high"&gt;La Liga&lt;/span&gt; no tiene en su lista de bloqueo.&lt;/p&gt;
&lt;p&gt;El proceso en el panel de &lt;span class="high"&gt;Cloudflare&lt;/span&gt; es este:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Entrar en el panel de &lt;span class="high"&gt;Cloudflare&lt;/span&gt; y navegar a &lt;strong&gt;DNS &amp;gt; Registros (Records)&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Buscar los registros que apuntan a la web — normalmente un registro &lt;strong&gt;A&lt;/strong&gt; o &lt;strong&gt;CNAME&lt;/strong&gt; con el nombre del dominio.&lt;/li&gt;
&lt;li&gt;En la columna &lt;strong&gt;Estado del proxy&lt;/strong&gt;, hacer clic en el interruptor con la nube naranja para convertirla en una nube gris (&lt;strong&gt;Solo DNS&lt;/strong&gt;).&lt;/li&gt;
&lt;li&gt;Guardar los cambios.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;El cambio propaga en minutos. A partir de ese momento, el tráfico llega por una ruta distinta dentro de &lt;span class="high"&gt;Cloudflare&lt;/span&gt; — una que no está en el punto de mira de los bloqueos de &lt;span class="high"&gt;La Liga&lt;/span&gt;.&lt;/p&gt;
&lt;p&gt;Como mi web es estática y sigue alojada en &lt;span class="high"&gt;Cloudflare Pages&lt;/span&gt;, no noto prácticamente ninguna diferencia de velocidad. Pierdo algunas funciones de la capa proxy — como el firewall a nivel de CDN o la caché en sus edge nodes — pero para una web estática de portfolio esas ventajas son marginales comparadas con el beneficio de que la web funcione para todo el mundo durante los partidos.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Mi Resultado 🎯&lt;/h2&gt;
&lt;p&gt;Mi web ya carga durante los partidos de &lt;span class="high"&gt;La Liga&lt;/span&gt;. Cualquier persona que intente visitar mi portfolio o blog mientras suena el himno de la Champions no se va a encontrar con una pantalla en blanco.&lt;/p&gt;
&lt;p&gt;Cinco minutos. Un clic. Meses de problema resuelto.&lt;/p&gt;
&lt;p&gt;La lección que me llevo no es técnica — es pragmática. Tuviese razón o no, mi problema o lo solucionaba yo o no lo iba a solucionar nadie. Y creo que tengo razón: mi web no tiene por qué ser daño colateral de los bloqueos de &lt;span class="high"&gt;La Liga&lt;/span&gt;. Gracias, &lt;span class="high"&gt;La Liga&lt;/span&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Keep coding, keep running&lt;/strong&gt; 🏃‍♂️&lt;/p&gt;
&lt;hr /&gt;</content>
    <link href="https://jcalderita.com/es/blog/only-dns/" rel="alternate"></link>
    <media:content url="https://jcalderita.com/static/blog/OnlyDNS.webp" medium="image"></media:content>
  </entry>
  <entry>
    <id>https://jcalderita.com/es/blog/my-first-macro/</id>
    <title>My First Macro</title>
    <updated>2026-04-01T00:00:00Z</updated>
    <summary>Macro para generar automáticamente el ID, los timestamps y el espacio de nombres en cada modelo Fluent.</summary>
    <content type="html">&lt;hr /&gt;
&lt;h2&gt;Mi Problema 🤔&lt;/h2&gt;
&lt;p&gt;En mi proyecto tengo decenas de modelos &lt;span class="high"&gt;Fluent&lt;/span&gt;, y hay un bloque de código que se me repite en absolutamente todos ellos:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-swift"&gt;public static let space: String? = &amp;quot;sales&amp;quot;

@ID() public var id: UUID?
public init() {}

@Timestamp(.createdAt, on: .create) public var createdAt: Date?
@Timestamp(.updatedAt, on: .update) public var updatedAt: Date?
@Timestamp(.deletedAt, on: .delete) public var deletedAt: Date?
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Siempre el mismo bloque. En cada modelo. Sin excepción.&lt;/p&gt;
&lt;p&gt;El espacio de nombres cambia, pero la estructura es idéntica. Con cada modelo nuevo, copio y pego, ajusto el espacio y espero no olvidarme de nada. Con 30 modelos, ese boilerplate se convierte en ruido que me dificulta leer lo que realmente importa: la lógica del modelo.&lt;/p&gt;
&lt;p&gt;Mi solución en Swift para este tipo de problemas es una &lt;span class="high"&gt;macro&lt;/span&gt;.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Mi Solución 🧩&lt;/h2&gt;
&lt;p&gt;Una macro de Swift puede generar miembros nuevos en una clase o struct en tiempo de compilación. Exactamente lo que necesito. El resultado que busco es escribir esto:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-swift"&gt;@FluentModel(.sales)
public final class ProductModel: Model {
    public static let schema = &amp;quot;products&amp;quot;

    @Field(.name) public var name: String
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Y que el compilador inyecte automáticamente &lt;span class="high"&gt;space&lt;/span&gt;, &lt;span class="high"&gt;id&lt;/span&gt;, &lt;span class="high"&gt;init()&lt;/span&gt; y los tres &lt;span class="high"&gt;timestamps&lt;/span&gt;. Sin escribirlos. Sin mantenerlos.&lt;/p&gt;
&lt;h3&gt;Estructura del paquete&lt;/h3&gt;
&lt;p&gt;Las macros en Swift requieren dos targets separados: la &lt;strong&gt;interfaz pública&lt;/strong&gt; (lo que consumo como desarrollador) y el &lt;strong&gt;plugin&lt;/strong&gt; (la implementación que ejecuta el compilador). En mi caso, ambos viven en el mismo paquete &lt;span class="high"&gt;Macros&lt;/span&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;span class="high"&gt;Macros&lt;/span&gt; — declara la macro y el enum &lt;span class="high"&gt;DatabaseSpace&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span class="high"&gt;MacrosPlugin&lt;/span&gt; — implementa la expansión con &lt;span class="high"&gt;SwiftSyntax&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;DatabaseSpace — los espacios como tipos&lt;/h3&gt;
&lt;p&gt;Antes de definir la macro, necesito modelar los espacios de nombres disponibles. En lugar de usar strings sueltos, decidí que el enum &lt;span class="high"&gt;DatabaseSpace&lt;/span&gt; los haga exhaustivos y seguros en tiempo de compilación:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-swift"&gt;public enum DatabaseSpace: String, CaseIterable, Sendable {
    case sales, warehouse
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Esto permite escribir &lt;span class="high"&gt;@FluentModel(.sales)&lt;/span&gt; en lugar de &lt;span class="high"&gt;@FluentModel(“sales”)&lt;/span&gt;. Si el espacio no existe, el compilador lo dice antes de ejecutar nada.&lt;/p&gt;
&lt;h3&gt;La declaración del macro&lt;/h3&gt;
&lt;p&gt;La interfaz pública de la macro es sorprendentemente compacta:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-swift"&gt;@attached(member, names: named(space), named(id), named(init), named(createdAt), named(updatedAt), named(deletedAt))
public macro FluentModel(_ space: DatabaseSpace? = nil) = #externalMacro(
    module: &amp;quot;MacrosPlugin&amp;quot;,
    type: &amp;quot;FluentModelMacro&amp;quot;
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;El atributo &lt;span class="high"&gt;@attached(member, names:)&lt;/span&gt; le dice al compilador dos cosas: que esta macro añade miembros a la declaración donde se aplica, y cuáles son los nombres exactos que va a generar. Declarar los nombres es obligatorio — Swift los necesita para resolver el árbol de símbolos antes de expandir la macro.&lt;/p&gt;
&lt;h3&gt;La implementación — FluentModelMacro&lt;/h3&gt;
&lt;p&gt;La implementación conforma el protocolo &lt;span class="high"&gt;MemberMacro&lt;/span&gt; y devuelve un array de &lt;span class="high"&gt;DeclSyntax&lt;/span&gt; — fragmentos de código Swift que el compilador inserta en el modelo:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-swift"&gt;import SwiftSyntax
import SwiftSyntaxMacros

public struct FluentModelMacro: MemberMacro {
    public static func expansion(
        of node: AttributeSyntax,
        providingMembersOf declaration: some DeclGroupSyntax,
        conformingTo protocols: [TypeSyntax],
        in context: some MacroExpansionContext
    ) throws -&amp;gt; [DeclSyntax] {
        [
            spaceDecl(from: node),
            &amp;quot;@ID() public var id: UUID?&amp;quot;,
            &amp;quot;public init() {}&amp;quot;,
            &amp;quot;@Timestamp(.createdAt, on: .create) public var createdAt: Date?&amp;quot;,
            &amp;quot;@Timestamp(.updatedAt, on: .update) public var updatedAt: Date?&amp;quot;,
            &amp;quot;@Timestamp(.deletedAt, on: .delete) public var deletedAt: Date?&amp;quot;,
        ]
    }

    private static func spaceDecl(from node: AttributeSyntax) -&amp;gt; DeclSyntax {
        let value = node.arguments?.as(LabeledExprListSyntax.self)?
            .first?.expression.as(MemberAccessExprSyntax.self)
            .map { &amp;quot;\&amp;quot;\($0.declName.baseName.text)\&amp;quot;&amp;quot; } ?? &amp;quot;nil&amp;quot;
        return &amp;quot;public static let space: String? = \(raw: value)&amp;quot;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;El método &lt;span class="high"&gt;expansion&lt;/span&gt; devuelve directamente el array de declaraciones, sin variables intermedias. Cada elemento es código Swift literal que el compilador inyecta tal cual en el modelo.&lt;/p&gt;
&lt;p&gt;La clave está en &lt;span class="high"&gt;spaceDecl&lt;/span&gt;, que encapsula toda la lógica de extracción y generación en un solo método. Navega el árbol sintáctico del atributo con &lt;span class="high"&gt;SwiftSyntax&lt;/span&gt; usando optional chaining: accede a los argumentos como &lt;span class="high"&gt;LabeledExprListSyntax&lt;/span&gt;, toma la primera expresión, la castea a &lt;span class="high"&gt;MemberAccessExprSyntax&lt;/span&gt; (porque el argumento es un caso de enum como &lt;span class="high"&gt;.sales&lt;/span&gt;) y con &lt;span class="high"&gt;.map&lt;/span&gt; lo convierte en un string entrecomillado. Si cualquier paso de la cadena falla, el operador &lt;span class="high"&gt;??&lt;/span&gt; devuelve &lt;span class="high"&gt;“nil”&lt;/span&gt;. Finalmente, &lt;span class="high"&gt;\(raw:)&lt;/span&gt; interpola el valor directamente en el &lt;span class="high"&gt;DeclSyntax&lt;/span&gt;.&lt;/p&gt;
&lt;p&gt;Por último, necesito un &lt;span class="high"&gt;CompilerPlugin&lt;/span&gt; que registre la macro — es el punto de entrada que el compilador carga para saber qué macros están disponibles:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-swift"&gt;import SwiftCompilerPlugin
import SwiftSyntaxMacros

@main
struct MacrosPlugin: CompilerPlugin {
    let providingMacros: [Macro.Type] = [
        FluentModelMacro.self,
    ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Mi Resultado 🎯&lt;/h2&gt;
&lt;p&gt;Con la macro instalada, cada modelo queda limpio y sin ruido:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-swift"&gt;@FluentModel(.sales)
public final class ProductModel: Model {
    public static let schema = &amp;quot;products&amp;quot;

    @Field(.name) public var name: String
    @OptionalField(.description) public var description: String?
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;El compilador expande &lt;span class="high"&gt;@FluentModel(.sales)&lt;/span&gt; y genera automáticamente:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-swift"&gt;public static let space: String? = &amp;quot;sales&amp;quot;
@ID() public var id: UUID?
public init() {}
@Timestamp(.createdAt, on: .create) public var createdAt: Date?
@Timestamp(.updatedAt, on: .update) public var updatedAt: Date?
@Timestamp(.deletedAt, on: .delete) public var deletedAt: Date?
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Y si un modelo no pertenece a ningún espacio de nombres — como las vistas — simplemente se omite el argumento:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-swift"&gt;@FluentModel()
public final class OrderSummaryModel: Model {
    public static let schema = &amp;quot;order_summaries&amp;quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;En ese caso, &lt;span class="high"&gt;space&lt;/span&gt; se genera como &lt;span class="high"&gt;nil&lt;/span&gt; y Fluent ignora el prefijo de esquema.&lt;/p&gt;
&lt;p&gt;Los beneficios en números: &lt;strong&gt;32 modelos&lt;/strong&gt; con la macro aplicada. &lt;strong&gt;6 líneas eliminadas&lt;/strong&gt; por modelo. Más de &lt;strong&gt;190 líneas de boilerplate&lt;/strong&gt; que ya no existen en el repositorio y que nunca más habrá que mantener.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Keep coding, keep running&lt;/strong&gt; 🏃‍♂️&lt;/p&gt;
&lt;hr /&gt;</content>
    <link href="https://jcalderita.com/es/blog/my-first-macro/" rel="alternate"></link>
    <media:content medium="image" url="https://jcalderita.com/static/blog/MyFirstMacro.webp"></media:content>
  </entry>
  <entry>
    <id>https://jcalderita.com/es/blog/db-admin-vs-developer/</id>
    <title>DB Admin vs Dev</title>
    <updated>2026-03-23T00:00:00Z</updated>
    <summary>Separar la administración de base de datos del desarrollo no es una opinión: es una responsabilidad.</summary>
    <content type="html">&lt;hr /&gt;
&lt;h2&gt;El problema 🤔&lt;/h2&gt;
&lt;p&gt;En muchos proyectos he visto cómo el código de la aplicación termina haciendo cosas que no le corresponden: crear roles, configurar parámetros del servidor de base de datos, gestionar permisos a nivel de instancia. Todo mezclado en el mismo sitio, como si fuera una sola responsabilidad.&lt;/p&gt;
&lt;p&gt;Pero no lo es. La base de datos tiene &lt;strong&gt;dos mundos distintos&lt;/strong&gt;: la &lt;span class="high"&gt;administración&lt;/span&gt; 🛡️ y el &lt;span class="high"&gt;desarrollo&lt;/span&gt; 💻.&lt;/p&gt;
&lt;p&gt;Mezclarlos es uno de los errores más comunes, y también uno de los más costosos 💸. Un desarrollador que configura la instancia desde su código está cruzando una frontera que no le corresponde. Y un administrador que intenta decidir qué tablas o esquemas usa la aplicación está haciendo lo mismo desde el otro lado.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;La idea 🧩&lt;/h2&gt;
&lt;p&gt;La separación es conceptualmente limpia: &lt;strong&gt;la administración pertenece al servidor, el desarrollo pertenece al código&lt;/strong&gt;.&lt;/p&gt;
&lt;h3&gt;El territorio del administrador 🛡️&lt;/h3&gt;
&lt;p&gt;El administrador gestiona la &lt;strong&gt;instancia&lt;/strong&gt; de la base de datos. Su trabajo incluye:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;🔧 &lt;strong&gt;Configuración del servidor&lt;/strong&gt; — parámetros de rendimiento, extensiones a nivel de instancia, ajustes que requieren reinicio&lt;/li&gt;
&lt;li&gt;👤 &lt;strong&gt;Roles y credenciales&lt;/strong&gt; — crear usuarios, asignar contraseñas seguras, definir quién puede conectarse&lt;/li&gt;
&lt;li&gt;🗄️ &lt;strong&gt;Bases de datos&lt;/strong&gt; — crearlas, establecer timeouts, revocar accesos por defecto&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Todo esto es infraestructura 🔐. Son decisiones que se toman una vez, las ejecuta alguien con permisos de superusuario, y no tienen nada que ver con la lógica de negocio. Lo ideal es que vivan como scripts SQL versionados en el repositorio, pero separados del código de la aplicación.&lt;/p&gt;
&lt;h3&gt;El territorio del desarrollador 💻&lt;/h3&gt;
&lt;p&gt;El desarrollador gestiona la &lt;strong&gt;estructura de datos de la aplicación&lt;/strong&gt;: esquemas, tablas, índices, vistas, y todo lo que necesita existir para que la aplicación funcione.&lt;/p&gt;
&lt;p&gt;Esta responsabilidad se expresa a través de &lt;span class="high"&gt;migraciones&lt;/span&gt; ✨ — piezas de código versionadas, reversibles y revisables como cualquier otro cambio. Da igual si usas Swift, Python, Go o TypeScript: el principio es el mismo. La estructura de datos de tu aplicación debe poder reproducirse automáticamente en cualquier entorno 🔄.&lt;/p&gt;
&lt;h3&gt;La zona gris: ¿admin o desarrollo? 🤷&lt;/h3&gt;
&lt;p&gt;Hay casos que parecen ambiguos. Los roles de base de datos son un buen ejemplo. El &lt;strong&gt;rol en sí&lt;/strong&gt; — con su contraseña y permisos de conexión — es un concepto de infraestructura. Lo crea el administrador 🛡️.&lt;/p&gt;
&lt;p&gt;Pero los &lt;strong&gt;permisos sobre tablas concretas&lt;/strong&gt; — qué puede leer, qué puede escribir, sobre qué esquema — eso depende del desarrollador 💻. La tabla tiene que existir antes de que pueda tener permisos, así que esos permisos van en las migraciones, no en scripts de infraestructura.&lt;/p&gt;
&lt;p&gt;La regla que yo aplico es sencilla: si el objeto existe &lt;strong&gt;antes&lt;/strong&gt; que la aplicación, es administración. Si su existencia &lt;strong&gt;depende&lt;/strong&gt; de que la aplicación lo cree, es desarrollo ✅.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;El resultado 🎯&lt;/h2&gt;
&lt;p&gt;Cuando aplicas esta separación, el proceso de puesta en marcha queda claro y reproducible:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;🛡️ El administrador prepara la instancia → servidor configurado, roles creados, base de datos lista&lt;/li&gt;
&lt;li&gt;💻 El desarrollador ejecuta las migraciones → esquemas, tablas y permisos de aplicación aplicados automáticamente&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Cada capa tiene su herramienta. El administrador no toca el código de la aplicación. El desarrollador no necesita credenciales de superusuario 🔒. Nadie pisa el territorio del otro.&lt;/p&gt;
&lt;p&gt;Esta separación también facilita el trabajo en equipo 🤝: los scripts de administración los ejecuta quien tiene acceso al servidor, una sola vez. Las migraciones las ejecuta cualquier desarrollador del equipo en su entorno local, tantas veces como necesite.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;La base de datos no es solo tuya como desarrollador. Pero la estructura de datos de tu aplicación, sí.&lt;/strong&gt; 🏗️&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Keep coding, keep running&lt;/strong&gt; 🏃‍♂️&lt;/p&gt;
&lt;hr /&gt;</content>
    <link href="https://jcalderita.com/es/blog/db-admin-vs-developer/" rel="alternate"></link>
    <media:content url="https://jcalderita.com/static/blog/DBAdminVSDeveloper.webp" medium="image"></media:content>
  </entry>
  <entry>
    <id>https://jcalderita.com/es/blog/migrate-spaces/</id>
    <title>Migrate Spaces</title>
    <updated>2026-03-14T00:00:00Z</updated>
    <summary>Cómo crear espacios de nombres en PostgreSQL con migraciones de FluentKit en Vapor, reemplazando la configuración manual por código versionado y reversible.</summary>
    <content type="html">&lt;hr /&gt;
&lt;h2&gt;🧩 Problema&lt;/h2&gt;
&lt;p&gt;En el artículo anterior sobre &lt;a href="/es/blog/schema-space/"&gt;Esquemas y Espacios&lt;/a&gt; vimos cómo asignar un espacio de nombres a un modelo usando la propiedad &lt;span class="high"&gt;space&lt;/span&gt;. Muchos me preguntasteis lo mismo: &lt;strong&gt;¿cómo se crean esos espacios en la base de datos?&lt;/strong&gt; 🤔&lt;/p&gt;
&lt;p&gt;La respuesta más habitual era hacerlo a mano, ejecutando un &lt;span class="high"&gt;CREATE SCHEMA&lt;/span&gt; en la consola de PostgreSQL antes de lanzar las migraciones. Funciona, pero rompe algo fundamental: si la base de datos no existe o el entorno es nuevo, &lt;strong&gt;el proceso falla&lt;/strong&gt; antes de llegar a las migraciones reales 💥.&lt;/p&gt;
&lt;p&gt;El otro problema es que administrar espacios de nombres a mano no escala. En cuanto tienes varios entornos (desarrollo, staging, producción) o un equipo, mantener esa sincronía manual se convierte en una fuente de errores silenciosos 😬.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;💡 Solución&lt;/h2&gt;
&lt;p&gt;La solución es convertir la creación de espacios de nombres en una &lt;span class="high"&gt;migración&lt;/span&gt; más. De esta forma, se ejecuta automáticamente en el orden correcto, es reversible y está versionada junto al resto del código ✅.&lt;/p&gt;
&lt;p&gt;La clave está en combinar &lt;span class="high"&gt;FluentKit&lt;/span&gt; con &lt;span class="high"&gt;SQLKit&lt;/span&gt;. FluentKit gestiona el ciclo de vida de las migraciones, pero para ejecutar SQL arbitrario necesitamos acceder al driver subyacente mediante el protocolo &lt;span class="high"&gt;SQLDatabase&lt;/span&gt;. Para mantener el código organizado, encapsulamos los espacios de nombres en un enum con una acción que determina si se crean o se eliminan:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-swift"&gt;import FluentKit
import SQLKit

enum SRSchema: String, CaseIterable {
    case account, ai, device, location, sport, task, view

    enum Action {
        case create, drop
    }

    static func execute(_ action: Action, on db: Database) async throws {
        let sql = db as! any SQLDatabase
        let template: (String) -&amp;gt; String =
            switch action {
            case .create: { &amp;quot;CREATE SCHEMA IF NOT EXISTS \($0)&amp;quot; }
            case .drop: { &amp;quot;DROP SCHEMA IF EXISTS \($0) RESTRICT&amp;quot; }
            }
        for schema in allCases {
            try await sql.raw(&amp;quot;\(unsafeRaw: template(schema.rawValue))&amp;quot;).run()
        }
    }
}

struct SchemasMigration: AsyncMigration {
    func prepare(on db: Database) async throws {
        try await SRSchema.execute(.create, on: db)
    }

    func revert(on db: Database) async throws {
        try await SRSchema.execute(.drop, on: db)
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Algunos detalles importantes 📋:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;SRSchema&lt;/strong&gt; como &lt;strong&gt;CaseIterable&lt;/strong&gt;: al iterar sobre &lt;span class="high"&gt;allCases&lt;/span&gt;, nos aseguramos de que todos los espacios definidos se crean sin tener que listarlos manualmente en la migración. Añadir un nuevo espacio solo requiere agregar un caso al enum 🔄.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Action&lt;/strong&gt;: el enum interno modela las dos operaciones posibles (&lt;span class="high"&gt;create&lt;/span&gt; y &lt;span class="high"&gt;drop&lt;/span&gt;), lo que permite reutilizar el mismo método &lt;span class="high"&gt;execute&lt;/span&gt; tanto en el &lt;span class="high"&gt;prepare&lt;/span&gt; como en el &lt;span class="high"&gt;revert&lt;/span&gt; de la migración ♻️.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Switch expression&lt;/strong&gt;: se usa una &lt;em&gt;switch expression&lt;/em&gt; de Swift para seleccionar el template SQL según la acción. Cada rama devuelve un closure que genera la sentencia correspondiente, manteniendo la lógica compacta y legible 🧹.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CREATE SCHEMA IF NOT EXISTS&lt;/strong&gt;: hace que la migración sea idempotente. Si el esquema ya existe, no falla, simplemente continúa 🛡️.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;DROP SCHEMA … RESTRICT&lt;/strong&gt;: en el &lt;span class="high"&gt;revert&lt;/span&gt;, el modificador &lt;span class="high"&gt;RESTRICT&lt;/span&gt; impide eliminar un esquema que contenga tablas. Es una red de seguridad que evita pérdidas accidentales de datos al revertir 🔒.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;unsafeRaw&lt;/strong&gt;: se usa para interpolar el nombre del schema directamente en el SQL. Es seguro aquí porque el valor proviene de un enum propio, nunca de input externo ⚠️.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;📊 Resultado&lt;/h2&gt;
&lt;p&gt;Esta migración debe ser la &lt;strong&gt;primera&lt;/strong&gt; en registrarse ☝️. Antes de crear cualquier tabla, los espacios de nombres tienen que existir. En el runner de migraciones, el orden queda así:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-swift"&gt;fluent.migrations.add(
    SchemasMigration()
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Al ejecutar las migraciones en un entorno nuevo, el proceso completo es automático ⚡:&lt;/p&gt;
&lt;p&gt;Y si en algún momento necesitas añadir un nuevo dominio, solo añades el caso al enum &lt;span class="high"&gt;SRSchema&lt;/span&gt; y la próxima vez que se ejecuten las migraciones, el esquema aparece. &lt;strong&gt;Sin tocar la base de datos a mano, sin documentación extra que mantener&lt;/strong&gt; 🎯.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Keep coding, keep running&lt;/strong&gt; 🏃‍♂️&lt;/p&gt;
&lt;hr /&gt;</content>
    <link rel="alternate" href="https://jcalderita.com/es/blog/migrate-spaces/"></link>
    <media:content medium="image" url="https://jcalderita.com/static/blog/MigrateSpaces.webp"></media:content>
  </entry>
  <entry>
    <id>https://jcalderita.com/es/blog/tailwind-to-css/</id>
    <title>Tailwind to CSS</title>
    <updated>2026-03-07T00:00:00Z</updated>
    <summary>Por qué migré mi portfolio de Tailwind CSS a vanilla CSS y qué ventajas obtuve en rendimiento, peso y control total del código.</summary>
    <content type="html">&lt;hr /&gt;
&lt;h2&gt;🎨 ¿Por Qué Tocar Lo Que Funciona?&lt;/h2&gt;
&lt;p&gt;Mi portfolio llevaba meses funcionando perfectamente con &lt;span class="high"&gt;Tailwind CSS&lt;/span&gt;. Todo iba bien. Los estilos estaban en su sitio, los componentes se veían geniales y la web cargaba rápido.&lt;/p&gt;
&lt;p&gt;Entonces, ¿por qué cambiar?&lt;/p&gt;
&lt;p&gt;Porque &lt;strong&gt;funcionar bien&lt;/strong&gt; y &lt;strong&gt;funcionar de la mejor manera posible&lt;/strong&gt; son cosas distintas. Y cuando te paras a analizar qué hay debajo del capó, a veces descubres que llevas una capa que &lt;span class="high"&gt;no necesitas&lt;/span&gt; 🧅.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;🤔 El Problema con Tailwind (en mi caso)&lt;/h2&gt;
&lt;p&gt;No me malinterpretéis: &lt;span class="high"&gt;Tailwind CSS&lt;/span&gt; es una herramienta &lt;strong&gt;brutal&lt;/strong&gt;. Lo he usado en proyectos profesionales y lo seguiré usando donde tenga sentido. Pero en un portfolio personal con &lt;span class="high"&gt;Astro&lt;/span&gt;, empecé a notar ciertas cosas:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Dependencia innecesaria&lt;/strong&gt;: 3 paquetes extra (&lt;span class="high"&gt;tailwindcss&lt;/span&gt;, &lt;span class="high"&gt;@tailwindcss/vite&lt;/span&gt;, &lt;span class="high"&gt;@tailwindcss/typography&lt;/span&gt;) para un proyecto que no los necesitaba realmente 📦&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Capa de abstracción&lt;/strong&gt;: Tailwind genera CSS a partir de clases de utilidad. Es una capa entre lo que escribes y lo que el navegador interpreta. En un proyecto pequeño, esa capa &lt;strong&gt;suma sin aportar&lt;/strong&gt; 🧱&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Peso extra&lt;/strong&gt;: El CSS generado incluía utilidades que no siempre aprovechaba al máximo. ~20KB de más que el usuario descargaba sin necesidad 📊&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Menor control&lt;/strong&gt;: Cuando quieres algo muy específico, acabas luchando contra el framework en vez de escribir directamente lo que necesitas ⚔️&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;💡 La Decisión: Vanilla CSS con Design Tokens&lt;/h2&gt;
&lt;p&gt;La idea era sencilla: &lt;strong&gt;eliminar Tailwind&lt;/strong&gt; y sustituirlo por &lt;span class="high"&gt;CSS vanilla&lt;/span&gt; con un sistema de &lt;strong&gt;design tokens&lt;/strong&gt; usando &lt;span class="high"&gt;custom properties&lt;/span&gt; de CSS.&lt;/p&gt;
&lt;p&gt;¿Qué son los design tokens? Son variables CSS que definen tu sistema de diseño:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-css"&gt;:root {
  --color-primary: oklch(0.55 0.2 260);
  --color-gray-100: oklch(0.97 0 0);
  --color-gray-900: oklch(0.21 0.006 285.75);

  --text-sm: 0.875rem;
  --text-base: 1rem;
  --text-xl: 1.25rem;

  --spacing: 0.25rem;
  --radius-lg: 0.5rem;

  --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1);
  --transition-colors: color 0.15s, background-color 0.15s, border-color 0.15s;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Con esto tienes &lt;strong&gt;consistencia&lt;/strong&gt; en todo el proyecto sin depender de ningún framework. Solo CSS puro que el navegador entiende &lt;span class="high"&gt;directamente&lt;/span&gt; 🎯.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;🔧 La Migración&lt;/h2&gt;
&lt;p&gt;El proceso fue migrar &lt;strong&gt;38 archivos&lt;/strong&gt; en un solo commit. Cada componente &lt;span class="high"&gt;Astro&lt;/span&gt; pasó de usar clases de Tailwind a tener su propio bloque &lt;span class="high"&gt;&amp;lt;style&amp;gt;&lt;/span&gt; con estilos scoped:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Antes (Tailwind):&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-astro"&gt;&amp;lt;header class=&amp;quot;flex items-center justify-between px-6 py-4 bg-white dark:bg-gray-900&amp;quot;&amp;gt;
  &amp;lt;nav class=&amp;quot;flex gap-4&amp;quot;&amp;gt;
    &amp;lt;a class=&amp;quot;text-sm font-medium text-gray-700 hover:text-blue-500&amp;quot;&amp;gt;Blog&amp;lt;/a&amp;gt;
  &amp;lt;/nav&amp;gt;
&amp;lt;/header&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Después (Vanilla CSS):&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-astro"&gt;&amp;lt;header class=&amp;quot;header&amp;quot;&amp;gt;
  &amp;lt;nav class=&amp;quot;nav&amp;quot;&amp;gt;
    &amp;lt;a class=&amp;quot;nav-link&amp;quot;&amp;gt;Blog&amp;lt;/a&amp;gt;
  &amp;lt;/nav&amp;gt;
&amp;lt;/header&amp;gt;

&amp;lt;style&amp;gt;
  .header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: calc(var(--spacing) * 4) calc(var(--spacing) * 6);
    background-color: var(--color-white);
  }

  :global(.dark) .header {
    background-color: var(--color-gray-900);
  }

  .nav {
    display: flex;
    gap: calc(var(--spacing) * 4);
  }

  .nav-link {
    font-size: var(--text-sm);
    font-weight: 500;
    color: var(--color-gray-700);
    transition: var(--transition-colors);
  }

  .nav-link:hover {
    color: var(--color-blue-500);
  }
&amp;lt;/style&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;¿Más líneas? Sí. ¿Más claro y mantenible? &lt;strong&gt;Absolutamente&lt;/strong&gt; ✅.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;📚 Lo Que Aprendí por el Camino&lt;/h2&gt;
&lt;p&gt;La migración no fue solo “quitar Tailwind y poner CSS”. Hubo trampas interesantes que vale la pena compartir:&lt;/p&gt;
&lt;h3&gt;Estilos scoped y &lt;span class="high"&gt;&amp;lt;slot&amp;gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;En &lt;span class="high"&gt;Astro&lt;/span&gt;, los estilos dentro de &lt;span class="high"&gt;&amp;lt;style&amp;gt;&lt;/span&gt; son &lt;strong&gt;scoped&lt;/strong&gt; por defecto. Esto significa que cada componente recibe un atributo único (&lt;span class="high"&gt;data-astro-cid-*&lt;/span&gt;) y los estilos solo afectan a ese componente.&lt;/p&gt;
&lt;p&gt;El problema: el contenido que llega por &lt;span class="high"&gt;&amp;lt;slot&amp;gt;&lt;/span&gt; &lt;strong&gt;no recibe&lt;/strong&gt; ese atributo. Si un componente padre intenta estilizar lo que viene por slot, los estilos no se aplican 😱.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;La solución&lt;/strong&gt;: usar &lt;span class="high"&gt;:global()&lt;/span&gt; para los selectores que afectan a contenido slotted:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-css"&gt;.container :global(a) {
  color: var(--color-primary);
  text-decoration: underline;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;span class="high"&gt;opacity&lt;/span&gt; vs transparencia de fondo&lt;/h3&gt;
&lt;p&gt;Con Tailwind usaba clases como &lt;span class="high"&gt;bg-opacity-70&lt;/span&gt;. Al migrar, el primer instinto fue usar la propiedad &lt;span class="high"&gt;opacity&lt;/span&gt;. &lt;strong&gt;Error&lt;/strong&gt;: &lt;span class="high"&gt;opacity&lt;/span&gt; afecta al elemento entero, &lt;strong&gt;incluidos sus hijos&lt;/strong&gt; 👶.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;La solución correcta&lt;/strong&gt;: &lt;span class="high"&gt;color-mix()&lt;/span&gt; para transparencia solo en el fondo:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-css"&gt;.modal-overlay {
  /* MAL: afecta a todo */
  opacity: 0.7;

  /* BIEN: solo el fondo es transparente */
  background-color: color-mix(in oklab, var(--color-gray-900) 70%, transparent);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;📊 Los Resultados&lt;/h2&gt;
&lt;p&gt;Los números hablan por sí solos:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;~20KB menos&lt;/strong&gt; de CSS entregado al navegador 📉&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;3 dependencias eliminadas&lt;/strong&gt; del &lt;code&gt;package.json&lt;/code&gt; 🗑️&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;0 capas de abstracción&lt;/strong&gt; entre tu código y el navegador 🎯&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Build más rápido&lt;/strong&gt; al no necesitar el procesamiento de Tailwind ⚡&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Control total&lt;/strong&gt; sobre cada línea de CSS que se genera 🎛️&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;El &lt;span class="high"&gt;package.json&lt;/span&gt; pasó de tener &lt;span class="high"&gt;tailwindcss&lt;/span&gt;, &lt;span class="high"&gt;@tailwindcss/vite&lt;/span&gt; y &lt;span class="high"&gt;@tailwindcss/typography&lt;/span&gt; a &lt;strong&gt;no tener ninguna dependencia de estilos&lt;/strong&gt;. Solo CSS puro.&lt;/p&gt;
&lt;p&gt;Y lo mejor: se eliminó el archivo &lt;span class="high"&gt;tailwind.config.mjs&lt;/span&gt; por completo. &lt;span class="high"&gt;Una configuración menos&lt;/span&gt; que mantener 🧹.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;🏗️ La Arquitectura Final&lt;/h2&gt;
&lt;p&gt;El sistema de estilos quedó organizado en 4 archivos CSS:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Archivo&lt;/th&gt;
&lt;th&gt;Responsabilidad&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span class="high"&gt;design-tokens.css&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;Colores, tipografía, espaciado, sombras, transiciones&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span class="high"&gt;base-reset.css&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;Reset CSS mínimo y utilidades como &lt;span class="high"&gt;.sr-only&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span class="high"&gt;prose.css&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;Tipografía para contenido del blog con soporte dark mode&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span class="high"&gt;layout.css&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;Clases de layout globales (&lt;span class="high"&gt;.bodyLayout&lt;/span&gt;, &lt;span class="high"&gt;.mainLayout&lt;/span&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Todo importado desde un único &lt;span class="high"&gt;global.css&lt;/span&gt;. Limpio, predecible y sin magia negra 🧙‍♂️.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;💭 Reflexión Final&lt;/h2&gt;
&lt;p&gt;Migrar de &lt;span class="high"&gt;Tailwind CSS&lt;/span&gt; a &lt;span class="high"&gt;vanilla CSS&lt;/span&gt; no es para todos ni para todos los proyectos. En equipos grandes o proyectos con muchos desarrolladores, Tailwind sigue siendo una &lt;strong&gt;opción fantástica&lt;/strong&gt; por su consistencia y velocidad de desarrollo.&lt;/p&gt;
&lt;p&gt;Pero en un proyecto personal como un portfolio con &lt;span class="high"&gt;Astro&lt;/span&gt;, donde el rendimiento importa y el control total es un lujo que puedes permitirte, &lt;strong&gt;quitarte esa capa de abstracción&lt;/strong&gt; es liberador 🕊️. De hecho, acabé llevando esta filosofía aún más lejos y migré todo el portfolio de Astro a Swift — cuento la historia completa en &lt;a href="/es/blog/astro-to-saga/"&gt;Astro to Saga&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Es como pintar un cuadro: puedes usar plantillas y herramientas que te guíen, o puedes coger el pincel y crear &lt;strong&gt;exactamente&lt;/strong&gt; lo que tienes en mente. Ambas opciones son válidas. Pero cuando el lienzo es tuyo, &lt;span class="high"&gt;pintar a mano tiene su encanto&lt;/span&gt; 🎨.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Keep coding, keep running&lt;/strong&gt; 🏃‍♂️&lt;/p&gt;
&lt;hr /&gt;</content>
    <link href="https://jcalderita.com/es/blog/tailwind-to-css/" rel="alternate"></link>
    <media:content url="https://jcalderita.com/static/blog/TailwindToCSS.webp" medium="image"></media:content>
  </entry>
  <entry>
    <id>https://jcalderita.com/es/blog/cupertino-mcp/</id>
    <title>Cupertino MCP</title>
    <updated>2026-02-11T00:00:00Z</updated>
    <summary>Descubre Cupertino MCP, la herramienta que integra toda la documentación de Apple directamente en tu IA para desarrollo iOS/macOS.</summary>
    <content type="html">&lt;hr /&gt;
&lt;h2&gt;🍎 El Problema de la Documentación de Apple&lt;/h2&gt;
&lt;p&gt;Si desarrollas para &lt;strong&gt;iOS, macOS o cualquier plataforma de Apple&lt;/strong&gt;, conoces el drill perfecto:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Estás programando tranquilamente 💻&lt;/li&gt;
&lt;li&gt;Necesitas consultar &lt;strong&gt;cómo funciona algo en SwiftUI&lt;/strong&gt; 🤔&lt;/li&gt;
&lt;li&gt;Abres Safari / tu navegador favorito 🌐&lt;/li&gt;
&lt;li&gt;Buscas en Google / DuckDuckGo 🔍&lt;/li&gt;
&lt;li&gt;Entras a la documentación oficial de Apple 📚&lt;/li&gt;
&lt;li&gt;Lees, entiendes, vuelves a tu IDE 🔄&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Repites el proceso 47 veces al día&lt;/strong&gt; 😵&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;O peor aún: &lt;strong&gt;le preguntas a tu IA favorita&lt;/strong&gt; y te da información &lt;span class="high"&gt;desactualizada&lt;/span&gt; o directamente &lt;span class="high"&gt;alucinada&lt;/span&gt; porque su conocimiento no está actualizado con las últimas versiones de Swift o SwiftUI 🤖❌.&lt;/p&gt;
&lt;p&gt;¿Y si te dijera que hay una forma mejor?&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;💡 Cupertino MCP: La Solución&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/mihaelamj/cupertino"&gt;Cupertino MCP&lt;/a&gt;&lt;/strong&gt; es una herramienta que &lt;span class="high"&gt;indexa localmente toda la documentación de Apple&lt;/span&gt; y la pone disponible para tu IA mediante el &lt;strong&gt;Model Context Protocol (MCP)&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Traducido a cristiano: es como darle a tu IA (Claude, en mi caso) un &lt;strong&gt;acceso directo y verificado&lt;/strong&gt; a toda la documentación oficial de Apple 🎯.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;¿Cuánta documentación estamos hablando?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;📄 &lt;strong&gt;302,424+ páginas de documentación&lt;/strong&gt; oficial&lt;br /&gt;&lt;br /&gt;
🧰 &lt;strong&gt;307 frameworks&lt;/strong&gt; indexados&lt;br /&gt;&lt;br /&gt;
📦 &lt;strong&gt;9,699 paquetes Swift&lt;/strong&gt; catalogados&lt;br /&gt;&lt;br /&gt;
💾 &lt;strong&gt;606 proyectos de código de ejemplo&lt;/strong&gt; de Apple&lt;br /&gt;&lt;br /&gt;
📱 &lt;strong&gt;Human Interface Guidelines&lt;/strong&gt; completas&lt;br /&gt;&lt;br /&gt;
📖 &lt;strong&gt;Swift Evolution proposals&lt;/strong&gt; (~400 propuestas)&lt;br /&gt;&lt;br /&gt;
🏛️ &lt;strong&gt;Apple Archive guides&lt;/strong&gt; (documentación legacy pero valiosa)&lt;/p&gt;
&lt;p&gt;Todo esto, &lt;strong&gt;disponible offline&lt;/strong&gt;, sin necesidad de internet, y &lt;span class="high"&gt;sin riesgo de alucinaciones&lt;/span&gt; de la IA 🛡️.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;🎯 Precisión Absoluta&lt;/h2&gt;
&lt;p&gt;Cuando Claude me responde sobre APIs de Apple, &lt;strong&gt;ya no adivina&lt;/strong&gt;. Busca en la documentación oficial real y me da información &lt;span class="high"&gt;100% verificada&lt;/span&gt;.&lt;/p&gt;
&lt;p&gt;Nada de: &lt;em&gt;“Creo que en SwiftUI 6 se usa así…”&lt;/em&gt; ❌&lt;/p&gt;
&lt;p&gt;Ahora es: &lt;em&gt;“Según la documentación oficial de SwiftUI 6…”&lt;/em&gt; ✅&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;⚡ Velocidad de Desarrollo&lt;/h2&gt;
&lt;p&gt;Antes:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Pregunta → Espera respuesta → Duda → Abrir Safari →
Buscar en Google → Leer docs → Volver al IDE → Implementar
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Ahora:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Pregunta → Respuesta con documentación oficial → Implementar
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;span class="high"&gt;Ahorro brutal de tiempo&lt;/span&gt; en cada consulta 🏎️.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;📊 Búsqueda Súper Potente&lt;/h2&gt;
&lt;p&gt;Cupertino usa &lt;strong&gt;SQLite FTS5 con ranking BM25&lt;/strong&gt;. En cristiano: búsquedas &lt;span class="high"&gt;ultra-rápidas&lt;/span&gt; (menos de 100ms) con resultados relevantes ordenados por importancia.&lt;/p&gt;
&lt;p&gt;Puedes filtrar por:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Framework específico 🧰&lt;/li&gt;
&lt;li&gt;Versión de plataforma (iOS 17, macOS 14, etc.) 📱&lt;/li&gt;
&lt;li&gt;Tipo de documentación (API, ejemplos, guías) 📚&lt;/li&gt;
&lt;li&gt;Búsqueda en código de ejemplo 💻&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;🧠 Aprendizaje Contextualizado&lt;/h2&gt;
&lt;p&gt;No solo obtengo &lt;strong&gt;la API correcta&lt;/strong&gt;, también obtengo:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Ejemplos de código reales de Apple 📝&lt;/li&gt;
&lt;li&gt;Mejores prácticas documentadas 👍&lt;/li&gt;
&lt;li&gt;Patrones de diseño oficiales 🎨&lt;/li&gt;
&lt;li&gt;Alternativas y deprecaciones ⚠️&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Es como tener &lt;strong&gt;un mentor de Apple dentro de Claude&lt;/strong&gt; 🍎🤝🤖.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;💭 Reflexión Final&lt;/h2&gt;
&lt;p&gt;Cupertino MCP es un &lt;strong&gt;ejemplo perfecto&lt;/strong&gt; de cómo las herramientas de IA se vuelven &lt;strong&gt;realmente útiles&lt;/strong&gt; cuando tienen acceso a &lt;span class="high"&gt;información verificada y actualizada&lt;/span&gt;.&lt;/p&gt;
&lt;p&gt;No se trata de darle más poder a la IA para que “adivine mejor”. Se trata de &lt;span class="high"&gt;conectarla con fuentes fiables&lt;/span&gt; para que te dé respuestas precisas.&lt;/p&gt;
&lt;p&gt;Si desarrollas para ecosistemas de Apple, Cupertino MCP es una &lt;strong&gt;inversión de 5 minutos&lt;/strong&gt; (instalación) que te ahorrará &lt;strong&gt;horas&lt;/strong&gt; cada semana. Y si usas Claude Code como yo, la experiencia se vuelve &lt;strong&gt;aún mejor&lt;/strong&gt; 💎.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;¿Lo recomiendo?&lt;/strong&gt; Absolutamente. Es &lt;strong&gt;obligatorio&lt;/strong&gt; si desarrollas con Swift, SwiftUI o cualquier framework de Apple 🍎.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;¿Es para todo el mundo?&lt;/strong&gt; Solo si desarrollas para plataformas Apple. Si trabajas con otras tecnologías, busca herramientas similares (seguro hay equivalentes para Android, web, etc.) 🔍.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Keep coding, keep running&lt;/strong&gt; 🏃‍♂️&lt;/p&gt;
&lt;hr /&gt;</content>
    <link href="https://jcalderita.com/es/blog/cupertino-mcp/" rel="alternate"></link>
    <media:content url="https://jcalderita.com/static/blog/CupertinoMCP.webp" medium="image"></media:content>
  </entry>
  <entry>
    <id>https://jcalderita.com/es/blog/which-ai/</id>
    <title>Which AI</title>
    <updated>2026-02-04T00:00:00Z</updated>
    <summary>Cómo lidiar con la fatiga de herramientas de IA como desarrollador: por qué perseguir cada nuevo modelo es imposible y cómo elegir sin caer en el FOMO.</summary>
    <content type="html">&lt;hr /&gt;
&lt;h2&gt;🎭 Déjà Vu: Los Frameworks Web 2.0&lt;/h2&gt;
&lt;p&gt;¿Recuerdan aquella época dorada donde cada semana aparecía un &lt;span class="high"&gt;nuevo framework web&lt;/span&gt;?&lt;/p&gt;
&lt;p&gt;Uno más &lt;strong&gt;moderno&lt;/strong&gt;, más &lt;strong&gt;rápido&lt;/strong&gt;, más &lt;strong&gt;potente&lt;/strong&gt;, más &lt;strong&gt;bonito&lt;/strong&gt;… Uno que venía a &lt;span class="high"&gt;“matar” a todos los anteriores&lt;/span&gt; y que iba a convertirse en el &lt;span class="high"&gt;único framework que necesitarías usar jamás&lt;/span&gt; 💀.&lt;/p&gt;
&lt;p&gt;Spoiler: ninguno mató a nadie, y ahora tenemos más frameworks que nunca 😂.&lt;/p&gt;
&lt;p&gt;Pues bien, &lt;span class="high"&gt;bienvenidos a la era de las IAs&lt;/span&gt;: el mismo concepto, pero con esteroides 🚀.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;📱 La Fatiga de las Notificaciones&lt;/h2&gt;
&lt;p&gt;Imagina la escena: estás tranquilamente &lt;strong&gt;programando con tu IA de confianza&lt;/strong&gt;, flujo de trabajo perfecto, productividad al máximo… y de repente:&lt;/p&gt;
&lt;p&gt;📢 &lt;strong&gt;Notificación:&lt;/strong&gt; &lt;em&gt;”¡Nueva IA revolucionaria lanzada!”&lt;/em&gt;&lt;br /&gt;&lt;br /&gt;
🎥 &lt;strong&gt;Video:&lt;/strong&gt; &lt;em&gt;“Cómo usarla en 10 minutos”&lt;/em&gt;&lt;br /&gt;&lt;br /&gt;
📝 &lt;strong&gt;Tutorial:&lt;/strong&gt; &lt;em&gt;“Integrala en tu workflow HOY”&lt;/em&gt;&lt;br /&gt;&lt;br /&gt;
💡 &lt;strong&gt;Thread:&lt;/strong&gt; &lt;em&gt;“Por qué deberías cambiarte ahora mismo”&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Y aquí viene lo &lt;strong&gt;más divertido&lt;/strong&gt;: el creador de contenido que hace 1 hora tenía un video explicando &lt;strong&gt;“Por qué la antigua IA es el futuro”&lt;/strong&gt;, ahora tiene otro video sobre &lt;strong&gt;“Por qué la nueva IA es mejor que todo”&lt;/strong&gt; 🎪.&lt;/p&gt;
&lt;p&gt;No los culpo, eh. Es su trabajo mantenerse actualizados y crear contenido relevante. Pero como consumidor… &lt;strong&gt;es agotador&lt;/strong&gt; 😅.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;🏃‍♂️ La Carrera Imposible&lt;/h2&gt;
&lt;p&gt;Si ya es &lt;strong&gt;complicado seguir las actualizaciones diarias&lt;/strong&gt; de la IA que estás usando (y créeme, son &lt;strong&gt;muchísimas&lt;/strong&gt; actualizaciones), imagínate ponerte a:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Probar cada nueva IA que sale ✅&lt;/li&gt;
&lt;li&gt;Aprender su interfaz y peculiaridades 📚&lt;/li&gt;
&lt;li&gt;Integrarla en tu workflow 🔧&lt;/li&gt;
&lt;li&gt;Compararla con las que ya usas ⚖️&lt;/li&gt;
&lt;li&gt;Decidir si vale la pena el cambio 🤔&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;span class="high"&gt;Resultado:&lt;/span&gt; Misión imposible 🎯.&lt;/p&gt;
&lt;p&gt;Y no me malinterpreten: &lt;strong&gt;me encanta tener opciones&lt;/strong&gt;. De hecho, es &lt;strong&gt;maravilloso&lt;/strong&gt; que exista tanta competencia e innovación. Más opciones = más mejor 👍🏻.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;🚫 Mi Línea Roja&lt;/h2&gt;
&lt;p&gt;Ahora bien, hay algo en lo que soy &lt;strong&gt;tajante&lt;/strong&gt;:&lt;/p&gt;
&lt;p&gt;&lt;span class="high"&gt;Jamás usaré una IA que requiera permisos totales desde el principio&lt;/span&gt; 🔒.&lt;/p&gt;
&lt;p&gt;Nada de &lt;em&gt;“Dame acceso completo a tu máquina y confía en mí”&lt;/em&gt;. &lt;strong&gt;Eso no va conmigo&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Mi filosofía es clara:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;📋 &lt;strong&gt;Dime qué vas a hacer&lt;/strong&gt; (o planifícalo)&lt;/li&gt;
&lt;li&gt;👀 &lt;strong&gt;Yo reviso los cambios&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Yo apruebo&lt;/strong&gt; (o no apruebo)&lt;/li&gt;
&lt;li&gt;🚀 &lt;strong&gt;Entonces procedes&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;No al libre albedrío de la IA con permisos de administrador. &lt;span class="high"&gt;Control total&lt;/span&gt; es mi mantra, como ya conté en mi artículo sobre &lt;a href="/es/blog/claude-code/"&gt;Claude Code&lt;/a&gt; 💪.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;🌐 Ya No Es Solo un Modelo&lt;/h2&gt;
&lt;p&gt;Y aquí está lo &lt;strong&gt;realmente fascinante&lt;/strong&gt;: la IA ya no es simplemente &lt;em&gt;“un modelo”&lt;/em&gt; o &lt;em&gt;“una app”&lt;/em&gt; que instalas y usas.&lt;/p&gt;
&lt;p&gt;Ahora tenemos &lt;strong&gt;ecosistemas completos&lt;/strong&gt; 🏗️:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Modelos base + fine-tuned ⚙️&lt;/li&gt;
&lt;li&gt;Integraciones con IDEs 💻&lt;/li&gt;
&lt;li&gt;Plugins y extensiones 🔌&lt;/li&gt;
&lt;li&gt;APIs y SDKs 📡&lt;/li&gt;
&lt;li&gt;Plataformas de orquestación 🎼&lt;/li&gt;
&lt;li&gt;Herramientas especializadas 🛠️&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Es &lt;strong&gt;increíble&lt;/strong&gt; la velocidad a la que todo esto está evolucionando. Lo que era ciencia ficción hace 2 años, ahora es tu &lt;span class="high"&gt;asistente de código diario&lt;/span&gt; 🤖.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;🎢 Viviendo una Época Increíble&lt;/h2&gt;
&lt;p&gt;Al final del día, creo que estamos viviendo algo &lt;span class="high"&gt;histórico&lt;/span&gt; 🎊:&lt;/p&gt;
&lt;p&gt;🥊 &lt;strong&gt;Batalla de los IDEs&lt;/strong&gt;: VSCode vs. Cursor vs. WindSurf vs. Zed vs…&lt;br /&gt;&lt;br /&gt;
⚔️ &lt;strong&gt;Batalla de los Frameworks&lt;/strong&gt;: React vs. Vue vs. Svelte vs. Solid vs…&lt;br /&gt;&lt;br /&gt;
🤖 &lt;strong&gt;Batalla de las IAs&lt;/strong&gt;: Claude vs. ChatGPT vs. Gemini vs. Copilot vs…&lt;/p&gt;
&lt;p&gt;Es &lt;strong&gt;agotador&lt;/strong&gt; intentar seguirlo todo, lo admito. A mí personalmente me resulta &lt;span class="high"&gt;imposible&lt;/span&gt; 🙃.&lt;/p&gt;
&lt;p&gt;Pero también es &lt;strong&gt;emocionante&lt;/strong&gt;. Cada día hay algo nuevo que aprender, algo que probar, algo que te hace replantear cómo trabajas.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;🎯 Reflexión Final&lt;/h2&gt;
&lt;p&gt;La moraleja de todo esto es simple:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;No necesitas usar todo&lt;/strong&gt;. De verdad, no lo necesitas 🙅‍♂️.&lt;/p&gt;
&lt;p&gt;Encuentra &lt;strong&gt;las herramientas que funcionen para ti&lt;/strong&gt;, aprende a usarlas bien, mantente actualizado con &lt;strong&gt;sus&lt;/strong&gt; novedades, y está bien que de vez en cuando explores alternativas.&lt;/p&gt;
&lt;p&gt;Pero no te vuelvas loco intentando estar en &lt;strong&gt;todas las olas a la vez&lt;/strong&gt;. Elige tu tabla de surf 🏄‍♂️, aprende a usarla bien, y disfruta del viaje.&lt;/p&gt;
&lt;p&gt;Y si un día decides cambiar de tabla, pues adelante. Pero que sea una &lt;strong&gt;decisión consciente&lt;/strong&gt;, no una reacción al &lt;span class="high"&gt;FOMO&lt;/span&gt; (&lt;em&gt;Fear Of Missing Out&lt;/em&gt;) creado por 47 notificaciones simultáneas 😂.&lt;/p&gt;
&lt;p&gt;&lt;span class="high"&gt;Feliz coding&lt;/span&gt; (con la IA que prefieras) 🚀&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Keep coding, keep running&lt;/strong&gt; 🏃‍♂️&lt;/p&gt;
&lt;hr /&gt;</content>
    <link href="https://jcalderita.com/es/blog/which-ai/" rel="alternate"></link>
    <media:content medium="image" url="https://jcalderita.com/static/blog/WhichAI.webp"></media:content>
  </entry>
  <entry>
    <id>https://jcalderita.com/es/blog/conferences-25/</id>
    <title>Conferences 25</title>
    <updated>2026-01-28T00:00:00Z</updated>
    <summary>Lista curada de las mejores conferencias de iOS, Swift y Apple en 2025 con fechas, ubicaciones y enlaces para ver todas las charlas gratis.</summary>
    <content type="html">&lt;hr /&gt;
&lt;h2&gt;iOSConfSG&lt;/h2&gt;
&lt;p&gt;📅 15-17 de enero&lt;br /&gt;&lt;br /&gt;
📍 Singapur, 🇸🇬&lt;br /&gt;&lt;br /&gt;
🔗 &lt;a href="https://2025.iosconf.sg/"&gt;iosconf&lt;/a&gt;&lt;br /&gt;&lt;br /&gt;
🎥 &lt;a href="https://youtube.com/watch?v=N1H9lvHwQxc&amp;amp;list=PLED4k3CZkY9RBltAgj-o9xSFOMOhBdmXm"&gt;youtube&lt;/a&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;AppDevCon&lt;/h2&gt;
&lt;p&gt;📅 18–21 de marzo&lt;br /&gt;&lt;br /&gt;
📍 Ámsterdam, 🇳🇱&lt;br /&gt;&lt;br /&gt;
🔗 &lt;a href="https://appdevcon.nl"&gt;appdevcon&lt;/a&gt;&lt;br /&gt;&lt;br /&gt;
🎥 &lt;a href="https://appdevcon.nl/videos/"&gt;videos&lt;/a&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Swift Heroes&lt;/h2&gt;
&lt;p&gt;📅 8–9 de abril&lt;br /&gt;&lt;br /&gt;
📍 Turín, 🇮🇹&lt;br /&gt;&lt;br /&gt;
🔗 &lt;a href="https://swiftheroes.com"&gt;swiftheroes&lt;/a&gt;&lt;br /&gt;&lt;br /&gt;
🎥 &lt;a href="https://youtube.com/watch?v=cVNtQK34xdw&amp;amp;list=PLfCiO1zYKkARXhFxrqv3WR6b6JEp4LaAN"&gt;youtube&lt;/a&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;try! Swift Tokyo&lt;/h2&gt;
&lt;p&gt;📅 9–11 de abril&lt;br /&gt;&lt;br /&gt;
📍 Tokio, 🇯🇵&lt;br /&gt;&lt;br /&gt;
🔗 &lt;a href="https://tryswift.jp/en"&gt;tryswift&lt;/a&gt;&lt;br /&gt;&lt;br /&gt;
🎥 &lt;a href="https://youtube.com/watch?v=qY09lmDo7GU&amp;amp;list=PLCl5NM4qD3u_Azg7gKw5CK_DqSLeb4QMY"&gt;youtube&lt;/a&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Deep Dish Swift&lt;/h2&gt;
&lt;p&gt;📅 27–29 de abril&lt;br /&gt;&lt;br /&gt;
📍 Chicago, 🇺🇸&lt;br /&gt;&lt;br /&gt;
🔗 &lt;a href="https://deepdishswift.com"&gt;deepdishswift&lt;/a&gt;&lt;br /&gt;&lt;br /&gt;
🎥 &lt;a href="https://youtube.com/watch?v=eN6NJR67HL4&amp;amp;list=PLGLg44jP5U-4l1OERkMTPrzgG7nQkGGYy"&gt;youtube&lt;/a&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;iOSKonf&lt;/h2&gt;
&lt;p&gt;📅 13–15 de mayo&lt;br /&gt;&lt;br /&gt;
📍 Skopje, 🇲🇰&lt;br /&gt;&lt;br /&gt;
🔗 &lt;a href="https://ioskonf.mk"&gt;ioskonf&lt;/a&gt;&lt;br /&gt;&lt;br /&gt;
🎥 &lt;a href="https://youtube.com/watch?v=AIvCc1v5F4g&amp;amp;list=PLVKQDFwOy1XZXhFOdWfXKdjCz4r_LBeto"&gt;youtube&lt;/a&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Swift Craft&lt;/h2&gt;
&lt;p&gt;📅 19–21 de mayo&lt;br /&gt;&lt;br /&gt;
📍 Kent, 🇬🇧&lt;br /&gt;&lt;br /&gt;
🔗 &lt;a href="https://swiftcraft.uk"&gt;swiftcraft&lt;/a&gt;&lt;br /&gt;&lt;br /&gt;
🎥 &lt;a href="https://youtube.com/watch?v=XR4XrVvHUQo&amp;amp;list=PLugrLwuQvERolaa2XA0dl4UK9Z8XEwiAA"&gt;youtube&lt;/a&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;WWDC&lt;/h2&gt;
&lt;p&gt;📅 9–13 de junio&lt;br /&gt;&lt;br /&gt;
📍 Cupertino, 🇺🇸&lt;br /&gt;&lt;br /&gt;
🔗 &lt;a href="https://developer.apple.com/wwdc25"&gt;apple&lt;/a&gt;&lt;br /&gt;&lt;br /&gt;
🎥 &lt;a href="https://youtube.com/watch?v=IrGYUq1mklk&amp;amp;list=PLjODKV8YBFHZKEn1wsUCL1n-q7tzysEBM"&gt;youtube&lt;/a&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;NSSpain&lt;/h2&gt;
&lt;p&gt;📅 17-19 septiembre&lt;br /&gt;&lt;br /&gt;
📍 Logroño, 🇪🇸&lt;br /&gt;&lt;br /&gt;
🔗 &lt;a href="https://nsspain.com"&gt;nsspain&lt;/a&gt;&lt;br /&gt;&lt;br /&gt;
🎥 &lt;a href="https://youtube.com/watch?v=NjW7lxtZwIM&amp;amp;list=PLztE34GS_piKKQ6y1dkkuhW76jLBHm3NV"&gt;youtube&lt;/a&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Swift Connection&lt;/h2&gt;
&lt;p&gt;📅 6–7 de octubre&lt;br /&gt;&lt;br /&gt;
📍 París, 🇫🇷&lt;br /&gt;&lt;br /&gt;
🔗 &lt;a href="https://swiftconnection.io"&gt;swiftconnection&lt;/a&gt;&lt;br /&gt;&lt;br /&gt;
🎥 &lt;a href="https://youtube.com/watch?v=N05WEfGPp3M&amp;amp;list=PLZsRQnRG-mlIkHsjeax_cRq6kAclNrWBF"&gt;youtube&lt;/a&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;SwiftLeeds&lt;/h2&gt;
&lt;p&gt;📅 7–8 de octubre&lt;br /&gt;&lt;br /&gt;
📍 Leeds, 🇬🇧&lt;br /&gt;&lt;br /&gt;
🔗 &lt;a href="https://swiftleeds.co.uk"&gt;swiftleeds&lt;/a&gt;&lt;br /&gt;&lt;br /&gt;
🎥 &lt;a href="https://youtube.com/watch?v=iNiQHGKULQw&amp;amp;list=PL-wmxEeX64YTpDbpfszWMV76oZZO3wxZH"&gt;youtube&lt;/a&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Pragma Conference&lt;/h2&gt;
&lt;p&gt;📅 30–31 de octubre&lt;br /&gt;&lt;br /&gt;
📍 Bolonia, 🇮🇹&lt;br /&gt;&lt;br /&gt;
🔗 &lt;a href="https://pragmaconference.com"&gt;pragmaconference&lt;/a&gt;&lt;br /&gt;&lt;br /&gt;
🎥 &lt;a href="https://youtube.com/watch?v=7o6Fkj3buxM&amp;amp;list=PLAVm70iJlMuvTihK1OzK9S4Vzw_KO71b0"&gt;youtube&lt;/a&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Do iOS&lt;/h2&gt;
&lt;p&gt;📅 12-13 de noviembre&lt;br /&gt;&lt;br /&gt;
📍 Amsterdam, 🇳🇱&lt;br /&gt;&lt;br /&gt;
🔗 &lt;a href="https://do-ios.com"&gt;do-ios&lt;/a&gt;&lt;br /&gt;&lt;br /&gt;
🎥 &lt;a href="https://youtube.com/watch?v=q-Y44dh6sTo&amp;amp;list=PLJEA4wsA0WeSP_NB1LcqcA7tBjhcg1u4U"&gt;youtube&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Keep coding, keep running&lt;/strong&gt; 🏃‍♂️&lt;/p&gt;
&lt;hr /&gt;</content>
    <link rel="alternate" href="https://jcalderita.com/es/blog/conferences-25/"></link>
    <media:content medium="image" url="https://jcalderita.com/static/blog/Conferences25.webp"></media:content>
  </entry>
  <entry>
    <id>https://jcalderita.com/es/blog/async-concurrent-map/</id>
    <title>Async Concurrent Map</title>
    <updated>2026-01-21T00:00:00Z</updated>
    <summary>Como combinar procesamiento concurrente con chunking para optimizar el uso de recursos en operaciones asíncronas masivas.</summary>
    <content type="html">&lt;hr /&gt;
&lt;h2&gt;Problema&lt;/h2&gt;
&lt;p&gt;En aplicaciones backend con &lt;span class="high"&gt;Vapor&lt;/span&gt;, cuando procesamos &lt;strong&gt;grandes volúmenes de datos&lt;/strong&gt; de forma concurrente, nos enfrentamos a un dilema de optimización:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Procesamiento secuencial&lt;/strong&gt; con &lt;span class="high"&gt;asyncMap&lt;/span&gt;: garantiza control sobre los recursos, pero es &lt;strong&gt;lento&lt;/strong&gt; al procesar elementos uno a uno.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Procesamiento totalmente concurrente&lt;/strong&gt; con &lt;span class="high"&gt;concurrentMap&lt;/span&gt;: maximiza la velocidad, pero puede &lt;strong&gt;saturar recursos&lt;/strong&gt; al lanzar miles de tareas simultáneas.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Por ejemplo, al procesar 10,000 registros con llamadas a APIs externas:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;span class="high"&gt;asyncMap&lt;/span&gt;: 10,000 llamadas secuenciales → muy lento pero controlado.&lt;/li&gt;
&lt;li&gt;&lt;span class="high"&gt;concurrentMap&lt;/span&gt;: 10,000 llamadas simultáneas → muy rápido pero puede agotar conexiones/memoria.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Necesitamos una solución que &lt;strong&gt;combine ambos enfoques&lt;/strong&gt;: dividir el trabajo en grupos manejables y procesar cada grupo de forma concurrente, equilibrando velocidad y uso de recursos.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Solución&lt;/h2&gt;
&lt;p&gt;Extendemos &lt;span class="high"&gt;Collection&lt;/span&gt; con una función que combina &lt;strong&gt;chunking&lt;/strong&gt; (división en grupos) y &lt;strong&gt;procesamiento concurrente&lt;/strong&gt;, permitiendo configurar el tamaño de los grupos y timeout por chunk.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-swift"&gt;extension Collection where Element: Sendable {
    func asyncConcurrentMap&amp;lt;T: Sendable&amp;gt;(
        chunkSize: Int? = nil,
        timeout: Double? = nil,
        _ transform: @escaping @Sendable (Element) async throws -&amp;gt; T
    ) async throws -&amp;gt; [T] {
        guard let chunkSize else {
            return try await concurrentMap(transform)
        }

        return try await chunks(ofCount: chunkSize)
            .asyncMap(timeout: timeout) {
                try await $0.concurrentMap(transform)
            }.flatMap { $0 }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Puntos clave:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Si no se especifica &lt;span class="high"&gt;chunkSize&lt;/span&gt;, usa &lt;span class="high"&gt;concurrentMap&lt;/span&gt; puro (procesamiento totalmente concurrente).&lt;/li&gt;
&lt;li&gt;Si se especifica &lt;span class="high"&gt;chunkSize&lt;/span&gt;, divide la colección en grupos con &lt;span class="high"&gt;chunks(ofCount:)&lt;/span&gt;.&lt;/li&gt;
&lt;li&gt;Procesa los &lt;strong&gt;chunks secuencialmente&lt;/strong&gt; con &lt;span class="high"&gt;asyncMap&lt;/span&gt; (con timeout opcional).&lt;/li&gt;
&lt;li&gt;Dentro de cada chunk, procesa los elementos &lt;strong&gt;concurrentemente&lt;/strong&gt; con &lt;span class="high"&gt;concurrentMap&lt;/span&gt;.&lt;/li&gt;
&lt;li&gt;Aplana los resultados con &lt;span class="high"&gt;flatMap&lt;/span&gt; para devolver un array unificado.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;Resultado&lt;/h2&gt;
&lt;pre&gt;&lt;code class="language-swift"&gt;// Procesar 10,000 registros en chunks de 100
// 100 tareas concurrentes a la vez, 100 veces
let results = try await records.asyncConcurrentMap(
    chunkSize: 100,
    timeout: 30.0
) { record in
    try await apiClient.process(record)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Beneficios de esta aproximación:&lt;/p&gt;
&lt;p&gt;⚡ &lt;strong&gt;Balance perfecto&lt;/strong&gt;: combina la velocidad del procesamiento concurrente con el control del procesamiento secuencial por chunks.&lt;br /&gt;&lt;br /&gt;
🎯 &lt;strong&gt;Control de recursos&lt;/strong&gt;: limita la cantidad de tareas simultáneas al tamaño del chunk, evitando saturación.&lt;br /&gt;&lt;br /&gt;
⏱️ &lt;strong&gt;Timeout por chunk&lt;/strong&gt;: detecta y maneja chunks problemáticos sin bloquear todo el procesamiento.&lt;br /&gt;&lt;br /&gt;
🔧 &lt;strong&gt;Flexibilidad total&lt;/strong&gt;: usa chunking cuando lo necesites, o procesamiento concurrente puro cuando no.&lt;br /&gt;&lt;br /&gt;
📊 &lt;strong&gt;Escalabilidad&lt;/strong&gt;: permite procesar millones de registros ajustando el tamaño del chunk según los recursos disponibles.&lt;/p&gt;
&lt;p&gt;Si necesitas ejecución paralela pura, lo cubrí en &lt;a href="/es/blog/concurrent-map/"&gt;Concurrent Map&lt;/a&gt;. Y si tus chunks secuenciales necesitan rate limiting entre ellos, añadí esa capacidad en &lt;a href="/es/blog/async-map-timeout/"&gt;Async Map Timeout&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Esta solución es la &lt;strong&gt;evolución natural&lt;/strong&gt; de &lt;span class="high"&gt;asyncMap&lt;/span&gt; y &lt;span class="high"&gt;concurrentMap&lt;/span&gt;, combinando lo mejor de ambos mundos para optimizar el procesamiento de datos masivos en aplicaciones backend.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Keep coding, keep running&lt;/strong&gt; 🏃‍♂️&lt;/p&gt;
&lt;hr /&gt;</content>
    <link rel="alternate" href="https://jcalderita.com/es/blog/async-concurrent-map/"></link>
    <media:content medium="image" url="https://jcalderita.com/static/blog/AsyncConcurrentMap.webp"></media:content>
  </entry>
  <entry>
    <id>https://jcalderita.com/es/blog/async-map-timeout/</id>
    <title>Async Map Timeout</title>
    <updated>2026-01-14T00:00:00Z</updated>
    <summary>Actualización de asyncMap para añadir control de rate limiting mediante timeouts entre operaciones.</summary>
    <content type="html">&lt;hr /&gt;
&lt;h2&gt;Problema&lt;/h2&gt;
&lt;p&gt;En el &lt;a href="/es/blog/async-map/"&gt;post sobre AsyncMap&lt;/a&gt; vimos cómo procesar colecciones de forma &lt;strong&gt;secuencial asíncrona&lt;/strong&gt;. Sin embargo, al trabajar con &lt;strong&gt;APIs externas&lt;/strong&gt; que implementan rate limiting, nos enfrentamos a un problema crítico:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-swift"&gt;// Procesar 1000 URLs secuencialmente
let results = try await urls.asyncMap { url in
    try await apiClient.fetch(url)  // ⚠️ 1000 llamadas sin pausa
}
// Error 429: Too Many Requests
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Realizar llamadas consecutivas sin pausas puede:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Exceder límites de rate&lt;/strong&gt;: APIs rechazan con &lt;span class="high"&gt;429 Too Many Requests&lt;/span&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Saturar servicios externos&lt;/strong&gt;: sobrecarga de conexiones simultáneas.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Desperdiciar recursos&lt;/strong&gt;: forzar reintentos consume más tiempo y ancho de banda.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Bloqueos temporales&lt;/strong&gt;: algunas APIs bloquean la IP tras múltiples infracciones.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Necesitamos una forma de &lt;strong&gt;controlar el ritmo&lt;/strong&gt; de las operaciones secuenciales, añadiendo pausas intencionales entre cada procesamiento.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Solución&lt;/h2&gt;
&lt;p&gt;Actualizamos &lt;span class="high"&gt;asyncMap&lt;/span&gt; añadiendo un parámetro opcional &lt;span class="high"&gt;timeout&lt;/span&gt; que introduce una pausa configurable después de procesar cada elemento.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-swift"&gt;extension Sequence {
    func asyncMap&amp;lt;T&amp;gt;(
        timeout: Double? = nil,
        _ transform: (Element) async throws -&amp;gt; T
    ) async throws -&amp;gt; [T] {
        var results = [T]()
        results.reserveCapacity(underestimatedCount)
        for element in self {
            try await results.append(transform(element))
            if let timeout {
                try await Task.sleep(for: .seconds(timeout))
            }
        }
        return results
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Cambios clave respecto a la versión original&lt;/strong&gt;:&lt;/p&gt;
&lt;p&gt;✨ Parámetro &lt;span class="high"&gt;timeout: Double? = nil&lt;/span&gt; opcional y retrocompatible.&lt;br /&gt;&lt;br /&gt;
⏱️ Si se especifica timeout, añade &lt;span class="high"&gt;Task.sleep(for: .seconds(timeout))&lt;/span&gt; después de cada elemento.&lt;br /&gt;&lt;br /&gt;
🔄 Mantiene el comportamiento original cuando no se especifica timeout (sin pausas).&lt;br /&gt;&lt;br /&gt;
📊 Permite ajustar dinámicamente el rate limiting según los límites de cada API.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Resultado&lt;/h2&gt;
&lt;pre&gt;&lt;code class="language-swift"&gt;// Rate limiting: 1 llamada por segundo
let results = try await urls.asyncMap(timeout: 1.0) {
    try await apiClient.fetch($0)
}

// Rate limiting agresivo: 1 llamada cada 5 segundos
let results = try await endpoints.asyncMap(timeout: 5.0) {
    try await scraper.parse($0)
}

// Sin timeout: comportamiento original (máxima velocidad)
let results = try await localFiles.asyncMap {
    try await processFile($0)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Beneficios de esta actualización:&lt;/p&gt;
&lt;p&gt;⏱️ &lt;strong&gt;Rate limiting configurable&lt;/strong&gt;: controla el ritmo de llamadas según los límites de cada API.&lt;br /&gt;&lt;br /&gt;
🛡️ &lt;strong&gt;Prevención de bloqueos&lt;/strong&gt;: evita errores &lt;span class="high"&gt;429&lt;/span&gt; y suspensiones temporales de IP.&lt;br /&gt;&lt;br /&gt;
🔄 &lt;strong&gt;Retrocompatibilidad total&lt;/strong&gt;: sin timeout funciona exactamente igual que antes.&lt;br /&gt;&lt;br /&gt;
🎯 &lt;strong&gt;Flexibilidad por caso de uso&lt;/strong&gt;: ajusta el timeout según la tolerancia del servicio externo.&lt;br /&gt;&lt;br /&gt;
📊 &lt;strong&gt;Procesamiento predecible&lt;/strong&gt;: calcula fácilmente el tiempo total (n elementos × timeout).&lt;/p&gt;
&lt;p&gt;Esta actualización convierte &lt;span class="high"&gt;asyncMap&lt;/span&gt; en una herramienta &lt;strong&gt;completa para procesamiento secuencial controlado&lt;/strong&gt;, ideal para integración con APIs que imponen límites de tasa y necesitan un flujo de peticiones regulado.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Keep coding, keep running&lt;/strong&gt; 🏃‍♂️&lt;/p&gt;
&lt;hr /&gt;</content>
    <link href="https://jcalderita.com/es/blog/async-map-timeout/" rel="alternate"></link>
    <media:content medium="image" url="https://jcalderita.com/static/blog/AsyncMapTimeout.webp"></media:content>
  </entry>
  <entry>
    <id>https://jcalderita.com/es/blog/copy-to/</id>
    <title>Copy To</title>
    <updated>2026-01-07T00:00:00Z</updated>
    <summary>Exporta grandes conjuntos de datos de PostgreSQL a CSV en Vapor usando el comando nativo COPY TO, evitando serialización manual y reduciendo consumo de memoria.</summary>
    <content type="html">&lt;hr /&gt;
&lt;h2&gt;Problema&lt;/h2&gt;
&lt;p&gt;En aplicaciones backend con &lt;span class="high"&gt;Vapor&lt;/span&gt;, a menudo necesitamos &lt;strong&gt;exportar grandes volúmenes de datos&lt;/strong&gt; desde la base de datos a archivos para diferentes propósitos: backups, análisis offline, integración con sistemas externos, o auditorías de datos.&lt;/p&gt;
&lt;p&gt;Usar consultas tradicionales con &lt;span class="high"&gt;Fluent&lt;/span&gt; para luego serializar los resultados manualmente presenta varios inconvenientes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Alto consumo de memoria&lt;/strong&gt;: cargar miles de registros en memoria para procesarlos uno a uno.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Procesamiento lento&lt;/strong&gt;: serialización manual a &lt;span class="high"&gt;CSV&lt;/span&gt; requiere iterar y formatear cada registro.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Falta de optimización&lt;/strong&gt;: no aprovecha las capacidades nativas de exportación del motor de base de datos.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Complejidad innecesaria&lt;/strong&gt;: gestión manual de formatos, escapado de caracteres y manejo de valores nulos.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Para escenarios de exportación masiva, necesitamos una estrategia que aproveche las capacidades nativas de &lt;span class="high"&gt;PostgreSQL&lt;/span&gt; para generar archivos de forma eficiente.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Solución&lt;/h2&gt;
&lt;p&gt;Extendemos &lt;span class="high"&gt;Database&lt;/span&gt; con una función que ejecuta el comando &lt;span class="high"&gt;COPY … TO&lt;/span&gt; de &lt;span class="high"&gt;PostgreSQL&lt;/span&gt;, permitiendo exportar datos directamente desde el schema de la tabla a archivos &lt;span class="high"&gt;CSV&lt;/span&gt; en el sistema de archivos.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-swift"&gt;extension Database {
    func exportCSV(
        _ model: any Model.Type,
        file: PathEnum)
        async throws {
        let query = SQLQueryString(
            &amp;quot;&amp;quot;&amp;quot;
            COPY \&amp;quot;\(unsafeRaw: model.space ?? &amp;quot;public&amp;quot;)\&amp;quot;.
            \&amp;quot;\(unsafeRaw: model.schema)\&amp;quot;
            TO '\(unsafeRaw: file.rawValue)'
            WITH (FORMAT csv, HEADER true, DELIMITER ',',
            QUOTE '&amp;quot;', ESCAPE '&amp;quot;', NULL '')
            &amp;quot;&amp;quot;&amp;quot;
        )

        try await self.sqlDatabase
            .raw(query).run()
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Puntos clave:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Usa &lt;span class="high"&gt;COPY … TO&lt;/span&gt; de &lt;span class="high"&gt;PostgreSQL&lt;/span&gt;, el método más eficiente para exportación masiva.&lt;/li&gt;
&lt;li&gt;El parámetro &lt;span class="high"&gt;model.space&lt;/span&gt; soporta schemas personalizados (por defecto &lt;span class="high"&gt;“public”&lt;/span&gt;).&lt;/li&gt;
&lt;li&gt;&lt;span class="high"&gt;model.schema&lt;/span&gt; obtiene automáticamente el nombre de la tabla del modelo &lt;span class="high"&gt;Fluent&lt;/span&gt;.&lt;/li&gt;
&lt;li&gt;Configuración &lt;span class="high"&gt;CSV&lt;/span&gt; estándar: headers incluidos, delimitadores y manejo correcto de valores nulos.&lt;/li&gt;
&lt;li&gt;Utiliza &lt;span class="high"&gt;unsafeRaw&lt;/span&gt; para interpolación directa en la query SQL.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;Resultado&lt;/h2&gt;
&lt;pre&gt;&lt;code class="language-swift"&gt;func exportRegions() async throws {
    try await db.exportCSV(
        LocationRegionModel.self, 
        file: file
    )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Beneficios de esta aproximación:&lt;/p&gt;
&lt;p&gt;🚀 &lt;strong&gt;Performance óptimo&lt;/strong&gt;: &lt;span class="high"&gt;COPY … TO&lt;/span&gt; es mucho más rápido que serialización manual.&lt;br /&gt;&lt;br /&gt;
💾 &lt;strong&gt;Eficiencia de recursos&lt;/strong&gt;: &lt;span class="high"&gt;PostgreSQL&lt;/span&gt; escribe directamente al archivo sin cargar datos en memoria de la aplicación.&lt;br /&gt;&lt;br /&gt;
📦 &lt;strong&gt;Formato consistente&lt;/strong&gt;: el motor de base de datos garantiza un &lt;span class="high"&gt;CSV&lt;/span&gt; válido con escapado correcto.&lt;br /&gt;&lt;br /&gt;
🔧 &lt;strong&gt;Integración nativa&lt;/strong&gt;: aprovecha capacidades optimizadas del motor &lt;span class="high"&gt;PostgreSQL&lt;/span&gt;.&lt;br /&gt;&lt;br /&gt;
📊 &lt;strong&gt;Escalabilidad&lt;/strong&gt;: permite exportar millones de registros sin impacto en el rendimiento de la aplicación.&lt;/p&gt;
&lt;p&gt;Esta solución es el complemento perfecto para &lt;span class="high"&gt;importCSV&lt;/span&gt;, formando un par de funciones que permite &lt;strong&gt;movimiento bidireccional de datos&lt;/strong&gt; entre &lt;span class="high"&gt;PostgreSQL&lt;/span&gt; y el sistema de archivos de forma eficiente y confiable. Si aún no has visto la parte de importación, explico cómo construirla usando &lt;code&gt;COPY FROM&lt;/code&gt; de PostgreSQL en &lt;a href="/es/blog/copy-from/"&gt;Copy From&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Keep coding, keep running&lt;/strong&gt; 🏃‍♂️&lt;/p&gt;
&lt;hr /&gt;</content>
    <link href="https://jcalderita.com/es/blog/copy-to/" rel="alternate"></link>
    <media:content url="https://jcalderita.com/static/blog/CopyTo.webp" medium="image"></media:content>
  </entry>
  <entry>
    <id>https://jcalderita.com/es/blog/copy-from/</id>
    <title>Copy From</title>
    <updated>2025-12-31T00:00:00Z</updated>
    <summary>Como hacer bulk insert con Vapor para acelerar tus inserciones de datos a base de datos.</summary>
    <content type="html">&lt;hr /&gt;
&lt;h2&gt;Problema&lt;/h2&gt;
&lt;p&gt;En aplicaciones backend con &lt;span class="high"&gt;Vapor&lt;/span&gt;, cuando necesitamos insertar &lt;strong&gt;grandes volúmenes de datos&lt;/strong&gt; en la base de datos (migraciones iniciales, importación de catálogos, carga masiva desde APIs externas), usar &lt;span class="high"&gt;.save()&lt;/span&gt; en un bucle genera &lt;strong&gt;múltiples transacciones individuales&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Esto resulta en:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Alta latencia&lt;/strong&gt;: cada insert abre/cierra conexión y transaction overhead.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pobre throughput&lt;/strong&gt;: no aprovecha las capacidades de inserción masiva del motor de base de datos.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Riesgo de timeout&lt;/strong&gt;: operaciones lentas que pueden fallar en entornos con límites de tiempo.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Para escenarios de importación masiva (miles o millones de registros), necesitamos una estrategia de &lt;strong&gt;bulk insert&lt;/strong&gt; que aproveche las capacidades nativas de &lt;span class="high"&gt;PostgreSQL&lt;/span&gt;.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Solución&lt;/h2&gt;
&lt;p&gt;Extendemos &lt;span class="high"&gt;Database&lt;/span&gt; con una función que ejecuta el comando &lt;span class="high"&gt;COPY&lt;/span&gt; de &lt;span class="high"&gt;PostgreSQL&lt;/span&gt;, permitiendo importar archivos &lt;span class="high"&gt;CSV&lt;/span&gt; directamente desde el sistema de archivos al schema de la tabla.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-swift"&gt;extension Database {
    func importCSV(
        _ model: any Model.Type,
        file: PathEnum
        ) async throws {
        let query = SQLQueryString(
            &amp;quot;&amp;quot;&amp;quot;
            COPY \&amp;quot;\(unsafeRaw: model.space ?? &amp;quot;public&amp;quot;)\&amp;quot;.
            \&amp;quot;\(unsafeRaw: model.schema)\&amp;quot;
            FROM '\(unsafeRaw: file.rawValue)'
            WITH (FORMAT csv, HEADER true, DELIMITER ',',
            QUOTE '&amp;quot;', ESCAPE '&amp;quot;', NULL '')
            &amp;quot;&amp;quot;&amp;quot;
        )

        try await self.sqlDatabase
            .raw(query).run()
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Puntos clave:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Usa &lt;span class="high"&gt;COPY&lt;/span&gt; de &lt;span class="high"&gt;PostgreSQL&lt;/span&gt;, el método más rápido para bulk insert desde archivos.&lt;/li&gt;
&lt;li&gt;El parámetro &lt;span class="high"&gt;model.space&lt;/span&gt; soporta schemas personalizados (por defecto &lt;span class="high"&gt;“public”&lt;/span&gt;).&lt;/li&gt;
&lt;li&gt;&lt;span class="high"&gt;model.schema&lt;/span&gt; obtiene automáticamente el nombre de la tabla del modelo &lt;span class="high"&gt;Fluent&lt;/span&gt;.&lt;/li&gt;
&lt;li&gt;Configuración &lt;span class="high"&gt;CSV&lt;/span&gt; estándar: headers, delimitadores y manejo de valores nulos.&lt;/li&gt;
&lt;li&gt;Utiliza &lt;span class="high"&gt;unsafeRaw&lt;/span&gt; para interpolación directa en la query SQL.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;Resultado&lt;/h2&gt;
&lt;pre&gt;&lt;code class="language-swift"&gt;private func importRegions() async throws {
    try await db.importCSV(
        RegionModel.self,
        file: .LocationFile(&amp;quot;regions&amp;quot;, .csv)
    )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Beneficios de esta aproximación:&lt;/p&gt;
&lt;p&gt;🚀 &lt;strong&gt;Performance extremo&lt;/strong&gt;: &lt;span class="high"&gt;COPY&lt;/span&gt; es hasta &lt;strong&gt;10-100x más rápido&lt;/strong&gt; que inserts individuales.&lt;br /&gt;&lt;br /&gt;
📦 &lt;strong&gt;Transacción atómica&lt;/strong&gt;: toda la importación ocurre en una sola operación, garantizando consistencia.&lt;br /&gt;&lt;br /&gt;
💾 &lt;strong&gt;Eficiencia de recursos&lt;/strong&gt;: minimiza el uso de memoria y conexiones de base de datos.&lt;br /&gt;&lt;br /&gt;
🔧 &lt;strong&gt;Integración nativa&lt;/strong&gt;: aprovecha capacidades optimizadas del motor &lt;span class="high"&gt;PostgreSQL&lt;/span&gt;.&lt;br /&gt;&lt;br /&gt;
📊 &lt;strong&gt;Escalabilidad&lt;/strong&gt;: permite importar millones de registros sin degradación significativa.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Notas&lt;/h2&gt;
&lt;p&gt;Esta implementación utiliza &lt;span class="high"&gt;COPY … FROM&lt;/span&gt; con archivos del sistema. Actualmente existe una &lt;strong&gt;issue abierta en Vapor&lt;/strong&gt; para implementar soporte nativo de &lt;span class="high"&gt;COPY … FROM STDIN&lt;/span&gt;, que permitiría realizar bulk inserts directamente desde memoria sin necesidad de archivos intermedios.&lt;/p&gt;
&lt;p&gt;Estoy vigilando esta issue de manera activa para integrar esta funcionalidad cuando esté disponible, lo que proporcionará una API aún más flexible y eficiente para operaciones de importación masiva.&lt;/p&gt;
&lt;p&gt;Si también necesitas la operación inversa — exportar datos de PostgreSQL a archivos CSV — lo cubrí en &lt;a href="/es/blog/copy-to/"&gt;Copy To&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Keep coding, keep running&lt;/strong&gt; 🏃‍♂️&lt;/p&gt;
&lt;hr /&gt;</content>
    <link rel="alternate" href="https://jcalderita.com/es/blog/copy-from/"></link>
    <media:content url="https://jcalderita.com/static/blog/CopyFrom.webp" medium="image"></media:content>
  </entry>
  <entry>
    <id>https://jcalderita.com/es/blog/swift-package/</id>
    <title>Swift Package</title>
    <updated>2025-12-24T00:00:00Z</updated>
    <summary>Guía completa sobre los comandos de limpieza de Swift Package Manager: clean, reset, purge-cache y cuándo usar cada uno para resolver problemas de dependencias.</summary>
    <content type="html">&lt;hr /&gt;
&lt;h2&gt;El dilema de las dependencias rotas&lt;/h2&gt;
&lt;p&gt;Cuando trabajas con &lt;strong&gt;Swift Package Manager (SPM)&lt;/strong&gt;, eventualmente te encontrarás con un error de compilación que parece no tener sentido. Has intentado compilar varias veces, has reiniciado Xcode, pero el error persiste. La solución muchas veces está en &lt;strong&gt;limpiar correctamente las cachés y artefactos&lt;/strong&gt; de SPM, pero ¿qué comando usar?&lt;/p&gt;
&lt;p&gt;Existen múltiples formas de “limpiar” en SPM, cada una con un propósito específico. Usar el comando equivocado puede no resolver tu problema o, peor aún, forzarte a descargar gigabytes de dependencias nuevamente.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Los cuatro caminos para limpiar&lt;/h2&gt;
&lt;p&gt;SPM ofrece tres comandos oficiales más una alternativa manual. Cada uno afecta diferentes partes del sistema:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Comando&lt;/th&gt;
&lt;th&gt;Elimina .build local&lt;/th&gt;
&lt;th&gt;Elimina Package.resolved&lt;/th&gt;
&lt;th&gt;Elimina caché global&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span class="high"&gt;swift package clean&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;❌ (solo binarios compilados)&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span class="high"&gt;swift package reset&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;✅ (completo)&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span class="high"&gt;swift package purge-cache&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span class="high"&gt;rm -rf .build&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;✅ (completo)&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;Swift package clean&lt;/h3&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;swift package clean
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Qué hace:&lt;/strong&gt;&lt;br /&gt;
Elimina únicamente los &lt;strong&gt;binarios compilados finales&lt;/strong&gt; dentro de la carpeta &lt;span class="high"&gt;.build&lt;/span&gt;, pero &lt;strong&gt;mantiene dependencias descargadas y archivos intermedios&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Cuándo usarlo:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Después de cambiar configuraciones de compilación&lt;/li&gt;
&lt;li&gt;Para forzar una recompilación completa sin re-descargar dependencias&lt;/li&gt;
&lt;li&gt;Cuando los binarios están corruptos pero las fuentes están bien&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;💡 No descarga dependencias nuevamente ni resuelve problemas de caché de paquetes.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Swift package reset&lt;/h3&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;swift package reset
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Qué hace:&lt;/strong&gt;&lt;br /&gt;
Elimina &lt;strong&gt;completamente&lt;/strong&gt; la carpeta &lt;span class="high"&gt;.build&lt;/span&gt; (incluyendo dependencias descargadas) y el archivo &lt;span class="high"&gt;Package.resolved&lt;/span&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Cuándo usarlo:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Cuando hay conflictos en las versiones de dependencias&lt;/li&gt;
&lt;li&gt;Después de cambios importantes en &lt;span class="high"&gt;Package.swift&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;Para resolver errores de “dependencia no encontrada”&lt;/li&gt;
&lt;li&gt;Cuando &lt;span class="high"&gt;Package.resolved&lt;/span&gt; está desactualizado o corrupto&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;⚠️ La próxima compilación descargará y resolverá todas las dependencias desde cero. Esto puede tardar varios minutos dependiendo de la cantidad de paquetes.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Swift package purge-cache&lt;/h3&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;swift package purge-cache
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;💡 Disponible desde Swift 5.7 en adelante.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Qué hace:&lt;/strong&gt;&lt;br /&gt;
Elimina la &lt;strong&gt;caché global de paquetes&lt;/strong&gt; ubicada en:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;~/Library/Caches/org.swift.swiftpm/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Esta caché contiene repositorios clonados y artefactos binarios compartidos entre todos tus proyectos.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Cuándo usarlo:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Cuando múltiples proyectos tienen el mismo problema&lt;/li&gt;
&lt;li&gt;Después de actualizar Xcode o herramientas de Swift&lt;/li&gt;
&lt;li&gt;Para liberar espacio en disco (puede ocupar varios GB)&lt;/li&gt;
&lt;li&gt;Cuando sospechas que la caché global está corrupta&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;⚠️ Todos tus proyectos tendrán que re-descargar dependencias comunes.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;rm -rf .build&lt;/h3&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;rm -rf .build
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Qué hace:&lt;/strong&gt;&lt;br /&gt;
Elimina manualmente la carpeta &lt;span class="high"&gt;.build&lt;/span&gt; completa, similar a &lt;span class="high"&gt;reset&lt;/span&gt; pero &lt;strong&gt;sin tocar&lt;/strong&gt; &lt;span class="high"&gt;Package.resolved&lt;/span&gt;. Es menos agresivo que reset porque no re-resuelve dependencias.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Cuándo usarlo:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Para limpiar artefactos de compilación manteniendo la resolución de versiones&lt;/li&gt;
&lt;li&gt;En scripts de CI/CD donde quieres control total&lt;/li&gt;
&lt;li&gt;Cuando &lt;span class="high"&gt;swift package reset&lt;/span&gt; no está disponible&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;💡 Preserva &lt;span class="high"&gt;Package.resolved&lt;/span&gt;, lo que significa que las versiones de dependencias no se re-resolverán.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;Estrategia de resolución de problemas&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Nivel 1: Limpieza ligera&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;swift package clean
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;💡 Prueba primero lo menos invasivo. Resuelve el 30% de los problemas.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Nivel 2: Reset completo del proyecto&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;swift package reset
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;💡 Si el nivel 1 falla. Resuelve el 60% de los problemas restantes.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Nivel 3: Purgar caché global&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;swift package purge-cache
swift package reset
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;💡 Para problemas persistentes o que afectan múltiples proyectos.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Nivel 4: Nuclear&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;rm -rf .build
rm Package.resolved
swift package purge-cache
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;💡 Última opción. Borrón y cuenta nueva total.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;Casos de uso reales&lt;/h2&gt;
&lt;h3&gt;Escenario 1: Error después de actualizar Xcode&lt;/h3&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;swift package purge-cache
swift package reset
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;💡 Las herramientas de compilación cambiaron y la caché puede tener artefactos incompatibles.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Escenario 2: Conflicto de versiones de dependencias&lt;/h3&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;swift package reset
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;💡 Necesitas re-resolver todas las dependencias con las nuevas restricciones.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Escenario 3: Espacio en disco lleno&lt;/h3&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;swift package purge-cache
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;💡 La caché global puede crecer hasta varios GB sin que lo notes.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Escenario 4: CI/CD builds&lt;/h3&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;rm -rf .build
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;💡 En entornos de integración continua, quieres builds limpios pero reproducibles con &lt;code&gt;Package.resolved&lt;/code&gt; versionado.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;Entendiendo los componentes&lt;/h2&gt;
&lt;h3&gt;.build (local)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Artefactos de compilación del proyecto actual&lt;/li&gt;
&lt;li&gt;Dependencias descargadas específicas del proyecto&lt;/li&gt;
&lt;li&gt;Archivos intermedios de compilación&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Package.resolved&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;“Lockfile” que fija versiones exactas de dependencias&lt;/li&gt;
&lt;li&gt;Garantiza builds reproducibles&lt;/li&gt;
&lt;li&gt;Debe versionarse en Git para proyectos compartidos&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Caché global&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Compartida entre todos tus proyectos Swift&lt;/li&gt;
&lt;li&gt;Contiene clones de repositorios de dependencias&lt;/li&gt;
&lt;li&gt;Artefactos binarios pre-compilados&lt;/li&gt;
&lt;li&gt;Puede alcanzar varios GB con el tiempo&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;Mejores prácticas&lt;/h2&gt;
&lt;p&gt;✅ &lt;strong&gt;Versiona&lt;/strong&gt; &lt;span class="high"&gt;Package.resolved&lt;/span&gt; en Git para garantizar que todo el equipo use las mismas versiones&lt;br /&gt;&lt;br /&gt;
✅ &lt;strong&gt;Usa&lt;/strong&gt; &lt;span class="high"&gt;reset&lt;/span&gt; &lt;strong&gt;después de cambios en Package.swift&lt;/strong&gt; para asegurar una resolución limpia&lt;br /&gt;&lt;br /&gt;
✅ &lt;strong&gt;Purga la caché periódicamente&lt;/strong&gt; si trabajas con muchos proyectos&lt;br /&gt;&lt;br /&gt;
✅ &lt;strong&gt;En CI/CD, mantén&lt;/strong&gt; &lt;span class="high"&gt;Package.resolved&lt;/span&gt; pero elimina &lt;span class="high"&gt;.build&lt;/span&gt; para builds limpios&lt;br /&gt;&lt;br /&gt;
❌ &lt;strong&gt;No ignores&lt;/strong&gt; &lt;span class="high"&gt;.build&lt;/span&gt; &lt;strong&gt;en .gitignore&lt;/strong&gt; - ya está ignorado por defecto&lt;br /&gt;&lt;br /&gt;
❌ &lt;strong&gt;No borres&lt;/strong&gt; &lt;span class="high"&gt;Package.resolved&lt;/span&gt; a menos que realmente necesites re-resolver versiones&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Conclusión&lt;/h2&gt;
&lt;p&gt;Entender la diferencia entre estos comandos te ahorra tiempo y frustración. No todos los problemas de compilación necesitan una limpieza nuclear:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;span class="high"&gt;clean&lt;/span&gt; → Solo binarios compilados&lt;/li&gt;
&lt;li&gt;&lt;span class="high"&gt;reset&lt;/span&gt; → Proyecto completo + Package.resolved&lt;/li&gt;
&lt;li&gt;&lt;span class="high"&gt;purge-cache&lt;/span&gt; → Caché global compartida&lt;/li&gt;
&lt;li&gt;&lt;span class="high"&gt;rm -rf&lt;/span&gt; → Control manual quirúrgico&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;La próxima vez que SPM te muestre un error extraño, ya sabes exactamente qué herramienta usar.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Keep coding, keep running&lt;/strong&gt; 🏃‍♂️&lt;/p&gt;
&lt;hr /&gt;</content>
    <link href="https://jcalderita.com/es/blog/swift-package/" rel="alternate"></link>
    <media:content url="https://jcalderita.com/static/blog/SwiftPackage.webp" medium="image"></media:content>
  </entry>
  <entry>
    <id>https://jcalderita.com/es/blog/sub-process/</id>
    <title>Subprocess</title>
    <updated>2025-12-17T00:00:00Z</updated>
    <summary>Cambiando el uso de process por Subprocess, la nueva libreria Subprocess es un paquete multiplataforma para lanzar procesos en Swift.</summary>
    <content type="html">&lt;hr /&gt;
&lt;h2&gt;Problema&lt;/h2&gt;
&lt;p&gt;Al trabajar con procesos externos en aplicaciones &lt;span class="high"&gt;Swift&lt;/span&gt;, la clase tradicional &lt;span class="high"&gt;Process&lt;/span&gt; presenta varias limitaciones importantes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Gestión manual de recursos&lt;/strong&gt;: Requiere configuración explícita de URLs ejecutables, argumentos y manejo de salidas&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Falta de soporte async/await&lt;/strong&gt;: Utiliza métodos síncronos que bloquean el hilo de ejecución&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Manejo complejo de errores&lt;/strong&gt;: Dificulta la captura y procesamiento de errores del proceso hijo&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Configuración verbosa&lt;/strong&gt;: Cada ejecución requiere múltiples líneas de configuración repetitiva&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;En el código anterior, necesitaba ejecutar &lt;span class="high"&gt;Ghostscript&lt;/span&gt; para convertir archivos PDF a imágenes PNG, pero la implementación con &lt;span class="high"&gt;Process&lt;/span&gt; resulta extensa y poco elegante.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-swift"&gt;func _PDFToImages(
    _ fileName: PathEnum
) async throws {
    let process = Process()

    process.executableURL = URL(
        fileURLWithPath: &amp;quot;/opt/homebrew/bin/gs&amp;quot;
    )

    process.arguments = [
        &amp;quot;-dNOPAUSE&amp;quot;, &amp;quot;-dBATCH&amp;quot;,
        &amp;quot;-dQUIET&amp;quot;, &amp;quot;-sDEVICE=png16m&amp;quot;,
        &amp;quot;-r300&amp;quot;,
        &amp;quot;-sOutputFile=\(fileName.rawValue)-%d.png&amp;quot;,
        fileName.rawValue.appending(&amp;quot;.pdf&amp;quot;),
    ]

    try process.run()
    process.waitUntilExit()
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Solución&lt;/h2&gt;
&lt;p&gt;La nueva librería &lt;span class="high"&gt;Subprocess&lt;/span&gt; de Apple proporciona una API moderna y robusta para la ejecución de procesos en &lt;span class="high"&gt;Swift&lt;/span&gt;. Esta librería &lt;strong&gt;multiplataforma&lt;/strong&gt; ofrece soporte nativo para &lt;span class="high"&gt;async/await&lt;/span&gt; y gestión automática de recursos.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Características principales:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;✅ &lt;strong&gt;API nativa async/await&lt;/strong&gt; para operaciones no bloqueantes.&lt;br /&gt;&lt;br /&gt;
✅ &lt;strong&gt;Gestión automática de recursos&lt;/strong&gt; y limpieza de procesos.&lt;br /&gt;&lt;br /&gt;
✅ &lt;strong&gt;Control granular de salidas&lt;/strong&gt; (stdout, stderr).&lt;br /&gt;&lt;br /&gt;
✅ &lt;strong&gt;Verificación de estado de terminación&lt;/strong&gt; integrada.&lt;br /&gt;&lt;br /&gt;
✅ &lt;strong&gt;Sintaxis concisa y expresiva.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;La implementación modernizada utiliza la función &lt;span class="high"&gt;run&lt;/span&gt; de &lt;span class="high"&gt;Subprocess&lt;/span&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-swift"&gt;func _PDFToImages(
    _ fileName: PathEnum
) async throws {
    let result = try await run(
        .path(.init(&amp;quot;/opt/homebrew/bin/gs&amp;quot;)),
        arguments: [
            &amp;quot;-dNOPAUSE&amp;quot;, &amp;quot;-dBATCH&amp;quot;,
            &amp;quot;-dQUIET&amp;quot;, &amp;quot;-sDEVICE=png16m&amp;quot;,
            &amp;quot;-r300&amp;quot;,
            &amp;quot;-sOutputFile=\(fileName.rawValue)-%d.png&amp;quot;,
            fileName.rawValue.appending(&amp;quot;.pdf&amp;quot;),
        ],
        output: .discarded,
        error: .string(limit: .max)
    )

    try guardAndLogError(
        result.terminationStatus == .exited(0),
        message: result.standardError,
        status: .internalServerError
    )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Parámetros clave:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;span class="high"&gt;.path&lt;/span&gt;: Especifica el ejecutable de forma directa&lt;/li&gt;
&lt;li&gt;&lt;span class="high"&gt;output: .discarded&lt;/span&gt;: Descarta la salida estándar ya que no necesitamos procesarla&lt;/li&gt;
&lt;li&gt;&lt;span class="high"&gt;error: .string(limit: .max)&lt;/span&gt;: Captura errores como string para logging&lt;/li&gt;
&lt;li&gt;&lt;span class="high"&gt;result.terminationStatus&lt;/span&gt;: Verifica que el proceso terminó exitosamente&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;Resultado&lt;/h2&gt;
&lt;p&gt;La migración a &lt;span class="high"&gt;Subprocess&lt;/span&gt; transforma el código de gestión de procesos en una solución &lt;strong&gt;más limpia, segura y eficiente&lt;/strong&gt;:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Beneficios obtenidos:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;🚀 &lt;strong&gt;Rendimiento mejorado&lt;/strong&gt; con operaciones asíncronas reales.&lt;br /&gt;&lt;br /&gt;
🔒 &lt;strong&gt;Mejor manejo de errores&lt;/strong&gt; con captura integrada de stderr.&lt;br /&gt;&lt;br /&gt;
📝 &lt;strong&gt;Código más legible&lt;/strong&gt; con menos configuración manual.&lt;br /&gt;&lt;br /&gt;
⚡ &lt;strong&gt;Integración perfecta&lt;/strong&gt; con el ecosistema moderno de Swift concurrency.&lt;br /&gt;&lt;/p&gt;
&lt;p&gt;La nueva implementación no solo reduce la complejidad sino que también &lt;strong&gt;mejora la robustez&lt;/strong&gt; del sistema al proporcionar mejor visibilidad de errores y un manejo más elegante de la ejecución asíncrona en aplicaciones &lt;span class="high"&gt;Vapor&lt;/span&gt;.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Nota sobre DYLD_LIBRARY_PATH&lt;/h2&gt;
&lt;p&gt;En versiones anteriores de &lt;span class="high"&gt;Swift&lt;/span&gt;, existía un problema conocido con la variable de entorno &lt;span class="high"&gt;DYLD_LIBRARY_PATH&lt;/span&gt; al ejecutar procesos externos. Debido a las restricciones de &lt;strong&gt;System Integrity Protection (SIP)&lt;/strong&gt; de macOS, esta variable era eliminada automáticamente al lanzar subprocesos, lo que causaba errores de “Library not loaded” en ciertos casos.&lt;/p&gt;
&lt;p&gt;La solución temporal requería configurar manualmente las rutas de las librerías usando &lt;span class="high"&gt;install_name_tool&lt;/span&gt; con &lt;span class="high"&gt;@rpath&lt;/span&gt;, o bien establecer la variable &lt;span class="high"&gt;DYLD_LIBRARY_PATH&lt;/span&gt; en su lugar.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;¡Buenas noticias!&lt;/strong&gt; 🎉 Este problema ha sido resuelto en las versiones más recientes de &lt;span class="high"&gt;Swift&lt;/span&gt;. La librería &lt;span class="high"&gt;Subprocess&lt;/span&gt; maneja correctamente las variables de entorno del sistema, incluida &lt;span class="high"&gt;DYLD_LIBRARY_PATH&lt;/span&gt;, sin necesidad de configuraciones adicionales.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Keep coding, keep running&lt;/strong&gt; 🏃‍♂️&lt;/p&gt;
&lt;hr /&gt;</content>
    <link rel="alternate" href="https://jcalderita.com/es/blog/sub-process/"></link>
    <media:content url="https://jcalderita.com/static/blog/Subprocess.webp" medium="image"></media:content>
  </entry>
  <entry>
    <id>https://jcalderita.com/es/blog/json-snakecase/</id>
    <title>Json Snake Case</title>
    <updated>2025-12-11T00:00:00Z</updated>
    <summary>Conveniencias para codificar/decodificar JSON en snake_case sin boilerplate, válidas para cliente iOS y servidor Vapor.</summary>
    <content type="html">&lt;hr /&gt;
&lt;h2&gt;Problema&lt;/h2&gt;
&lt;p&gt;Cuando integramos APIs REST, es muy común que las claves JSON vengan en &lt;span class="high"&gt;snake_case&lt;/span&gt; (por ejemplo, &lt;strong&gt;first_name&lt;/strong&gt;) mientras que en Swift modelamos las propiedades en &lt;span class="high"&gt;camelCase&lt;/span&gt; (&lt;strong&gt;firstName&lt;/strong&gt;).&lt;br /&gt;
Si no configuramos nada, nos toca escribir &lt;span class="high"&gt;CodingKeys&lt;/span&gt; a mano en cada modelo o aceptar errores de decodificación.&lt;/p&gt;
&lt;p&gt;Buscamos una forma &lt;strong&gt;centralizada&lt;/strong&gt; y &lt;strong&gt;reutilizable&lt;/strong&gt; de:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Decodificar JSON sin &lt;span class="high"&gt;CodingKeys&lt;/span&gt; manuales.&lt;/li&gt;
&lt;li&gt;Codificar nuestros modelos al enviar datos.&lt;/li&gt;
&lt;li&gt;Mantener el mismo comportamiento tanto en apps iOS/macOS (URLSession) como en &lt;strong&gt;Vapor&lt;/strong&gt; (req/res content).&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;Solución&lt;/h2&gt;
&lt;p&gt;Creamos extensiones de &lt;span class="high"&gt;JSONDecoder&lt;/span&gt; y &lt;span class="high"&gt;JSONEncoder&lt;/span&gt; que exponen constructores de conveniencia y atajos estáticos &lt;span class="high"&gt;snakeCase&lt;/span&gt;.&lt;br /&gt;
Ventajas:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Cero boilerplate&lt;/strong&gt; en los modelos: evita &lt;span class="high"&gt;CodingKeys&lt;/span&gt; repetitivas.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Nombre autoexplicativo&lt;/strong&gt; al usarlas: &lt;span class="high"&gt;JSONDecoder.snakeCase&lt;/span&gt; / &lt;span class="high"&gt;JSONEncoder.snakeCase&lt;/span&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Consistencia&lt;/strong&gt; en todo el proyecto (cliente y servidor).&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class="language-swift"&gt;extension JSONDecoder {
    convenience init(keyDecodingStrategy: KeyDecodingStrategy) {
        self.init()
        self.keyDecodingStrategy = keyDecodingStrategy
    }

    static var snakeCase: JSONDecoder {
        .init(keyDecodingStrategy: .convertFromSnakeCase)
    }
}

extension JSONEncoder {
    convenience init(keyEncodingStrategy: KeyEncodingStrategy) {
        self.init()
        self.keyEncodingStrategy = keyEncodingStrategy
    }

    static var snakeCase: JSONEncoder {
        .init(keyEncodingStrategy: .convertToSnakeCase)
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;Nota: si un modelo necesita un nombre de clave específico, puedes seguir usando &lt;span class="high"&gt;CodingKeys&lt;/span&gt; localmente; la estrategia &lt;span class="high"&gt;snake_case&lt;/span&gt; actuará como valor por defecto.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;Resultado&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Ejemplos de uso&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-swift"&gt;// iOS / macOS: leer datos
let data: Data = ...
let user = try JSONDecoder.snakeCase
    .decode(UserDTO.self, from: data)

// iOS / macOS: enviar datos
let body = CreateUserDTO(firstName: &amp;quot;Ada&amp;quot;, lastName: &amp;quot;Lovelace&amp;quot;)
request.httpBody = try JSONEncoder.snakeCase.encode(body)

// Vapor
let input = try req.content
    .decode(CreateUserDTO.self, using: JSONDecoder.snakeCase)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Con estos atajos obtenemos:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Menos errores&lt;/strong&gt; y mayor &lt;strong&gt;legibilidad&lt;/strong&gt;: las propiedades permanecen en camelCase idiomático Swift.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Interoperabilidad&lt;/strong&gt; inmediata con APIs legacy en snake_case.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Configuración única&lt;/strong&gt; y reutilizable en todo el proyecto (tests incluidos).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Esto estandariza cómo serializamos/parseamos JSON sin sacrificar claridad ni control fino cuando hace falta.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Keep coding, keep running&lt;/strong&gt; 🏃‍♂️&lt;/p&gt;
&lt;hr /&gt;</content>
    <link rel="alternate" href="https://jcalderita.com/es/blog/json-snakecase/"></link>
    <media:content medium="image" url="https://jcalderita.com/static/blog/JsonSnakeCase.webp"></media:content>
  </entry>
  <entry>
    <id>https://jcalderita.com/es/blog/array-dictionary/</id>
    <title>Array to dictionary</title>
    <updated>2025-12-03T00:00:00Z</updated>
    <summary>Convierte un Array de elementos Identifiable (ID opcional UUID?) en un diccionario [UUID: Element], ignorando IDs nulos y manteniendo acceso O(1).</summary>
    <content type="html">&lt;hr /&gt;
&lt;h2&gt;Problema&lt;/h2&gt;
&lt;p&gt;Al trabajar con listas de modelos (por ejemplo, resultados, eventos o usuarios), a menudo necesitamos acceso &lt;span class="high"&gt;O(1)&lt;/span&gt; por identificador para búsquedas, merges o deduplicación.&lt;/p&gt;
&lt;p&gt;Sin embargo, en muchos dominios el &lt;strong&gt;id&lt;/strong&gt; puede ser opcional (&lt;span class="high"&gt;UUID?&lt;/span&gt;) hasta que el backend lo asigne. Si construimos un diccionario directamente, aparecen dos fricciones:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Hay que &lt;strong&gt;filtrar los elementos sin ID&lt;/strong&gt; para evitar entradas inválidas.&lt;/li&gt;
&lt;li&gt;Debemos &lt;strong&gt;garantizar unicidad&lt;/strong&gt; de las claves o el constructor de &lt;span class="high"&gt;Dictionary(uniqueKeysWithValues:)&lt;/span&gt; fallará en tiempo de ejecución si hay duplicados.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;Solución&lt;/h2&gt;
&lt;p&gt;Extendemos &lt;span class="high"&gt;Array&lt;/span&gt; (cuando sus elementos son &lt;span class="high"&gt;Identifiable&lt;/span&gt; con &lt;span class="high"&gt;ID == UUID?&lt;/span&gt;) para exponer &lt;span class="high"&gt;toDictionary()&lt;/span&gt;.&lt;br /&gt;
La función usa &lt;span class="high"&gt;compactMap&lt;/span&gt; para &lt;strong&gt;descartar elementos sin ID&lt;/strong&gt; y construye el diccionario con &lt;span class="high"&gt;Dictionary(uniqueKeysWithValues:)&lt;/span&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Precondición&lt;/strong&gt;: los IDs existentes deben ser &lt;strong&gt;únicos&lt;/strong&gt; en la colección. Si esperas colisiones, considera una variante con &lt;span class="high"&gt;Dictionary(_, uniquingKeysWith:)&lt;/span&gt; para resolver duplicados.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-swift"&gt;extension Array where Element: Identifiable, Element.ID == UUID? {
    func toDictionary() -&amp;gt; [UUID: Element] {
        Dictionary(uniqueKeysWithValues: self.compactMap {
            guard let id = $0.id else { return nil }
            return (id, $0) }
        )
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Notas:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Los elementos con &lt;span class="high"&gt;id == nil&lt;/span&gt; &lt;strong&gt;no&lt;/strong&gt; se incluyen.&lt;/li&gt;
&lt;li&gt;El acceso por clave es &lt;span class="high"&gt;O(1)&lt;/span&gt; y simplifica merges/joins en memoria.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;Resultado&lt;/h2&gt;
&lt;p&gt;Un helper pequeño y claro para pasar de un array a un mapa por ID:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Más rápido de consultar&lt;/strong&gt; &lt;span class="high"&gt;O(1)&lt;/span&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Más seguro&lt;/strong&gt;: evita insertar elementos sin identificador.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Más expresivo&lt;/strong&gt; y reutilizable en servicios y view models.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Keep coding, keep running&lt;/strong&gt; 🏃‍♂️&lt;/p&gt;
&lt;hr /&gt;</content>
    <link href="https://jcalderita.com/es/blog/array-dictionary/" rel="alternate"></link>
    <media:content url="https://jcalderita.com/static/blog/ToDictionary.webp" medium="image"></media:content>
  </entry>
  <entry>
    <id>https://jcalderita.com/es/blog/schedule-queues/</id>
    <title>Schedule Queues</title>
    <updated>2025-11-26T00:00:00Z</updated>
    <summary>Programa un mismo ScheduledJob varias veces por hora con una API expresiva: cada N minutos, sin repetir configuración ni cron strings.</summary>
    <content type="html">&lt;hr /&gt;
&lt;h2&gt;Problema&lt;/h2&gt;
&lt;p&gt;Cuando usamos &lt;span class="high"&gt;Vapor Queues&lt;/span&gt; para planificar trabajos, la API típica es:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-swift"&gt;app.queues
    .schedule(MyJob()).hourly().at(0)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Esto planifica cada hora. Si queremos ejecutarlo &lt;strong&gt;cada N minutos&lt;/strong&gt; (p. ej., cada 5, 10 o 15), tenemos que registrar manualmente varios &lt;span class="high"&gt;.at(…)&lt;/span&gt;, lo que acaba en &lt;strong&gt;código repetitivo&lt;/strong&gt;, propenso a errores (minutos duplicados/olvidados) y poco legible.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Solución&lt;/h2&gt;
&lt;p&gt;Extraemos un helper sobre &lt;span class="high"&gt;Application.Queues&lt;/span&gt; que registra el mismo &lt;span class="high"&gt;ScheduledJob&lt;/span&gt; &lt;strong&gt;múltiples veces dentro de la hora&lt;/strong&gt; usando un &lt;span class="high"&gt;stride&lt;/span&gt; con paso &lt;span class="high"&gt;minutes&lt;/span&gt;. Así, con una sola llamada expresamos: &lt;em&gt;“ejecútalo cada N minutos”&lt;/em&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-swift"&gt;extension Application.Queues {
    func scheduleEvery(
        _ job: ScheduledJob, 
        minutes: Int
    ) {
        for minuteOffset in stride(
            from: 0, 
            to: 60, 
            by: minutes
        ) {
            schedule(job).hourly()
                .at(.init(integerLiteral: minuteOffset))
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Puntos clave:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Usa &lt;span class="high"&gt;stride(from: 0, to: 60, by: minutes)&lt;/span&gt; para generar los offsets de minuto dentro de la hora.&lt;/li&gt;
&lt;li&gt;Por cada offset se registra &lt;span class="high"&gt;hourly().at(…)&lt;/span&gt;, evitando duplicar lógica.&lt;/li&gt;
&lt;li&gt;Si &lt;span class="high"&gt;minutes&lt;/span&gt; &lt;strong&gt;no divide 60&lt;/strong&gt;, la última ejecución será el mayor múltiplo &amp;lt; 60 (p. ej., &lt;span class="high"&gt;minutes = 7&lt;/span&gt; ⇒ 0, 7, 14, …, 56).&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;Resultado&lt;/h2&gt;
&lt;p&gt;Ejemplo de uso:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-swift"&gt;func configure(_ app: Application) throws {
    app.queues.scheduleEvery(MyJob(), minutes: 5)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Una API &lt;strong&gt;concisa y autoexpresiva&lt;/strong&gt; para tareas periódicas cortas:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Menos boilerplate&lt;/strong&gt;: una sola llamada registra todos los disparos.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Menos errores&lt;/strong&gt;: sin listas manuales de &lt;span class="high"&gt;.at(…)&lt;/span&gt; ni minutos duplicados.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Legible y testeable&lt;/strong&gt;: el patrón es evidente y fácil de verificar.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Keep coding, keep running&lt;/strong&gt; 🏃‍♂️&lt;/p&gt;
&lt;hr /&gt;</content>
    <link rel="alternate" href="https://jcalderita.com/es/blog/schedule-queues/"></link>
    <media:content url="https://jcalderita.com/static/blog/ScheduleQueues.webp" medium="image"></media:content>
  </entry>
  <entry>
    <id>https://jcalderita.com/es/blog/task-list/</id>
    <title>Async Task List</title>
    <updated>2025-11-20T00:00:00Z</updated>
    <summary>Implementación de un sistema robusto para la gestión de listas de tareas asíncronas con persistencia de estado en base de datos utilizando Swift y Vapor.</summary>
    <content type="html">&lt;hr /&gt;
&lt;h2&gt;Problema&lt;/h2&gt;
&lt;p&gt;En sistemas distribuidos con microservicios, un proceso complejo puede generar dificultades de control y fiabilidad. Una solución es &lt;strong&gt;dividirlo&lt;/strong&gt; en tareas &lt;strong&gt;atómicas&lt;/strong&gt;, lo que permite un control más granular del &lt;strong&gt;flujo&lt;/strong&gt;, mejorar la &lt;strong&gt;observabilidad&lt;/strong&gt;, asegurar la &lt;strong&gt;idempotencia&lt;/strong&gt; y facilitar la &lt;strong&gt;recuperación&lt;/strong&gt; ante fallos.&lt;/p&gt;
&lt;p&gt;El reto es diseñar un patrón que permita una &lt;strong&gt;degradación controlada&lt;/strong&gt;, de modo que el fallo de una tarea no afecte al flujo completo, garantizando &lt;strong&gt;resiliencia&lt;/strong&gt; y &lt;strong&gt;tolerancia a fallos&lt;/strong&gt; en entornos de producción.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Solución&lt;/h2&gt;
&lt;p&gt;Crear un &lt;span class="high"&gt;task executor&lt;/span&gt; que orquesta tareas asíncronas.&lt;br /&gt;
La función &lt;span class="high"&gt;execute&lt;/span&gt; envuelve cada tarea, gestiona errores con &lt;span class="high"&gt;do-catch&lt;/span&gt; como &lt;span class="high"&gt;circuit breaker&lt;/span&gt;, actualiza el estado en caso de &lt;strong&gt;éxito&lt;/strong&gt;, registra y persiste los fallos, y asegura una &lt;strong&gt;transacción atómica&lt;/strong&gt; para mantener la consistencia de datos.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-swift"&gt;func execute(
    status: StatusEnum,
    process: ProcessModel,
    _ work: () async throws -&amp;gt; ProcessModel
) async throws {
    do {
        let job = try await work()
        process.setStatus(job.status)
    } catch {
        process.setError(type: status, message: &amp;quot;\(error)&amp;quot;)
    }
    try await repo.updateProcess(process)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Resultado&lt;/h2&gt;
&lt;p&gt;Esta implementación ofrece una &lt;strong&gt;abstracción de alto nivel&lt;/strong&gt; que permite una &lt;strong&gt;orquestación precisa&lt;/strong&gt; de procesos asíncronos complejos con garantías de &lt;strong&gt;atomicidad&lt;/strong&gt; y &lt;strong&gt;durabilidad&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;🎯 &lt;strong&gt;Separation of Concerns:&lt;/strong&gt; cada tarea aísla su contexto de ejecución y su gestión de errores.&lt;br /&gt;&lt;br /&gt;
🔄 &lt;strong&gt;Workflow Orchestration:&lt;/strong&gt; posibilita encadenar tareas mediante el pipeline pattern.&lt;br /&gt;&lt;br /&gt;
📊 &lt;strong&gt;Observability:&lt;/strong&gt; genera un audit trail completo para depuración y monitorización.&lt;br /&gt;&lt;br /&gt;
⚡ &lt;strong&gt;Performance:&lt;/strong&gt; mantiene alta concurrencia sin comprometer la consistencia de datos.&lt;br /&gt;&lt;br /&gt;
🛡️ &lt;strong&gt;Resilience:&lt;/strong&gt; incorpora fail-fast y recovery patterns automáticos.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-swift"&gt;try await execute(status: .loadImages, process: process) {
    // Your implementation
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Keep coding, keep running&lt;/strong&gt; 🏃‍♂️&lt;/p&gt;
&lt;hr /&gt;</content>
    <link rel="alternate" href="https://jcalderita.com/es/blog/task-list/"></link>
    <media:content medium="image" url="https://jcalderita.com/static/blog/TaskList.webp"></media:content>
  </entry>
  <entry>
    <id>https://jcalderita.com/es/blog/multi-step/</id>
    <title>Multi-Step Process</title>
    <updated>2025-11-12T00:00:00Z</updated>
    <summary>Patrón en Swift para orquestar procesos complejos de varios pasos con control de estado y recuperación ante fallos.</summary>
    <content type="html">&lt;hr /&gt;
&lt;h2&gt;Problema&lt;/h2&gt;
&lt;p&gt;En sistemas distribuidos y procesos de backend, es común que una operación compleja requiera ejecutar &lt;strong&gt;múltiples pasos secuenciales&lt;/strong&gt; con dependencias entre sí: creación de ficheros, subida, generación de respuestas, validación, limpieza, etc.&lt;br /&gt;
Controlar este flujo de estados es crítico para:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Garantizar que cada paso se ejecute en el &lt;strong&gt;orden correcto&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Permitir la &lt;strong&gt;recuperación&lt;/strong&gt; en caso de error o reinicio del sistema.&lt;/li&gt;
&lt;li&gt;Mantener la &lt;strong&gt;consistencia de datos&lt;/strong&gt; incluso si el proceso se interrumpe.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Sin una estrategia clara, el código puede volverse frágil, difícil de escalar y propenso a errores.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Solución&lt;/h2&gt;
&lt;p&gt;Se implementa una función de &lt;strong&gt;gestión de estado por etapas&lt;/strong&gt; que controla el avance de un &lt;span class="high"&gt;ProcessModel&lt;/span&gt; a través de su ciclo de vida.&lt;br /&gt;
La idea es &lt;strong&gt;encapsular la lógica de transición de estado&lt;/strong&gt; en una única función que evalúa el estado actual y ejecuta la acción correspondiente, hasta que el proceso alcanza su estado final.&lt;/p&gt;
&lt;p&gt;El patrón se apoya en un bucle &lt;span class="high"&gt;repeat-while&lt;/span&gt; que vuelve a evaluar el estado tras cada operación, garantizando que las transiciones ocurran de forma &lt;strong&gt;determinista&lt;/strong&gt; y &lt;strong&gt;resiliente&lt;/strong&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-swift"&gt;func checkProcess(
    _ process: ProcessModel
) async throws {
    var process = process
    var status = process.status

    repeat {
        status = process.status
        process = try await checkStatus(process)
    } while status != process.status
}

func checkStatus(
    _ process: ProcessModel
) async throws -&amp;gt; ProcessModel {
    switch process.status {
        case .filesCreated: 
            try await _uploadFiles(process)
        case .filesUploaded: 
            try await _createResponses(process)
        case .responsesCreated, .responsesReasoning: 
            try await _checkResponses(process)
        case .responsesCompleted: 
            try await _deleteFiles(process)
        case .filesDeleted: 
            try await _deleteResponses(process)
        case .responsesDeleted: 
            try await _finishResponses(process)
        case .responsesFinished: 
            try await _deleteProcess(process)
        default: process
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Claves de la implementación:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Centralización de estados&lt;/strong&gt;: un único punto de control define todas las transiciones.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Reevaluación continua&lt;/strong&gt;: el bucle &lt;span class="high"&gt;repeat-while&lt;/span&gt; permite avanzar automáticamente mientras haya cambios de estado.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Aislamiento de operaciones&lt;/strong&gt;: cada caso del &lt;span class="high"&gt;switch&lt;/span&gt; delega en funciones especializadas (&lt;span class="high"&gt;_uploadFiles&lt;/span&gt;, &lt;span class="high"&gt;_createResponses&lt;/span&gt;, etc.), manteniendo el código limpio y testeable.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;Resultado&lt;/h2&gt;
&lt;p&gt;Este patrón de &lt;strong&gt;gestión de procesos multi‑etapa&lt;/strong&gt; aporta:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Resiliencia&lt;/strong&gt;: cada transición es atómica y se puede reintentar si ocurre un fallo.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Escalabilidad&lt;/strong&gt;: agregar nuevos pasos solo requiere añadir un nuevo caso al &lt;span class="high"&gt;switch&lt;/span&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Claridad&lt;/strong&gt;: el flujo completo del proceso se entiende con una sola lectura.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Ejemplo de aplicación: pipelines de procesamiento de datos, flujos de publicación de contenido, o cualquier proceso de larga duración que requiera &lt;strong&gt;control preciso de cada etapa&lt;/strong&gt; sin comprometer la integridad de los datos.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Keep coding, keep running&lt;/strong&gt; 🏃‍♂️&lt;/p&gt;
&lt;hr /&gt;</content>
    <link href="https://jcalderita.com/es/blog/multi-step/" rel="alternate"></link>
    <media:content url="https://jcalderita.com/static/blog/MultiStep.webp" medium="image"></media:content>
  </entry>
  <entry>
    <id>https://jcalderita.com/es/blog/concurrent-map/</id>
    <title>Concurrent Map</title>
    <updated>2025-10-27T00:00:00Z</updated>
    <summary>Crea una extensión concurrentMap sobre Sequence con concurrencia estructurada de Swift para ejecutar transformaciones async en paralelo de forma segura y eficiente.</summary>
    <content type="html">&lt;hr /&gt;
&lt;h2&gt;Problema&lt;/h2&gt;
&lt;p&gt;Cuando procesamos colecciones en Swift, el método &lt;span class="high"&gt;map&lt;/span&gt; ejecuta las transformaciones de forma secuencial.&lt;br /&gt;
En operaciones intensivas o que implican I/O —como peticiones HTTP, lectura de archivos o consultas de base de datos— esto puede ser un cuello de botella.&lt;br /&gt;
El objetivo es &lt;strong&gt;aprovechar la concurrencia&lt;/strong&gt; para ejecutar múltiples transformaciones en paralelo, garantizando seguridad con &lt;span class="high"&gt;Sendable&lt;/span&gt; y manejo de errores.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Solución&lt;/h2&gt;
&lt;p&gt;Se crea una extensión de &lt;span class="high"&gt;Sequence&lt;/span&gt; que añade &lt;span class="high"&gt;concurrentMap&lt;/span&gt;.&lt;br /&gt;
Internamente usa &lt;span class="high"&gt;withThrowingTaskGroup&lt;/span&gt;, lo que permite:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Lanzar una tarea por cada elemento de la secuencia.&lt;/li&gt;
&lt;li&gt;Ejecutar todas las transformaciones en paralelo, respetando el modelo de concurrencia estructurada de Swift.&lt;/li&gt;
&lt;li&gt;Propagar automáticamente el primer error que se produzca, cancelando las tareas restantes.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;El uso de &lt;span class="high"&gt;@Sendable&lt;/span&gt; asegura que tanto los elementos como el resultado sean seguros en entornos concurrentes.&lt;br /&gt;
De esta manera, cualquier operación asíncrona puede beneficiarse de la &lt;strong&gt;ejecución en paralelo&lt;/strong&gt; sin sacrificar la claridad del código.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-swift"&gt;extension Sequence where Element: Sendable {
    func concurrentMap&amp;lt;T: Sendable&amp;gt;(
        _ transform: @escaping @Sendable (Element) async throws -&amp;gt; T
    ) async throws -&amp;gt; [T] {
        try await withThrowingTaskGroup(of: T.self) { group in
            for element in self {
                group.addTask {
                    try await transform(element)
                }
            }
            return try await group.reduce(into: []) { 
                $0.append($1) 
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Resultado&lt;/h2&gt;
&lt;p&gt;Con &lt;span class="high"&gt;concurrentMap&lt;/span&gt; se consigue una &lt;strong&gt;ganancia de rendimiento significativa&lt;/strong&gt; en operaciones asíncronas que pueden ejecutarse en paralelo.&lt;br /&gt;
El patrón respeta las reglas de &lt;span class="high"&gt;concurrencia estructurada&lt;/span&gt; de Swift, evita &lt;em&gt;data races&lt;/em&gt; y mantiene el mismo estilo declarativo que &lt;span class="high"&gt;map&lt;/span&gt;, por lo que su adopción en proyectos existentes es sencilla.&lt;/p&gt;
&lt;p&gt;Ejemplo de uso:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-swift"&gt;let urls: [URL] = [...]
let contents = try await urls.concurrentMap {
    try await fetchContent(from: $0)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Este enfoque es ideal para peticiones HTTP en lotes, procesamiento de imágenes, lectura masiva de datos o cualquier escenario que requiera &lt;strong&gt;máximo paralelismo seguro&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Si necesitas &lt;strong&gt;orden garantizado&lt;/strong&gt; o limitar el consumo de recursos a una tarea a la vez, lo cubrí en &lt;a href="/es/blog/async-map/"&gt;Async Map&lt;/a&gt;. Y si quieres combinar ambas estrategias — procesamiento concurrente dentro de chunks controlados — lo construí en &lt;a href="/es/blog/async-concurrent-map/"&gt;Async Concurrent Map&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Keep coding, keep running&lt;/strong&gt; 🏃‍♂️&lt;/p&gt;
&lt;hr /&gt;</content>
    <link rel="alternate" href="https://jcalderita.com/es/blog/concurrent-map/"></link>
    <media:content medium="image" url="https://jcalderita.com/static/blog/ConcurrentMap.webp"></media:content>
  </entry>
  <entry>
    <id>https://jcalderita.com/es/blog/async-map/</id>
    <title>Async Map</title>
    <updated>2025-10-20T00:00:00Z</updated>
    <summary>Crea una extensión asyncMap secuencial sobre Sequence en Swift para transformar colecciones con async/await preservando el orden y limitando recursos.</summary>
    <content type="html">&lt;hr /&gt;
&lt;h2&gt;Problema&lt;/h2&gt;
&lt;p&gt;Cuando procesamos colecciones con transformaciones asíncronas, el método &lt;span class="high"&gt;map&lt;/span&gt; no sirve porque sus closures no pueden ser &lt;span class="high"&gt;async&lt;/span&gt;.&lt;br /&gt;
Una solución ingenua sería usar un bucle &lt;span class="high"&gt;for&lt;/span&gt; con &lt;span class="high"&gt;await&lt;/span&gt;, pero se pierde legibilidad y un manejo de errores homogéneo.&lt;br /&gt;
El objetivo es disponer de un &lt;span class="high"&gt;map&lt;/span&gt; &lt;strong&gt;secuencial&lt;/strong&gt; que acepte funciones &lt;span class="high"&gt;async throws&lt;/span&gt;, preserve el orden de los elementos y simplifique el flujo de trabajo.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Solución&lt;/h2&gt;
&lt;p&gt;Se crea una extensión de &lt;span class="high"&gt;Sequence&lt;/span&gt; que añade &lt;span class="high"&gt;asyncMap&lt;/span&gt;.&lt;br /&gt;
Su ejecución es &lt;strong&gt;secuencial&lt;/strong&gt; dentro de la misma tarea, lo que permite:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Mantener el orden determinista de los resultados.&lt;/li&gt;
&lt;li&gt;Limitar el consumo de recursos ejecutando solo una operación a la vez.&lt;/li&gt;
&lt;li&gt;Propagar de manera uniforme el primer error que se produzca, deteniendo el proceso.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class="language-swift"&gt;extension Sequence {
    @inlinable
    func asyncMap&amp;lt;T&amp;gt;(
        _ transform: @escaping @Sendable (Element) async throws -&amp;gt; T
    ) async throws -&amp;gt; [T] {
        var results: [T] = []
        results.reserveCapacity(underestimatedCount)
        for element in self {
            let value = try await transform(element)
            results.append(value)
        }
        return results
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Resultado&lt;/h2&gt;
&lt;p&gt;Con &lt;span class="high"&gt;asyncMap&lt;/span&gt; se obtiene un flujo claro y seguro para transformaciones asíncronas &lt;strong&gt;sin paralelismo&lt;/strong&gt;.&lt;br /&gt;
Es especialmente útil cuando:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Necesitamos orden garantizado.&lt;/li&gt;
&lt;li&gt;Debemos limitar el consumo de recursos (una tarea en vuelo).&lt;/li&gt;
&lt;li&gt;Existe dependencia entre las operaciones.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Si necesitas &lt;strong&gt;ejecución en paralelo&lt;/strong&gt; en lugar de secuencial, exploré ese enfoque en &lt;a href="/es/blog/concurrent-map/"&gt;Concurrent Map&lt;/a&gt;. Y si tus llamadas secuenciales alcanzan límites de tasa, añadí un mecanismo de timeout en &lt;a href="/es/blog/async-map-timeout/"&gt;Async Map Timeout&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Ejemplo de uso:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-swift"&gt;let urls: [URL] = [...]
let contents = try await urls.asyncMap {
    try await fetchContent(from: $0)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Keep coding, keep running&lt;/strong&gt; 🏃‍♂️&lt;/p&gt;
&lt;hr /&gt;</content>
    <link href="https://jcalderita.com/es/blog/async-map/" rel="alternate"></link>
    <media:content medium="image" url="https://jcalderita.com/static/blog/AsyncMap.webp"></media:content>
  </entry>
  <entry>
    <id>https://jcalderita.com/es/blog/guard-log/</id>
    <title>Guard and LogError</title>
    <updated>2025-10-13T00:00:00Z</updated>
    <summary>Crea una función genérica en Swift que combina guard-let, logging de errores y lanzamiento de excepciones en una sola línea reutilizable para backends Vapor.</summary>
    <content type="html">&lt;hr /&gt;
&lt;h2&gt;Problema&lt;/h2&gt;
&lt;p&gt;En múltiples secciones de mi código necesito ejecutar tres operaciones cada vez que manejo un &lt;span class="high"&gt;Optional&lt;/span&gt;:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Desempaquetar&lt;/strong&gt; el contenido del &lt;span class="high"&gt;Optional&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Registrar un error&lt;/strong&gt; en el sistema de logs si el valor es &lt;span class="high"&gt;nil&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Lanzar una excepción&lt;/strong&gt; cuando el valor no existe&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Este patrón se repite frecuentemente, generando código duplicado y reduciendo la mantenibilidad.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Solución&lt;/h2&gt;
&lt;p&gt;La estrategia consiste en &lt;strong&gt;encapsular las tres operaciones&lt;/strong&gt; en funciones reutilizables que trabajen de forma coordinada.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;&lt;strong&gt;Función auxiliar:&lt;/strong&gt;&lt;/em&gt; &lt;span class="high"&gt;logError&lt;/span&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-swift"&gt;func logError(
    _ message: String, 
    status: HTTPResponseStatus
) throws -&amp;gt; Never {
    self.logMessage(message, level: .error)
    throw Abort(status, reason: message)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Esta función ejecuta el &lt;strong&gt;registro del error&lt;/strong&gt; en el sistema de logs y posteriormente &lt;strong&gt;termina la ejecución&lt;/strong&gt; lanzando una excepción &lt;span class="high"&gt;Abort&lt;/span&gt;. El tipo de retorno &lt;span class="high"&gt;Never&lt;/span&gt; es fundamental, ya que indica al compilador que esta función &lt;strong&gt;nunca retorna normalmente&lt;/strong&gt;, garantizando que la ejecución se interrumpa completamente.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;&lt;strong&gt;Función principal:&lt;/strong&gt;&lt;/em&gt; &lt;span class="high"&gt;guardAndLogError&lt;/span&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-swift"&gt;func guardAndLogError&amp;lt;T&amp;gt;(
    _ optional: T?,
    message: String,
    status: HTTPResponseStatus = .noContent
) throws -&amp;gt; T {
    guard let optional else {
        try logError(message, status: status)
    }
    return optional
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Esta &lt;strong&gt;función genérica&lt;/strong&gt; implementa el patrón completo:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Utiliza &lt;span class="high"&gt;guard let&lt;/span&gt; para &lt;strong&gt;desempaquetar el Optional&lt;/strong&gt; de forma segura&lt;/li&gt;
&lt;li&gt;Si el valor es &lt;span class="high"&gt;nil&lt;/span&gt;, invoca &lt;span class="high"&gt;logError()&lt;/span&gt; para registrar el fallo y terminar la ejecución&lt;/li&gt;
&lt;li&gt;Si contiene un valor, lo &lt;strong&gt;retorna exitosamente&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;La &lt;strong&gt;genericidad&lt;/strong&gt; &lt;span class="high"&gt;T&lt;/span&gt; permite usar esta función con cualquier tipo de dato opcional.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Resultado&lt;/h2&gt;
&lt;p&gt;Con esta implementación, &lt;strong&gt;una sola línea de código&lt;/strong&gt; ejecuta las tres operaciones requeridas: desempaquetado seguro, logging de errores y manejo de excepciones.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-swift"&gt;let fileName = try guardAndLogError(
    fileName, 
    message: &amp;quot;Valor fileName no encontrado&amp;quot;
)
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Beneficios&lt;/h2&gt;
&lt;p&gt;✅ &lt;strong&gt;Reducción de código duplicado&lt;/strong&gt;&lt;br /&gt;&lt;br /&gt;
✅ &lt;strong&gt;Manejo consistente de errores&lt;/strong&gt;&lt;br /&gt;&lt;br /&gt;
✅ &lt;strong&gt;Logs centralizados y estructurados&lt;/strong&gt;&lt;br /&gt;&lt;br /&gt;
✅ &lt;strong&gt;Reutilización mediante genericidad&lt;/strong&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Extensibilidad&lt;/h2&gt;
&lt;p&gt;Este patrón puede &lt;strong&gt;extenderse&lt;/strong&gt; para casos de uso más específicos:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-swift"&gt;// Para validaciones booleanas
func guardAndLogError(
    _ condition: Bool, 
    message: String
) throws { ... }

// Para arrays
func guardAndLogError&amp;lt;T&amp;gt;(
    _ optionals: T?..., 
    message: String
) throws -&amp;gt; [T] { ... }

// Para tuplas
func guardAndLogError&amp;lt;T, U&amp;gt;(
    _ first: T?, 
    _ second: U?
    , message: String
) throws -&amp;gt; (T, U) { ... }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Keep coding, keep running&lt;/strong&gt; 🏃‍♂️&lt;/p&gt;
&lt;hr /&gt;</content>
    <link rel="alternate" href="https://jcalderita.com/es/blog/guard-log/"></link>
    <media:content url="https://jcalderita.com/static/blog/GuardLog.webp" medium="image"></media:content>
  </entry>
  <entry>
    <id>https://jcalderita.com/es/blog/schema-space/</id>
    <title>Esquemas y Espacios</title>
    <updated>2025-10-06T00:00:00Z</updated>
    <summary>Aprende a usar la propiedad space en los modelos Fluent de Vapor para organizar tablas en namespaces, y evita un sutil bug en la declaración de tipo opcional.</summary>
    <content type="html">&lt;hr /&gt;
&lt;h2&gt;Esquema y Espacio de Nombres&lt;/h2&gt;
&lt;p&gt;Al definir un modelo de datos en Vapor, es posible especificar el &lt;span class="high"&gt;schema&lt;/span&gt;, que corresponde al nombre de la tabla en la base de datos. Sin embargo, para mantener una arquitectura organizada, es recomendable agrupar las tablas en diferentes espacios de nombres según criterios funcionales, en lugar de concentrarlas todas en el esquema predeterminado. Esta práctica facilita la separación lógica por dominios o módulos de la aplicación.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Implementación&lt;/h2&gt;
&lt;p&gt;Para asignar una tabla a un espacio de nombres específico, se debe sobrescribir la propiedad estática &lt;span class="high"&gt;space&lt;/span&gt; en el modelo. Esta propiedad permite definir el namespace donde residirá la tabla, proporcionando una organización más granular de la estructura de base de datos.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-swift"&gt;public final class LocationCityModel: Model {
    public static let schema = &amp;quot;cities&amp;quot;
    public static let space: String? = &amp;quot;location&amp;quot;

    @ID() public var id: UUID?
    @Field(.name) public var name: String

    public init() { }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Consideración&lt;/h2&gt;
&lt;p&gt;Es fundamental declarar la propiedad &lt;span class="high"&gt;space&lt;/span&gt; como tipo &lt;span class="high"&gt;String?&lt;/span&gt; &lt;strong&gt;(opcional)&lt;/strong&gt;. Si se declara como &lt;span class="high"&gt;String&lt;/span&gt; &lt;strong&gt;(no opcional)&lt;/strong&gt;, no se sobrescribirá la propiedad heredada del protocolo &lt;span class="high"&gt;Model&lt;/span&gt;, sino que se creará una nueva propiedad con el mismo nombre. Esto ocasionará que el framework ignore la configuración del espacio de nombres, manteniendo las tablas en el esquema predeterminado sin indicación de error aparente.&lt;/p&gt;
&lt;p&gt;Si te preguntas cómo crear estos espacios de nombres automáticamente en la base de datos, explico cómo convertirlo en una migración en &lt;a href="/es/blog/migrate-spaces/"&gt;Migrate Spaces&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Keep coding, keep running&lt;/strong&gt; 🏃‍♂️&lt;/p&gt;
&lt;hr /&gt;</content>
    <link href="https://jcalderita.com/es/blog/schema-space/" rel="alternate"></link>
    <media:content url="https://jcalderita.com/static/blog/SchemaSpace.webp" medium="image"></media:content>
  </entry>
  <entry>
    <id>https://jcalderita.com/es/blog/partial-index/</id>
    <title>Índices Parciales</title>
    <updated>2025-09-29T00:00:00Z</updated>
    <summary>Extiende SQLCreateIndexBuilder de Vapor para crear índices parciales con cláusulas WHERE sobre columnas NULL, evitando SQL crudo y manteniendo type safety.</summary>
    <content type="html">&lt;hr /&gt;
&lt;h2&gt;Índices Parciales&lt;/h2&gt;
&lt;p&gt;Un &lt;strong&gt;índice parcial&lt;/strong&gt; es un índice que se crea únicamente sobre un subconjunto de filas de una tabla, definido mediante una condición &lt;span class="high"&gt;WHERE&lt;/span&gt; específica. En lugar de indexar todas las filas, el índice solo incluye aquellas que cumplen ciertos criterios, lo que puede mejorar el rendimiento y reducir el espacio utilizado.&lt;/p&gt;
&lt;p&gt;En mi caso particular, necesitaba indexar campos en función de si sus valores eran &lt;span class="high"&gt;NULL&lt;/span&gt; o &lt;span class="high"&gt;NOT NULL&lt;/span&gt;. Por ejemplo, los siguientes índices parciales en SQL:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sql"&gt;CREATE INDEX index_field_null 
ON table(field) 
WHERE field IS NULL;

CREATE INDEX index_field_not_null 
ON table(field) 
WHERE field IS NOT NULL;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;También es posible crear índices parciales que involucren múltiples columnas:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-sql"&gt;CREATE INDEX index_field1_null_field2_null 
ON table(field1, field2) 
WHERE field1 IS NULL AND field2 IS NULL;

CREATE INDEX index_field1_not_null_field2_not_null 
ON table(field1, field2) 
WHERE field1 IS NOT NULL AND field2 IS NOT NULL;
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;No todas las bases de datos admiten índices parciales, en mi caso particular estoy usando &lt;span class="high"&gt;PostgreSQL&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;Problema&lt;/h2&gt;
&lt;p&gt;Por defecto, Vapor no ofrece soporte directo para la creación de índices parciales, ya que la generación estándar de índices no contempla la inclusión de una cláusula &lt;span class="high"&gt;WHERE&lt;/span&gt; en la definición del índice.&lt;/p&gt;
&lt;p&gt;El siguiente fragmento de código crea un índice normal, ya sea sobre uno o varios campos, pero no permite especificar una condición para un índice parcial:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-swift"&gt;try await db.sqlDatabase
    .create($0.key)
    .on(table)
    .colums($0.colums)
    .run()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Este código funciona para crear índices simples, pero carece de la capacidad para agregar un predicado &lt;span class="high"&gt;WHERE&lt;/span&gt;.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Este fragmento ha sido adaptado por mí; el método &lt;span class="high"&gt;.create&lt;/span&gt; del constructor original expone más opciones de configuración, pero en este ejemplo muestro una implementación personalizada y simplificada que uso actualmente.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;Alternativa&lt;/h2&gt;
&lt;p&gt;La forma más directa de crear índices parciales es ejecutar sentencias SQL en crudo &lt;span class="high"&gt;raw SQL&lt;/span&gt;, tal como se mostró en los ejemplos iniciales. Esto implica construir un método para ejecutar en crudo las sentencias &lt;span class="high"&gt;CREATE INDEX&lt;/span&gt; con la cláusula &lt;span class="high"&gt;WHERE&lt;/span&gt; correspondiente.&lt;/p&gt;
&lt;p&gt;Aunque efectivo, este enfoque pierde la ventaja de la abstracción y seguridad que ofrece Vapor al construir migraciones y esquemas de base de datos mediante código Swift.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Solución&lt;/h2&gt;
&lt;p&gt;El builder &lt;span class="high"&gt;SQLCreateIndexBuilder&lt;/span&gt; admite un predicado opcional &lt;span class="high"&gt;predicate&lt;/span&gt;. Si este no es &lt;span class="high"&gt;nil&lt;/span&gt;, agrega una cláusula &lt;span class="high"&gt;WHERE&lt;/span&gt; al índice.&lt;/p&gt;
&lt;p&gt;Por ello, extendí este builder para incluir un método &lt;span class="high"&gt;where&lt;/span&gt; que acepta una lista de columnas y un tipo de índice parcial (por ejemplo, &lt;span class="high"&gt;null&lt;/span&gt; o &lt;span class="high"&gt;not null&lt;/span&gt;). Este método construye la expresión lógica adecuada para el predicado y la asigna al índice.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-swift"&gt;extension SQLCreateIndexBuilder {
    @discardableResult
    func `where`(
        _ columns: [FieldKey],
        _ partialIndex: SQLPartialIndexEnum?
    ) -&amp;gt; Self {
        guard let partialIndex else {
            return self
        }
        let op: SQLBinaryOperator = partialIndex == .null ? .is : .isNot

        let conditions = columns.map {
            self.where($0, op)
        }

        let combined: SQLBinaryExpression = conditions.dropFirst()
            .reduce(conditions[0]) { .init($0, .and, $1) }

        return self.where(combined)
    }

    private func `where`(
        _ column: FieldKey,
        _ binary: SQLBinaryOperator
    ) -&amp;gt; SQLBinaryExpression {
        .init(
            left: SQLIdentifier(column.description),
            op: binary,
            right: SQLLiteral.null
        )
    }

    private func `where`(_ expression: SQLExpression) -&amp;gt; Self {
        self.createIndex.predicate = expression
        return self
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Este código permite invocar el método &lt;span class="high"&gt;where&lt;/span&gt; para uno o varios campos, especificando si se desea un índice parcial para valores &lt;span class="high"&gt;NULL&lt;/span&gt; o &lt;span class="high"&gt;NOT NULL&lt;/span&gt;. Si no se proporciona un valor para &lt;span class="high"&gt;partialIndex&lt;/span&gt;, se devuelve el índice sin predicado, comportándose como un índice normal.&lt;/p&gt;
&lt;p&gt;La lógica consiste en crear una lista de expresiones binarias &lt;span class="high"&gt;SQLBinaryExpression&lt;/span&gt; para cada columna, combinándolas con el operador lógico &lt;span class="high"&gt;AND&lt;/span&gt; y asignando el resultado como predicado del índice.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Resultado&lt;/h2&gt;
&lt;p&gt;Con esta extensión, ahora puedo crear índices parciales de forma sencilla en Vapor, agregando únicamente una línea al código original:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-swift"&gt;try await db.sqlDatabase
    .create($0.key)
    .on(table)
    .colums($0.colums)
    .where($0.colums, $0.partial)
    .run()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Donde &lt;span class="high"&gt;$0.partial&lt;/span&gt; es un valor opcional que indica el tipo de índice parcial deseado &lt;span class="high"&gt;null&lt;/span&gt; o &lt;span class="high"&gt;not null&lt;/span&gt;.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Notas finales&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;Este enfoque está diseñado específicamente para índices parciales basados en la presencia o ausencia de valores &lt;span class="high"&gt;NULL&lt;/span&gt; en columnas. Sin embargo, la implementación puede adaptarse para soportar otras condiciones y casos de uso más complejos, simplemente modificando la construcción del predicado.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;La solución ofrece una forma limpia y reutilizable de crear índices parciales dentro del ecosistema Vapor, manteniendo la coherencia y seguridad del código Swift.&lt;/p&gt;
&lt;p&gt;Si además quieres organizar tus tablas en espacios de nombres usando la propiedad &lt;code&gt;space&lt;/code&gt; de Fluent, lo cubrí en &lt;a href="/es/blog/schema-space/"&gt;Esquemas y Espacios&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Keep coding, keep running&lt;/strong&gt; 🏃‍♂️&lt;/p&gt;
&lt;hr /&gt;</content>
    <link rel="alternate" href="https://jcalderita.com/es/blog/partial-index/"></link>
    <media:content medium="image" url="https://jcalderita.com/static/blog/PartialIndex.webp"></media:content>
  </entry>
  <entry>
    <id>https://jcalderita.com/es/blog/my-first-data-race/</id>
    <title>Mi primera condición de carrera</title>
    <updated>2025-09-22T00:00:00Z</updated>
    <summary>Un artículo técnico que explica cómo una refactorización para concurrencia en Swift llevó a una condición de carrera, y cómo resolverla usando patrones de actores.</summary>
    <content type="html">&lt;hr /&gt;
&lt;h2&gt;De refactor a condición de carrera en Swift&lt;/h2&gt;
&lt;p&gt;Durante un proceso de refactorización para convertir una funcionalidad previamente secuencial en una concurrente, me encontré con un problema clásico: garantizar la unicidad de los registros cuando múltiples tareas intentan crear eventos al mismo tiempo. Aunque el objetivo era mejorar el rendimiento procesando miles de eventos en paralelo usando Swift, la transición expuso un desafío típico de concurrencia: evitar duplicados y mantener la integridad de los datos bajo acceso simultáneo.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Actor con array&lt;/h2&gt;
&lt;pre&gt;&lt;code class="language-swift"&gt;private actor EventsActor {
    private var events: [SportEventModel] = []

    func getOrCreate(
        name: String, 
        cityId: UUID, 
        build: () async throws -&amp;gt; EventModel
    ) async throws -&amp;gt; EventModel {
        if let event = events.first(
            where: { 
                $0.normalizedName == name 
                &amp;amp;&amp;amp; $0.$city.id == cityId 
            }
        ) { return event }

        let event = try await build()
        events.append(event)
        return event
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Ventajas:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Simplicidad.&lt;/li&gt;
&lt;li&gt;Seguridad frente a condiciones de carrera.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Desventaja:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Búsqueda ineficiente en grandes volúmenes &lt;span class="high"&gt;O(n)&lt;/span&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;Actor con diccionario&lt;/h2&gt;
&lt;pre&gt;&lt;code class="language-swift"&gt;private actor EventsActor {
    private var events: [String: EventModel] = [:]

    func getOrCreate(
        name: String, 
        cityId: UUID, 
        build: () async throws -&amp;gt; EventModel
    ) async throws -&amp;gt; EventModel {
        let key = &amp;quot;\(name)\(cityId.uuidString)&amp;quot;
        if let event = events[key] { 
            return event 
        }
        let event = try await build()
        events[key] = event
        return event
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Ventajas:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Búsqueda e inserción rápida &lt;span class="high"&gt;O(1)&lt;/span&gt;.&lt;/li&gt;
&lt;li&gt;Ideal para grandes volúmenes de datos.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Bug:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Condición de carrera lógica&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;Condición de carrera lógica&lt;/h2&gt;
&lt;p&gt;Cuando múltiples tareas concurrentes intentan crear el mismo evento, todas pueden comprobar que no existe y proceder a crearlo al mismo tiempo.&lt;br /&gt;
Solo una de las instancias resultantes se almacena, mientras que el resto se convierten en “huérfanas”, lo que provoca inconsistencias y referencias rotas en otras estructuras de datos.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Por ejemplo: dos tareas concurrentes comprueban si un evento existe. Ambas ven que no, ambas lo crean, pero solo una sobrevive en el diccionario; la otra referencia ahora está perdida.&lt;/em&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Solución&lt;/h2&gt;
&lt;p&gt;Para evitar esto, es necesario serializar no solo el acceso sino también la creación por clave:&lt;br /&gt;
Si ya hay una creación en curso para esa clave, las tareas concurrentes deben esperar el resultado de la primera, asegurando que todas compartan exactamente el mismo recurso.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-swift"&gt;private actor EventsActor {
    private var events: [String: EventModel] = [:]
    private var builds: [String: Task&amp;lt;EventModel, Error&amp;gt;] = [:]
    
    func getOrCreate(
        name: String, 
        cityId: UUID, 
        build: @Sendable @escaping () async throws -&amp;gt; EventModel
    ) async throws -&amp;gt; EventModel {
        let key = &amp;quot;\(name)\(cityId.uuidString)&amp;quot;
        if let event = events[key] { 
            return event 
        }

        if let building = builds[key] { 
            return try await building.value 
        }
        
        let buildTask = Task { 
            try await build() 
        }
        builds[key] = buildTask
        
        let event = try await buildTask.value
        events[key] = event
        builds.removeValue(forKey: key)
        return event
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;Lecciones aprendidas&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Un actor por sí solo no previene condiciones de carrera lógicas del tipo “check-then-act”.&lt;/li&gt;
&lt;li&gt;En escenarios concurrentes, serializar la construcción del recurso por clave es fundamental para mantener la integridad de los datos.&lt;/li&gt;
&lt;li&gt;Es esencial probar bajo carga y escenarios concurrentes, no solo en modo secuencial.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Keep coding, keep running&lt;/strong&gt; 🏃‍♂️&lt;/p&gt;
&lt;hr /&gt;</content>
    <link rel="alternate" href="https://jcalderita.com/es/blog/my-first-data-race/"></link>
    <media:content medium="image" url="https://jcalderita.com/static/blog/MyFirstDataRace.webp"></media:content>
  </entry>
  <entry>
    <id>https://jcalderita.com/es/blog/timestamps-migrations/</id>
    <title>Timestamps en Migraciones</title>
    <updated>2025-09-15T00:00:00Z</updated>
    <summary>Cómo evitar repetir createdAt, updatedAt y deletedAt en las migraciones de base de datos usando extensiones.</summary>
    <content type="html">&lt;hr /&gt;
&lt;h2&gt;Timestamps&lt;/h2&gt;
&lt;p&gt;Si eres como yo, cuando diseñas un modelo de base de datos quieres que todas las tablas incluyan siempre tres campos clave:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;span class="high"&gt;createdAt&lt;/span&gt;: indica cuándo se creó el registro.&lt;/li&gt;
&lt;li&gt;&lt;span class="high"&gt;updatedAt&lt;/span&gt;: refleja la última vez que se actualizó.&lt;/li&gt;
&lt;li&gt;&lt;span class="high"&gt;deletedAt&lt;/span&gt;: marca cuándo se eliminó de forma lógica (sin borrar físicamente el registro).&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;Problema&lt;/h2&gt;
&lt;p&gt;Como se puede observar, en cada migración tenemos que escribir manualmente los tres campos de timestamp. Esto no solo es repetitivo, sino que también es propenso a errores si olvidamos algún campo o escribimos mal el tipo de dato.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-swift"&gt;try await db.schema(model)
    .id()
    .field(.name, .string, .required)
    .field(.createdAt, .datetime, .required)
    .field(.updatedAt, .datetime, .required)
    .field(.deletedAt, .datetime)
    .create()
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Solución&lt;/h2&gt;
&lt;p&gt;La solución es crear una extensión de &lt;span class="high"&gt;SchemaBuilder&lt;/span&gt; que agregue un método &lt;span class="high"&gt;timestamps()&lt;/span&gt;. Esta extensión encapsula la lógica repetitiva en un solo lugar, haciendo el código más limpio y mantenible.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-swift"&gt;extension SchemaBuilder {
    func timestamps() -&amp;gt; Self {
        self.field(.createdAt, .datetime, .required)
            .field(.updatedAt, .datetime, .required)
            .field(.deletedAt, .datetime)
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Resultado&lt;/h2&gt;
&lt;p&gt;Ahora nuestras migraciones son mucho más limpias y legibles. Llamando a &lt;span class="high"&gt;.timestamps()&lt;/span&gt; agregamos los tres campos necesarios. Esto reduce la posibilidad de errores y hace que el código sea más fácil de mantener.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-swift"&gt;try await db.schema(model)
    .id()
    .field(.name, .string, .required)
    .timestamps()
    .create()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Keep coding, keep running&lt;/strong&gt; 🏃‍♂️&lt;/p&gt;
&lt;hr /&gt;</content>
    <link rel="alternate" href="https://jcalderita.com/es/blog/timestamps-migrations/"></link>
    <media:content medium="image" url="https://jcalderita.com/static/blog/Timestamps.webp"></media:content>
  </entry>
  <entry>
    <id>https://jcalderita.com/es/blog/cloud-server/</id>
    <title>☁️ Servidor en la nube</title>
    <updated>2025-09-08T00:00:00Z</updated>
    <summary>Servidor en la nube, algo necesario cuando trabajas con backend. Descubre las mejores opciones gratuitas y por qué necesitas uno.</summary>
    <content type="html">&lt;hr /&gt;
&lt;h2&gt;☁️ Cloud Server&lt;/h2&gt;
&lt;p&gt;Cuando trabajas con backend, a no ser que estés trabajando en una red local y no vaya a salir nada de lo que hagas hacia afuera, vas a necesitar sí o sí un servidor en la nube y cuanto antes mejor 🚀.&lt;/p&gt;
&lt;p&gt;Trabajar en local está muy bien, pero tener un servidor en la nube es un paso hacia adelante, y es mejor empezar cuanto antes para tener un servidor de desarrollo en la nube.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;🔧 Arquitecturas de CPU&lt;/h2&gt;
&lt;p&gt;Conforme la tecnología avanza, cada vez hay más opciones tanto en proveedores como en tecnología de procesadores 💻:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;AMD&lt;/strong&gt; 🔴:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;✅ Excelente relación precio/rendimiento&lt;/li&gt;
&lt;li&gt;✅ Arquitectura x64 madura y estable&lt;/li&gt;
&lt;li&gt;✅ Amplia compatibilidad con software&lt;/li&gt;
&lt;li&gt;✅ Ideal para aplicaciones de alto rendimiento&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Intel&lt;/strong&gt; 🔵:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;✅ Líder histórico en el mercado de servidores&lt;/li&gt;
&lt;li&gt;✅ Excelente soporte empresarial&lt;/li&gt;
&lt;li&gt;✅ Optimizaciones específicas para aplicaciones legacy&lt;/li&gt;
&lt;li&gt;✅ Rendimiento consistente y predecible&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;ARM&lt;/strong&gt; 🍃:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;✅ El último en llegar pero con gran potencial&lt;/li&gt;
&lt;li&gt;✅ Eficiencia energética superior&lt;/li&gt;
&lt;li&gt;✅ Mejor precio por núcleo&lt;/li&gt;
&lt;li&gt;✅ Ideal para microservicios y contenedores&lt;/li&gt;
&lt;li&gt;✅ Menor consumo = menor costo operativo&lt;/li&gt;
&lt;li&gt;⚠️ Algunas limitaciones de compatibilidad con software legacy&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;💰 Free Tier - Oracle Cloud&lt;/h2&gt;
&lt;p&gt;Yo he optado por Oracle Cloud y el free tier que tiene, el cual te ofrece un servidor ARM, ya que para mí es la opción que mejor se adapta a mis necesidades 🎯.&lt;/p&gt;
&lt;p&gt;🖥️ VM.Standard.A1.Flex (Ampere ARM)&lt;br /&gt;&lt;br /&gt;
⏱️ Hasta &lt;strong&gt;3.000 OCPU-horas&lt;/strong&gt; al mes&lt;br /&gt;&lt;br /&gt;
🧠 Hasta &lt;strong&gt;18.000 GB-horas de memoria&lt;/strong&gt; al mes&lt;br /&gt;&lt;br /&gt;
🚀 1 servidor con &lt;strong&gt;4 OCPUs + 24 GB RAM&lt;/strong&gt; funcionando 24/7&lt;br /&gt;&lt;br /&gt;
🔄 O varias instancias más pequeñas (ejemplo: 4 × 1 OCPU + 6 GB RAM)&lt;br /&gt;&lt;br /&gt;
💾 Hasta &lt;strong&gt;200 GB combinados&lt;/strong&gt; entre volúmenes de arranque y bloques&lt;br /&gt;&lt;br /&gt;
♾️ &lt;strong&gt;Gratuito indefinidamente&lt;/strong&gt;, siempre dentro de los límites del plan&lt;br /&gt;&lt;br /&gt;
⚠️ Oracle puede &lt;strong&gt;reclamar instancias inactivas&lt;/strong&gt; (sin uso significativo en 7 días)&lt;br /&gt;&lt;br /&gt;
🌍 Puede haber &lt;strong&gt;restricciones regionales&lt;/strong&gt; por falta de capacidad&lt;br /&gt;&lt;br /&gt;
💸 &lt;strong&gt;100% gratis&lt;/strong&gt; dentro del plan Always Free&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Keep coding, keep running&lt;/strong&gt; 🏃‍♂️&lt;/p&gt;
&lt;hr /&gt;</content>
    <link href="https://jcalderita.com/es/blog/cloud-server/" rel="alternate"></link>
    <media:content url="https://jcalderita.com/static/blog/CloudServer.webp" medium="image"></media:content>
  </entry>
  <entry>
    <id>https://jcalderita.com/es/blog/claude-code/</id>
    <title>Claude Code</title>
    <updated>2025-09-01T00:00:00Z</updated>
    <summary>Descubre cómo Claude Code se ha convertido en mi asistente de desarrollo favorito y las reglas que aplico para maximizar su potencial.</summary>
    <content type="html">&lt;hr /&gt;
&lt;h2&gt;🤔 ¿Qué es Claude Code para mí?&lt;/h2&gt;
&lt;p&gt;Para alejarme de lo que habla todo el mundo 😂, voy a contaros sobre &lt;strong&gt;inteligencia artificial&lt;/strong&gt; desde mi experiencia personal.&lt;/p&gt;
&lt;p&gt;He integrado &lt;strong&gt;Claude Code&lt;/strong&gt; en mis proyectos, pero para mí es mucho más que una herramienta: es mi &lt;strong&gt;asistente digital&lt;/strong&gt; 👥, un miembro más de mi equipo de desarrollo, una extensión de mis capacidades o, como bien representa la imagen, &lt;strong&gt;mi sombra tecnológica&lt;/strong&gt; 🌑.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;No me sustituye ni creo que sustituya a ningún desarrollador en estos momentos. Claude Code es tan bueno como lo seas tú o como sepas utilizarlo 💡.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;📋 Mis Reglas de Oro&lt;/h2&gt;
&lt;p&gt;Tengo &lt;strong&gt;cuatro reglas fundamentales&lt;/strong&gt; que aplico religiosamente con Claude Code:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;🔍 &lt;strong&gt;Control Total de Cambios&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Yo reviso todo&lt;/strong&gt; antes de implementar cualquier cambio&lt;/li&gt;
&lt;li&gt;No tiene permisos para modificar archivos sin mi aprobación&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Flujo:&lt;/strong&gt; Me proporciona el plan → Reviso los cambios → Los entiendo → Los apruebo (o no)&lt;/li&gt;
&lt;li&gt;Esta regla es &lt;strong&gt;súper importante&lt;/strong&gt; para mí. Darle permisos ciegos no va con mi filosofía 🚫&lt;/li&gt;
&lt;/ul&gt;
&lt;ol start="2"&gt;
&lt;li&gt;📝 &lt;strong&gt;Claude.md como Biblia del Proyecto&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;Trabajo intensamente el archivo &lt;span class="high"&gt;CLAUDE.md&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;Defino qué es el proyecto y establezco &lt;strong&gt;guidelines claras&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Es mi manera de “entrenar” a Claude sobre mi estilo y preferencias&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Resultado:&lt;/strong&gt; Respuestas más alineadas con mi visión 🎯&lt;/li&gt;
&lt;/ul&gt;
&lt;ol start="3"&gt;
&lt;li&gt;⚙️ &lt;strong&gt;Configuración Inteligente&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;Configuro sus settings con &lt;strong&gt;rutas de documentación oficial&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Cuando hago preguntas sobre tecnologías específicas, utiliza fuentes oficiales&lt;/li&gt;
&lt;li&gt;Esto garantiza respuestas más precisas y actualizadas 📚&lt;/li&gt;
&lt;/ul&gt;
&lt;ol start="4"&gt;
&lt;li&gt;🚀 &lt;strong&gt;Trabajo Colaborativo, No Dependencia&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Nunca&lt;/strong&gt; parto de un IDE vacío&lt;/li&gt;
&lt;li&gt;Siempre trabajo sobre una base de código que ya he desarrollado&lt;/li&gt;
&lt;li&gt;Mi flujo: Empiezo a crear → Me atascos → Pregunto a Claude&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pregunta recurrente:&lt;/strong&gt; &lt;em&gt;”¿Hay una manera más limpia y eficiente de hacer este código?”&lt;/em&gt; 🤝&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;💡 Imaginación sin límite&lt;/h2&gt;
&lt;p&gt;Y aquí viene lo interesante: &lt;strong&gt;Claude Code no es solo para programar&lt;/strong&gt; 🎨. Puedes usarlo para lo que se te ocurra. Solo hazte esta pregunta:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;¿Cómo puedo hacer que esta idea funcione con Claude Code?&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Una de las formas más geniales en las que lo uso &lt;strong&gt;fuera de programación&lt;/strong&gt; es con mi consumo de contenido técnico:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Consumo muchísimo contenido de programación:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;🎓 Cursos online&lt;br /&gt;&lt;br /&gt;
💊 Píldoras de conocimiento&lt;br /&gt;&lt;br /&gt;
🎤 Conferencias y talks&lt;br /&gt;&lt;br /&gt;
🗣️ Debates técnicos&lt;br /&gt;&lt;br /&gt;
🎧 Podcasts especializados&lt;br /&gt;&lt;br /&gt;
🛠️ Talleres prácticos&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;El problema típico:&lt;/strong&gt; Estás programando y recuerdas haber visto algo útil en un vídeo, pero… ¿cuál era? ¿En qué minuto? 😅&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Mi solución con Claude:&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Le pregunto: &lt;em&gt;”¿Dónde se habla de esto?”&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;Me devuelve:
&lt;ul&gt;
&lt;li&gt;📹 El vídeo o vídeos específicos&lt;/li&gt;
&lt;li&gt;⏰ El timestamp exacto donde se menciona&lt;/li&gt;
&lt;li&gt;📄 Un pequeño resumen del contenido&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;Resultado:&lt;/strong&gt; ¡&lt;strong&gt;Maravilloso!&lt;/strong&gt; ✨ Acceso instantáneo a todo mi conocimiento consumido.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;🎯 Reflexión Final&lt;/h2&gt;
&lt;p&gt;Claude Code se ha convertido en mi &lt;strong&gt;sombra tecnológica&lt;/strong&gt;. No es magia, no hace el trabajo por ti, pero si sabes usarlo correctamente, puede &lt;strong&gt;potenciar enormemente&lt;/strong&gt; tu productividad y creatividad.&lt;/p&gt;
&lt;p&gt;La clave está en establecer límites claros, mantener el control y usarlo como lo que realmente es: &lt;strong&gt;un asistente excepcional&lt;/strong&gt; que amplifica tus capacidades, no que las reemplaza 🚀.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Keep coding, keep running&lt;/strong&gt; 🏃‍♂️&lt;/p&gt;
&lt;hr /&gt;</content>
    <link href="https://jcalderita.com/es/blog/claude-code/" rel="alternate"></link>
    <media:content medium="image" url="https://jcalderita.com/static/blog/ClaudeCode.webp"></media:content>
  </entry>
  <entry>
    <id>https://jcalderita.com/es/blog/love-vapor/</id>
    <title>Amo Vapor</title>
    <updated>2025-08-25T00:00:00Z</updated>
    <summary>Descubre por qué Vapor se convirtió en mi framework favorito de Swift en servidor en el Bootcamp, desde gRPC hasta PassKeys y tareas async.</summary>
    <content type="html">&lt;hr /&gt;
&lt;h2&gt;❤️ Swift del lado servidor&lt;/h2&gt;
&lt;p&gt;Cuando cursé el &lt;em&gt;Swift Full Stack Bootcamp&lt;/em&gt; en &lt;a href="https://acoding.academy"&gt;Apple Coding Academy&lt;/a&gt;, uno de los módulos que se nos impartió fue &lt;em&gt;Swift del lado servidor&lt;/em&gt; a través del framework &lt;a href="https://vapor.codes"&gt;Vapor&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Nada más comenzar con la parte teórica, mi corazón empezó a palpitar con fuerza y simplemente, a mitad de aquella primera clase, ya estaba completamente enamorado.&lt;/p&gt;
&lt;p&gt;Así que, si me preguntan &lt;strong&gt;“¿Cuál fue tu módulo favorito del Bootcamp?”&lt;/strong&gt;, sin lugar a dudas diría que este.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;👍🏻 ¿Por qué me gusta?&lt;/h2&gt;
&lt;p&gt;Siempre he pensado que la parte de &lt;strong&gt;backend&lt;/strong&gt; es el &lt;strong&gt;cerebro&lt;/strong&gt; de cualquier aplicación o web.&lt;br /&gt;
Es ahí donde está &lt;strong&gt;toda la chicha&lt;/strong&gt;: donde se procesa la información (normalmente desde una base de datos) y se prepara para que el usuario final la reciba de la forma más &lt;strong&gt;sencilla y transparente posible&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Que quede claro: &lt;strong&gt;no menosprecio el frontend&lt;/strong&gt;.&lt;br /&gt;
De hecho, me parece admirable la gente que crea diseños increíbles: auténticos héroes para mí, porque eso es algo que personalmente me resulta imposible… aunque con &lt;strong&gt;SwiftUI&lt;/strong&gt; me entiendo bastante bien.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;🧸 Jugando con Vapor&lt;/h2&gt;
&lt;p&gt;En próximas entradas del blog iré profundizando mucho más, pero ya puedo contar que me he dedicado a &lt;strong&gt;jugar mucho con Vapor&lt;/strong&gt;:&lt;/p&gt;
&lt;p&gt;📦 Paquete individual para configurar distintos servicios&lt;br /&gt;&lt;br /&gt;
🌐 Servidor &lt;strong&gt;gRPC&lt;/strong&gt;&lt;br /&gt;&lt;br /&gt;
🗂 Modelo de datos con &lt;strong&gt;vistas&lt;/strong&gt; (incluidas &lt;em&gt;materialized views&lt;/em&gt;), índices, permisos por aplicación y esquemas&lt;br /&gt;&lt;br /&gt;
🧮 Enumerados con migraciones automáticas&lt;br /&gt;&lt;br /&gt;
🔑 &lt;strong&gt;PassKey&lt;/strong&gt;&lt;br /&gt;&lt;br /&gt;
📊 &lt;em&gt;Bulk inserts&lt;/em&gt; para poblar grandes volúmenes de datos&lt;br /&gt;&lt;br /&gt;
⏱ Tareas programadas con asincronía&lt;br /&gt;&lt;br /&gt;
… y mucho más 🚀&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;A lo largo de distintas entradas del blog contaré un poco de todo.&lt;br /&gt;
Eso sí: aunque Vapor y Swift del lado servidor me guste mucho, &lt;strong&gt;no todo será sobre backend&lt;/strong&gt;… habrá espacio para muchos otros temas que me entusiasman.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Keep coding, keep running&lt;/strong&gt; 🏃‍♂️&lt;/p&gt;
&lt;hr /&gt;</content>
    <link href="https://jcalderita.com/es/blog/love-vapor/" rel="alternate"></link>
    <media:content medium="image" url="https://jcalderita.com/static/blog/LoveVapor.webp"></media:content>
  </entry>
  <entry>
    <id>https://jcalderita.com/es/blog/la-liga-thanks/</id>
    <title>¡Gracias, La Liga!</title>
    <updated>2025-08-18T00:00:00Z</updated>
    <summary>Cómo La Liga bloquea mi web durante los partidos por medidas antipiratería que afectan a sitios legítimos en Cloudflare.</summary>
    <content type="html">&lt;hr /&gt;
&lt;h2&gt;Mi web bloqueada en partidos de La Liga&lt;/h2&gt;
&lt;p&gt;Si estás intentando visitar mi web durante un partido de La Liga y no carga, no te lo estás imaginando: en realidad, está siendo bloqueada.&lt;/p&gt;
&lt;p&gt;En España, &lt;strong&gt;La Liga aplica medidas antipiratería muy agresivas&lt;/strong&gt;, y una de ellas consiste en &lt;strong&gt;bloquear el acceso a sitios web alojados en la red de Cloudflare&lt;/strong&gt; durante los partidos en directo. Desafortunadamente, esto significa que si tu web está alojada en Cloudflare —como la mía— puedes verte afectado aunque no estés emitiendo ni distribuyendo ningún contenido ilegal.&lt;/p&gt;
&lt;p&gt;Cloudflare protege y acelera millones de sitios web en todo el mundo, pero debido a la política de La Liga, &lt;strong&gt;rangos enteros de IP o nodos CDN pueden ser incluidos en listas negras&lt;/strong&gt;, lo cual afecta a &lt;strong&gt;innumerables webs legítimas&lt;/strong&gt;.&lt;/p&gt;
&lt;h2&gt;¿Por qué sucede esto?&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;La Liga utiliza sistemas automatizados y bloqueos a nivel DNS para restringir el acceso a lo que considera fuentes potenciales de piratería.&lt;/li&gt;
&lt;li&gt;Cloudflare es utilizado por todo tipo de sitios, incluidos aquellos que sí albergan retransmisiones ilegales —por lo que sus IPs se marcan.&lt;/li&gt;
&lt;li&gt;Como resultado, muchas &lt;strong&gt;webs inocentes se bloquean&lt;/strong&gt; durante los partidos, simplemente por compartir infraestructura.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;¿Qué puedes hacer?&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Si estás en España y no puedes acceder a mi portfolio durante un partido, vuelve a intentarlo más tarde.&lt;/li&gt;
&lt;li&gt;Si te pica la curiosidad, existen herramientas online para comprobar si un dominio está siendo bloqueado.&lt;/li&gt;
&lt;li&gt;O puedes usar una VPN para evitar temporalmente la restricción (solo para visitar mi portfolio, claro 😅).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Es un ejemplo frustrante de cómo una aplicación excesiva de medidas digitales puede dañar la web abierta —incluso portfolios pequeños como el mío.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;”¡Gracias, La Liga!”&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Actualización:&lt;/strong&gt; Finalmente encontré una solución sencilla para este problema. Si quieres saber cómo lo resolví en 5 minutos, lee &lt;a href="/es/blog/only-dns/"&gt;Only DNS&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Keep coding, keep running&lt;/strong&gt; 🏃‍♂️&lt;/p&gt;
&lt;hr /&gt;</content>
    <link rel="alternate" href="https://jcalderita.com/es/blog/la-liga-thanks/"></link>
    <media:content medium="image" url="https://jcalderita.com/static/blog/LaLiga.webp"></media:content>
  </entry>
  <entry>
    <id>https://jcalderita.com/es/blog/my-first-article/</id>
    <title>Mi primer artículo</title>
    <updated>2025-08-12T00:00:00Z</updated>
    <summary>Actualización de mi sitio web personal construido con Astro y Tailwind, donde compartiré artículos y experiencias como desarrollador Swift en el ecosistema Apple.</summary>
    <content type="html">&lt;hr /&gt;
&lt;h2&gt;🚀 ¡Gran actualización en mi sitio web!&lt;/h2&gt;
&lt;p&gt;Hoy lanzo una &lt;strong&gt;importante actualización&lt;/strong&gt; de mi sitio web personal, que ahora está construido con &lt;a href="https://astro.build/"&gt;Astro&lt;/a&gt; y &lt;a href="https://tailwindcss.com/"&gt;Tailwind&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;He añadido la posibilidad de &lt;strong&gt;escribir artículos&lt;/strong&gt; — ¡y este es el primero!&lt;br /&gt;
A partir de ahora, compartiré mis experiencias, descubrimientos y desafíos diarios como &lt;strong&gt;desarrollador Swift&lt;/strong&gt;.&lt;br /&gt;
También puede que comparta algo de mi vida personal.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;⚡️ &lt;strong&gt;Sin calendario fijo:&lt;/strong&gt;&lt;br /&gt;
Publicaré siempre que tenga algo relevante que compartir — ya sean problemas, soluciones, curiosidades o nuevas tecnologías que esté explorando.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Con esta nueva versión, me puedo centrar únicamente en escribir artículos en &lt;strong&gt;Markdown&lt;/strong&gt;; todo lo demás está completamente automatizado.&lt;/p&gt;
&lt;p&gt;Si te interesa &lt;strong&gt;Swift, iOS, visionOS, Vapor, desarrollo full-stack con Swift y Web&lt;/strong&gt;, o simplemente quieres acompañarme en este camino — ¡bienvenido!&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Keep coding, keep running&lt;/strong&gt; 🏃‍♂️&lt;/p&gt;
&lt;hr /&gt;</content>
    <link href="https://jcalderita.com/es/blog/my-first-article/" rel="alternate"></link>
    <media:content url="https://jcalderita.com/static/blog/MyFirstArticle.webp" medium="image"></media:content>
  </entry>
</feed>
