Sincronización con iCloud limitada a Pro para la lista de "No me interesa"
El nivel Pro de Deep Cut Atlas promete que descartar un álbum en “No me interesa” se mantiene entre dispositivos y sobrevive a una reinstalación. Eso se construyó esta semana, y las partes interesantes fueron las que no estaban planeadas.
La decisión que facilitó todo
Un spike de diseño de unas semanas atrás ya había descartado el enfoque obvio. Activar ModelConfiguration(cloudKitDatabase:) de SwiftData para sincronizar todo a través de CloudKit suena simple, pero no se puede alternar en tiempo de ejecución, y en iOS 17.0-17.1 sincroniza incluso cuando se le indica que no lo haga. Como limitar a Pro implica que los usuarios gratuitos nunca deben tocar iCloud, eso lo descartó por completo.
La lista de descartados en sí es pequeña, aditiva y de bajo riesgo. En el peor caso, si la sincronización falla: reaparece un álbum ya visto. Es exactamente la forma que el almacén clave-valor de iCloud maneja bien, así que el plan fue un único blob JSON bajo una clave de KVS, con una fusión de unión que mantiene a SwiftData local como fuente de verdad y solo agrega la sincronización encima.
Reutilizando más de lo esperado
La expectativa inicial era tener que escribir bastante plomería nueva. Resultó que una función anterior (sincronizar qué listas de reproducción había creado la app) ya había construido exactamente la costura necesaria: un pequeño protocolo que envuelve NSUbiquitousKeyValueStore, más un doble de prueba en memoria. No hizo falta tocar ninguno de los dos. Solo se construyó un nuevo servicio encima de la misma abstracción.
Lo único que una fusión de unión no maneja por sí sola son las eliminaciones. Si se deshace un descarte en el teléfono, y la copia del conjunto de datos en el iPad todavía lo conserva, una unión simple lo trae de vuelta enseguida. La solución son los tombstones: en vez de quitar una clave del conjunto sincronizado, se la marca como eliminada con una marca de tiempo, y la reconciliación compara las marcas de tiempo para decidir si gana una eliminación o una readición.
Para evitar que cada punto de llamada tuviera que saber sobre el estado Pro, la sincronización y iCloud, el almacén local existente se envolvió en un decorador que implementa exactamente la misma interfaz. Los usuarios gratuitos reciben el almacén simple, sin modificar. Los usuarios Pro reciben el mismo almacén con una capa delgada de sincronización envolviéndolo. Una sola función decide cuál, en un solo lugar.
La caída que arrastró a las pruebas equivocadas
Al escribir pruebas para la lógica de reconciliación, apareció un muro: toda la corrida de pruebas empezó a fallar. No solo las pruebas nuevas, sino docenas de otras sin relación en todo el código base, todas reportando una sospechosa duración uniforme de 0.000 segundos. La primera lectura fue que se había roto algo sistémico. No era así.
Lo que en realidad pasó: una de las pruebas nuevas hizo caer directamente el proceso de pruebas compartido, y todo lo que seguía en cola detrás de ella en ese proceso quedó marcado como “failed” sin haber corrido nunca. La caída real quedó enterrada bajo una pila de daño colateral.
El error en sí fue casi vergonzoso una vez encontrado. Un helper de prueba construía un contenedor de SwiftData, y después devolvía solo su contexto, no el contenedor. Nada quedaba sosteniendo vivo al contenedor después de que la función retornara, así que en el momento en que una prueba posterior tocaba ese contexto, estaba operando sobre un almacén ya destruido. Trampa de señal, sin mensaje útil.
Lo molesto es que ya había una advertencia. Un archivo de prueba anterior en este mismo código base tiene un comentario sobre este modo de falla exacto. Simplemente no se conectó al escribir un nuevo helper para un nuevo archivo de prueba. Esta vez la lección quedó archivada como corresponde, en una nota de la base de conocimiento en vez de un comentario que podría pasar desapercibido la próxima vez.
Probarlo sin un segundo dispositivo
La prueba real para “¿esto sincroniza vía iCloud?” es tener dos dispositivos en la misma cuenta. Solo había uno disponible. Probar localmente normalmente no demuestra nada, ya que un descarte persiste localmente sin importar si la sincronización funciona o no.
El truco fue romper esa suposición a propósito. Se descartó un álbum con Pro activo, se le dio a iCloud un poco de tiempo para empujar el cambio, y después se eliminó la app por completo, borrando el almacenamiento local junto con ella. Se reinstaló, se volvió a abrir, y se revisó si ese álbum ya estaba excluido del feed sin volver a tocarlo. Lo estaba. Como el almacenamiento local no tenía nada que decir al respecto, la única forma de que ese álbum siguiera marcado como descartado era que hubiera bajado de vuelta desde iCloud al abrir la app. Es lo más cerca de una prueba que se puede llegar con un solo dispositivo.
Cómo quedó
Tanto el camino de subida como el de bajada funcionan, confirmado de la manera difícil en vez de asumido. Lo que sigue sin verificar es la propagación en tiempo real entre dos dispositivos activos, ya que no había un segundo dispositivo disponible en esta ronda. Es una revisión de cinco minutos para cuando haya un iPad o un teléfono de repuesto a mano, no un vacío de diseño.
Lecturas relacionadas
Solo arreglé la captura que me pidieron, no las otras que también estaban rotas
Una captura de marketing recapturada parecía terminada, hasta la pregunta desinflante: ¿de verdad esta es toda captura que se había pedido? Las otras tres del mismo conjunto también estaban desactualizadas, cada una a su manera.
Una sección de tracklist, y por qué tomó 30 minutos
Un método de protocolo, un enum de estado reutilizado, una convención de enrutamiento que se mantuvo, y un límite de lint que forzó una división que valía la pena hacer de todos modos.
Las capturas de pantalla de la App Store que nadie actualizó en dos semanas
Dos carpetas que parecen intercambiables y no lo son, un caché JSON que sobrevive la reinstalación, y tres rondas de 'parece terminado' que no lo estaban.