Saltar al contenido
Development

Construyendo un reproductor de música que en realidad no puedo escuchar

Por Victor Da Luz
iosswiftmusickitdev-logdeep-cut-atlas

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