Building Echo

June 18, 2026 5 min read
swiftmacos

Table Of Contents

  1. Day 1: June 16
  2. Day 2: June 17
  3. Day 3: June 18

I wanted a music player I actually want to use. And I wanted to learn Swift. So I started building one.

The project is called Echo. It’s a native macOS app, built with SwiftUI, that plays MP3s from your ~/Music folder. The scope is intentionally small: small enough to ship something real, but complex enough to touch the interesting parts of the platform (audio APIs, system integration, file access, UI state management).

This post is a running devlog. I’ll update it as I build.

# Day 1: June 16

The first day was setup and getting something on screen. Xcode project, README, MIT license. The first real decision was sandbox entitlements. On macOS, apps don’t get filesystem access by default. You have to declare what you need in an entitlements file. I needed access to ~/Music, so I added com.apple.security.files.user-selected.read-only and the music folder bookmark entitlement. Nothing in code. Just an XML file that the OS reads. So when it silently fails, there’s nothing to grep for.

Then I built a Home view backed by a FileListViewModel that scans ~/Music and returns the MP3 files. Nothing plays yet. Then I added an AudioPlayer class wrapping AVAudioPlayer, Apple’s straightforward playback API. Wired up play and pause. By the end of the day, tapping a file starts playback.

Coming from web development, SwiftUI’s declarative model feels familiar. Describe what the UI should look like given state; the framework handles updates. @State and @ObservedObject are doing the same job as React’s useState and useContext, just baked into the language syntax. The sandbox roughness aside, the day went faster than expected. Something plays. That’s enough.

# Day 2: June 17

The first thing I did today was tear apart yesterday’s structure. The initial setup had everything tangled together, with view models holding UI concerns next to audio logic. I split it into core/ for pure Swift services with no SwiftUI dependency, and ui/ for views and view models. This forces a clean dependency direction: UI depends on core, never the other way around.

I added prev/next buttons. The queue is an array of songs with a current index, and advancing means incrementing the index and calling play(_:) on the next item. Auto-advance was a small satisfying piece: AVAudioPlayer calls a delegate method when a track finishes, and I forward that to the view model, which moves the queue forward automatically.

For the progress bar, AVAudioPlayer doesn’t push updates. You have to poll currentTime on a timer. So I set up a Timer that fires every 0.5 seconds and updates a @Published property on the view model. Seeking was simpler: dragging the scrubber just sets currentTime directly on the player.

MP3 files can embed album art in their metadata. I load it using AVURLAsset, reading the commonMetadata array and looking for the artwork key. It’s async, so there’s a brief moment where a placeholder shows before the artwork resolves. Acceptable.

The most interesting part of the day was system Now Playing integration. macOS has a system-level Now Playing concept that surfaces in the menu bar and handles media keys. MPNowPlayingInfoCenter is a dictionary you write to with the current track’s title, artist, artwork, duration, and elapsed time. MPRemoteCommandCenter is where you register handlers for play, pause, next, previous, and seek. When the user presses a media key or uses the Now Playing widget, your handler fires. Getting artwork into the system display requires converting NSImage to MPMediaItemArtwork, which takes a closure returning a scaled image at a given size. Small detail, but easy to miss.

# Day 3: June 18

Today I added a sidebar for navigation and moved the player controls to the bottom edge of the window. The bottom placement feels natural, which is what most music players do, and it keeps the controls out of the way of the content above. Navigation state lives in its own AppNavigationState object, a small ObservableObject that any view can hold a reference to and call a method on. Cleaner than scattering navigation logic across view models.

I added a settings page, wired up to ⌘,. Most macOS users will try it instinctively. If it doesn’t work, the app feels unfinished. The song list now also shows title, artist, and album from MP3 tags instead of raw filenames. Same AVURLAsset metadata pipeline used for artwork; it’s all in commonMetadata, keyed by identifier.

SwiftUI makes light/dark mode largely automatic if you use semantic colors like Color.primary. But some of the custom palette colors needed explicit light and dark variants defined in the asset catalog. Not hard, just easy to forget until you switch modes and something looks wrong.

Two bugs needed fixing. The system’s Now Playing display was showing the app icon instead of album art. The fix was making sure MPNowPlayingInfoCenter gets fresh artwork set whenever the track changes. It was being set too late in the update sequence. The other: closing the window was stopping playback. On macOS, closing a window doesn’t have to mean quitting; apps are expected to keep running. The fix was adjusting behavior so closing hides the window rather than terminating the audio session.

I’m updating this post daily as I build. Next up: search and filtering, shuffle, repeat.