swiftiosmusicnavidromeproject

Writing my own iOS music player without a Mac

I wanted a music player that streams from my own server. I don't own a Mac. So I wrote a native iOS app on my phone, compiled it on a jailbroken iPhone, and connected everything through a VPN.

/5 min read

Every music streaming service has the same pitch. Pay us monthly and you can listen to anything, anywhere. Spotify, Apple Music, Tidal, whatever. They all work fine until they don't. Songs get pulled. Playlists get rearranged by algorithms. The version of the song you liked gets replaced by a remaster you didn't ask for. And if you stop paying, everything disappears.

I've been maintaining my own music library for years. FLAC files, proper tags, organized by artist and album. The files sit on my desktop at home running Navidrome, a self-hosted music server that implements the Subsonic API. It works. But the official Subsonic clients for iOS are either abandoned, ugly, or both. The ones that aren't abandoned cost money for an app that does less than I want.

So I built my own. I called it Sonare.

The development environment problem

Writing an iOS app normally requires a Mac and Xcode. I don't have a Mac. I have a Windows desktop and two iPhones. That's it.

What I do have is an older iPhone running Dopamine, a semi-untethered jailbreak, with FridaCodeManager installed on it. FridaCodeManager is a full on-device Swift IDE and compiler. You write SwiftUI on the phone, hit build, and it compiles and installs the app right there. No Mac in the loop at all.

It's technically deprecated now. The project has moved on and the developer isn't actively maintaining it anymore. But it still works, and it's the only reason this app exists. I write and compile on the jailbroken device, then build for and install on my daily driver running iOS 18. The whole workflow is phone-to-phone.

Writing SwiftUI on a phone screen is exactly as painful as it sounds. No autocomplete worth mentioning, no Xcode previews, no Interface Builder. Just you and a text editor smaller than a playing card. But it works. You learn to keep things simple because you physically cannot manage complexity on a 6-inch screen.

Getting the music to my phone

The files are on my desktop. My phone is not always on my home network. I need to reach Navidrome from anywhere without exposing it to the public internet.

Tailscale handles this. It creates a mesh VPN between my devices using WireGuard under the hood. My desktop gets a stable Tailscale IP, my phone connects to the Tailscale network, and suddenly Navidrome is reachable from anywhere as if I were on my home LAN. No port forwarding, no dynamic DNS, no reverse proxy. Just install Tailscale on both devices and they can talk to each other.

The app points at my desktop's Tailscale IP on Navidrome's port. When I open Sonare on the train, on campus, wherever, it reaches back to my desktop through Tailscale and streams whatever I want. My library, my files, my server, no middleman.

What the app does

Sonare is a fairly straightforward music player built with SwiftUI. It talks to Navidrome using the Subsonic API, which gives you endpoints for browsing artists, albums, and tracks, streaming audio, and fetching cover art.

The app has three tabs: Artists, Albums, and Now Playing. You browse by artist, drill into their albums, see the track listing, and tap to play. Standard music player hierarchy. Each track row shows the track number, title, artist, and duration. The currently playing track gets highlighted.

The Now Playing screen shows the album art, track info, a progress slider, and playback controls. Shuffle, previous, play/pause, next, repeat. There's a queue system where you can long-press any track to add it to the queue, either as the next song or at the end. The queue view lets you reorder and remove tracks.

When you're not on the Now Playing tab, a floating mini player sits at the bottom of the screen showing what's currently playing with a play/pause button. Tap it and the full Now Playing view slides up.

It integrates with iOS's lock screen and Control Center through MPNowPlayingInfoCenter and MPRemoteCommandCenter. Album art, track info, playback controls, scrubbing, all of it works from the lock screen the same way Apple Music or Spotify would. It handles audio interruptions properly too. Phone call comes in, music pauses. Headphones disconnect, music pauses. The session resumes correctly afterward.

Playback state persists across app launches. If I kill the app and reopen it, it remembers what was playing, where in the track I was, the full queue, shuffle state, repeat mode, everything. It reconstructs the entire session from the saved state.

Why not just use an existing client

There are Subsonic-compatible iOS apps out there. play:Sub, Amperfy, iSub. Some of them are good. But none of them are mine.

When something doesn't work the way I want, I change it. When I want a feature, I add it. When the queue behavior annoys me, I rewrite the queue logic. There's no waiting for a developer to merge a pull request or hoping the next update addresses your issue. The app does exactly what I want because I'm the only user and the only developer.

There's also something satisfying about the full stack being mine. My files, my server, my VPN, my app. No account to sign into, no subscription to maintain, no terms of service to agree to. Just music.

The stack

The whole setup, from storage to playback:

  • Music files: FLAC and MP3 on my Windows desktop
  • Server: Navidrome, self-hosted, speaks the Subsonic API
  • Network: Tailscale mesh VPN for access from anywhere
  • Development: FridaCodeManager on a jailbroken iOS device
  • App: SwiftUI, targeting iOS 18, built entirely on-device

No Mac. No Xcode. No cloud. Just a phone, a desktop, and a VPN connecting them.