# WealthSim: calcular bien no bastaba para usar la app

> WealthSim mostraba un saldo en el preset y otro al abrirlo. Anuncios al explorar, resultados fuera de pantalla y el paso de calcular a revisar un plan.

Una tarjeta de WealthSim mostraba un saldo. La tocabas y la calculadora te daba otro, más bajo. Con suerte, además, aparecía un anuncio a pantalla completa para darte tiempo a pensar cuál de los dos era el bueno.

[WealthSim](/apps/wealthsim/) es mi calculadora de interés compuesto, con versión web y app nativa de iOS. Metes un capital inicial, una aportación mensual y unas hipótesis de rentabilidad; te devuelve una proyección. En la revisión del 5 de octubre encontré que la tarjeta del preset calculaba sin comisiones, pero al aplicarlo conservaba las de la calculadora. El mismo motor, dos entradas distintas que la pantalla presentaba como si fueran la misma.

La revisión también señalaba que el resultado desaparecía al bajar a los controles. Yo podía explicar cada número mirando el código. La persona con el móvil tenía que fiarse de una cifra que cambiaba al tocarla y hacer scroll para ver qué había hecho su último ajuste.

## El test que faltaba estaba entre la tarjeta y el resultado

La vista previa del preset llamaba a `InvestmentProjector` con comisión e inflación a cero. `CalculatorSession.applyPreset` conservaba las que ya tenía el usuario: por defecto, una comisión anual del 0,25 % y una inflación del 2 %.

La comisión explicaba la diferencia en el saldo nominal. La inflación también tenía que viajar con el mismo contexto, aunque afecte a la lectura en dinero de hoy. En `0f507ae` pasé ambos valores a `PresetStrip.preview`, de modo que la tarjeta usara las mismas hipótesis que quedarían activas al tocarla.

**La propiedad que quería proteger era la igualdad entre lo que la tarjeta enseña y lo que ocurre al aplicarla.** Tener tests de la fórmula no comprueba esa relación.

En `FirstSessionUXTests.swift` quedó un test parametrizado con los presets del catálogo. Este es el fragmento que hace la comparación, usando los valores por defecto de la sesión:

```swift
@Test(arguments: PresetCatalog.presets)
func presetPreviewMatchesAppliedBalance(preset: Preset) throws {
    let session = CalculatorSession()
    let preview = PresetStrip.preview(
        for: preset,
        managementFee: try #require(Decimal(string: "0.25")),
        inflation: 2,
        currencyCode: session.currencyCode
    )
    session.applyPreset(preset)
    #expect(preview.finalBalance == session.presentation?.finalBalance)
}
```

Ese test tiene un límite: cubre la configuración inicial. Si mañana quiero proteger todas las combinaciones de comisiones editables, tendré que ampliar las entradas. Pero ya compara dos caminos de la app que antes podían discrepar sin que el test de la fórmula se enterara.

La misma comprobación sirve para una tarifa que cambia al abrir el checkout o una estimación que cambia al guardarla. Antes de añadir otro caso al motor, compara la vista previa con el resultado de la acción que ofrece.

## Aplicar un preset contaba como ocasión para enseñar publicidad

El coordinador de anuncios tenía `actionsPerAd = 1`. Aplicar un preset emitía `onSignificantAction`, igual que guardar o reabrir un escenario. Si había consentimiento, un anuncio preparado y se cumplían las demás condiciones, el primer gesto de exploración podía acabar en un interstitial.

Para el contador eran acciones equivalentes. Para quien está probando la app, tocar un preset apenas es averiguar qué hace.

En el mismo commit quité esa llamada de `applyPreset`. Añadí protección durante la primera sesión y las primeras 24 horas desde el primer arranque, hasta que terminen ambas. Después, el contador exige tres tareas completadas y permite como máximo dos interstitials por sesión. El contador de tareas se conserva entre arranques.

También quité el banner que estaba encima del resultado. Sigue habiendo publicidad en la versión gratuita. Elegí reducir las interrupciones mientras alguien está probando valores, y acepté que eso elimina oportunidades de mostrar anuncios. No tengo una comparativa de ingresos que permita ponerle un precio a la decisión.

## El saldo tenía que seguir visible mientras movía el slider

El resultado estaba encima de los controles dentro de un scroll largo. Podía actualizarlo en cada cambio y, aun así, obligar al usuario a subir para verlo. El cálculo era inmediato; comprobarlo no.

El rediseño de `80238e9` añadió `PinnedResultBar`: cuando la tarjeta grande sale por arriba, una barra compacta conserva el año y el saldo proyectado mientras editas. El gráfico también pasó a mostrar barras con aportaciones e intereses separados.

Esto no necesitó una fórmula nueva. Necesitó mantener la respuesta cerca del gesto que la cambia. La captura bonita de la primera pantalla no enseñaba ese problema: había que bajar hasta un control y usarlo.

La revisión de primera sesión me dejó esta tabla. El orden va de una inconsistencia concreta a una decisión de producto bastante más cara:

| Lo que veía la persona | Cambio que hice | Qué puedo comprobar |
| --- | --- | --- |
| El preset y el resultado daban saldos distintos | Pasar las comisiones e inflación activas a la vista previa | Comparar preview y resultado tras aplicar el preset |
| Explorar podía disparar un anuncio | Sacar los presets del contador y proteger el primer uso | Comprobar que ese gesto no habilita un interstitial |
| Editar dejaba el resultado fuera de pantalla | Añadir la barra compacta del saldo | Mover un control con la tarjeta grande fuera de la vista |
| Guardar una proyección no daba una tarea para el mes siguiente | Añadir «Mi plan» y el registro de saldo | Verificar el flujo; saber si alguien vuelve requiere observar uso real |

## Mi plan añade un saldo real a una curva hipotética

Una calculadora puede cumplir su función en una sola visita. No considero que alguien que hace una cuenta y se va haya usado mal WealthSim. Para justificar otra visita necesitaba ofrecerle otra tarea.

Con «Mi plan» puedes convertir una proyección en un objetivo con importe y fecha. Después registras tu saldo real cada mes y lo comparas con la trayectoria del plan. La versión gratuita permite un plan activo. El recordatorio mensual es opcional y pide permiso al activarlo.

El cambio de modelo aparece en `GoalPlan` y `PlanCheckIn`. Una proyección guardada conserva hipótesis. Un registro mensual añade una observación de lo que ha pasado. `PlanStore.logCheckIn` actualiza la entrada del mes si ya existe: corregir el saldo de octubre no debe dibujar un segundo octubre.

Hay una limitación que conviene explicar junto al gráfico. `PlanRoute` usa los resultados anuales de `InvestmentProjector` e interpola entre ellos para colocar los meses. La trayectoria es una referencia calculada con las hipótesis del plan; no representa cómo se moverá el mercado. Y registrar el saldo requiere trabajo manual. Si esa comparación no te sirve, un recordatorio solo añade una molestia.

Por eso la pregunta que me interesa ahora es si alguien registra un saldo y vuelve otro mes a compararlo. Abrir una notificación, por sí solo, no me diría que el plan le resulta útil.

## Construí más de lo que todavía puedo validar

La revisión del 5 de octubre dejó constancia de que no había datos de retención disponibles en App Store Connect y de que la app no tenía analítica de producto para responder a estas preguntas. A 6 de octubre, no puedo presentar este rediseño como una mejora medida de retención.

Además, `80238e9` metió «Mi plan», el cambio visual y funciones Pro en un mismo commit. Aunque después vea más uso, ese paquete no permite atribuírselo a una sola decisión. Fue bastante más trabajo que arreglar el preset y retirar el banner superior.

Si repitiera el proceso, empezaría por esas correcciones pequeñas, comprobaría el recorrido con alguien que no conozca la app y dejaría «Mi plan» como una hipótesis separada. Tiene un coste de desarrollo mayor y depende de que la persona quiera llevar ese seguimiento. El anuncio temprano y la discrepancia del preset se podían corregir sin apostar por un hábito nuevo.

Coge una sola tarjeta de tu producto que anticipe un resultado. Anota el valor, ejecuta la acción y compáralos manteniendo la configuración del usuario. Si no coinciden, ya tienes un arreglo concreto que hacer antes de abrir el roadmap.

## Fuentes y alcance

Esta historia sale de la revisión de UI del 5 de octubre y del historial de mi repositorio privado de WealthSim: `0f507ae` para la primera sesión y `80238e9` para el rediseño y «Mi plan». El test reproducido está en `FirstSessionUXTests.swift`; el registro mensual y la trayectoria, en `PlanStore.swift` y `PlanMath.swift`. Son cambios de código, no resultados de un experimento de retención ni una confirmación de disponibilidad de cada función en la App Store.

**Lectura relacionada:** al guardar estos datos también aparecen decisiones de persistencia. En mi otra app conté [cómo SwiftData y CloudKit condicionan el modelo antes de sincronizar nada](/blog/datos-locales-cloudkit-coste/).

---

**P.D.** ¿Has encontrado una vista previa que prometía algo distinto a lo que hacía el botón? Cuéntame el caso en [Twitter/X](https://x.com/garbarok).
