Construyendo un reproductor de música que en realidad no puedo escuchar
Esta app se renombró más adelante a Deep Cut Atlas. Acá abajo se la llama “Discoverer” todo el tiempo, porque así se llamaba el día en que pasó esto.
La pestaña de Historial en mi app de música muestra las canciones reproducidas recientemente, y quería que se pudiera tocar una para reproducirla, un pequeño refrescador de memoria, no un reproductor completo. Lo justo para pensar “ah cierto, esa canción.” Funcionalidad simple. Había un solo problema: no puedo correrla.
La reproducción de MusicKit no funciona en el Simulador de iOS. Para nada. Sin audio, sin error, solo silencio. La única forma de escuchar un solo segundo de lo que estoy construyendo es ponerlo en un iPhone físico con una suscripción a Apple Music, y todavía no tengo configurada la cuenta de desarrollador paga (eso es una tarea para más adelante). Así que estoy construyendo un reproductor de música que no tengo forma de escuchar.
Eso suena a una razón para no construirlo todavía. Pero “no puedo correr el último 5%” no es una razón para saltarse el otro 95%, y la mayor parte de esta funcionalidad no es el audio, es la lista, la paginación, el manejo de toques, el indicador de reproducción actual, el flujo de agregar a lista de reproducción. Todo eso sí puedo construirlo y verificarlo. El truco está en trazar la línea en el lugar correcto.
Poner la parte no verificable detrás de una puertita
La reproducción real son cuatro líneas:
let song = try await resolveSong(id: track.id)
player.queue = [song]
try await player.play()
// ...and player.pause()
Así que escondí esas cuatro líneas detrás de un protocolo con exactamente dos métodos:
@MainActor
protocol MusicPlaying: AnyObject {
func play(_ track: LibraryTrack) async throws
func pause()
}
La implementación real envuelve a ApplicationMusicPlayer. Una implementación simulada simplemente registra qué se le pidió reproducir. En el simulador la app inyecta la simulada, en un dispositivo, la real. Este es el mismo patrón que uso para toda la capa de biblioteca, así que no era nuevo, solo significó que la superficie de “esto no se puede probar” se redujo de una funcionalidad entera a dos cuerpos de método.
El indicador es una mentira que cuento a propósito
Acá está la parte con la que tuve que tener cuidado. Al tocar una fila, aparece una pequeña forma de onda para mostrar que se está reproduciendo. ¿De dónde sale eso?
No del reproductor. El reproductor es un comando de disparar y olvidar, le digo que reproduzca, y lo hace (o, en el simulador, no lo hace). El indicador está manejado por el propio estado del view model: un nowPlayingTrackID y una bandera isPlaying que cambio al tocar. Estado optimista.
func togglePlayback(_ track: LibraryTrack) async {
if nowPlayingTrackID == track.id, isPlaying {
player.pause(); isPlaying = false; return
}
try? await player.play(track)
nowPlayingTrackID = track.id
isPlaying = true
}
Esto significa que el indicador funciona perfecto en el simulador (se toca una fila, aparece la forma de onda, se toca de nuevo, se pausa) mientras no suena ningún audio. Y esa es la trampa que tuve que nombrar en voz alta cuando revisé la funcionalidad: un indicador que alterna es evidencia de que la interfaz está conectada, no evidencia de que la música suena. Son dos afirmaciones completamente distintas, y la demo las hace parecer una sola.
Lo anoté explícitamente para no engañarme más adelante: el simulador confirma los gestos, la lista, la paginación, los toasts. Si de verdad se escucha a Khruangbin queda sin verificar hasta que la funcionalidad corra en un teléfono real. No “probablemente esté bien”, sin verificar.
Lo que el estado optimista me va a costar más adelante
Hay un bug real escondido en ese enfoque optimista, y lo estoy dejando adentro a propósito por ahora. Como el indicador está manejado por mi bandera y no por el reproductor, no sabe cuándo termina realmente una canción. En un dispositivo, una canción va a terminar, el audio se va a detener, y mi pequeña forma de onda va a seguir animándose feliz porque nada le dijo lo contrario.
Arreglar eso significa suscribirse al playbackStatus real del reproductor y reconciliar mi estado con él. Eso es un trabajo genuinamente exclusivo de dispositivo, ni siquiera puedo escribirlo con sentido sin un dispositivo para observar, así que va en la misma categoría que “verificar el audio.” Dejé una nota en el issue para que mi yo futuro no lo redescubra por las malas.
La lección
Cuando parte de una funcionalidad es genuinamente imposible de probar en el entorno de trabajo, la jugada no es saltearla ni fingir que el check verde la cubre. Es hacer esa parte lo más chica posible, construir y verificar honestamente todo lo que la rodea, y ser preciso sobre qué prueba en realidad ese “funciona.” Un mini-reproductor que no puedo escuchar sigue siendo mayormente construible, siempre que no confunda la parte que probé con la que no.
Lecturas relacionadas
La lista de reproducción que ya tenía el nombre correcto
Un artefacto del cambio de nombre, una API sin campo de autor, y una prueba en dispositivo que mostró que el error ya se había arreglado solo, cerrado así.
Álbumes destacados, datos reales de MusicKit y una prueba que mintió al pasar
No existe un flag isEssential: Essentials es solo una playlist. featuredAlbums es la señal real, y un fixture cambiado expuso un test sin re-ejecutar.
La pestaña Discover que quedó atascada excavando para siempre
Una búsqueda de artista fallida nunca contaba como revisada, así que un artista problemático se reelegía para siempre - hasta que una prueba de varias rondas detectó la segunda protección.
También te podría ser útil
Proton Mail
Correo electrónico cifrado de extremo a extremo, con arquitectura de acceso cero.
Como socio de Proton, obtengo ingresos por las compras que califican de los servicios de privacidad y seguridad de Proton (Pass, Mail, VPN, Drive).
Más informaciónNordPass
Gestor de contraseñas del equipo detrás de NordVPN, con un plan gratuito.
Como afiliado de NordPass, obtengo ingresos por las compras que califican.
Más informacióneSIM Airalo
eSIM de datos local para viajes - sin necesidad de cambiar una SIM física.
Este es mi enlace de referido de Airalo. Obtienes un descuento en tu primer eSIM y yo obtengo crédito de Airalo para el mío.
Más información