Building a High-Performance Custom Music Player for iOS

Developing a custom music player for iOS goes far beyond wrapping AVPlayer in a play button. Users expect seamless background playback, rich now-playing integration, AirPlay support, and responsive controls that feel native. This guide walks you through creating a production-ready music player with advanced controls using Swift, UIKit, and modern frameworks like AVFoundation and MediaPlayer. We’ll cover architecture, UI design, background audio, remote control handling, and performance tuning so you can ship an app that stands out on the App Store.

Core Architecture: Leveraging AVFoundation

Every custom music player in iOS starts with AVFoundation. The primary player classes are AVPlayer (for streaming and local files) and AVAudioPlayer (for simpler local audio playback). For a full-featured player, AVPlayer is the better choice because it supports streaming, timed metadata, and integration with AirPlay and background modes.

Setting Up the Player Instance

Create a singleton or service class that holds the AVPlayer instance. This ensures persistent playback state across view controllers. Configure the player with a local or remote URL:

import AVFoundation

class MusicPlayerManager {
    static let shared = MusicPlayerManager()
    let player: AVPlayer
    
    private init() {
        player = AVPlayer()
        configureAudioSession()
    }
    
    func play(url: URL) {
        let item = AVPlayerItem(url: url)
        player.replaceCurrentItem(with: item)
        player.play()
    }
}

Always set the audio session category to .playback so the player continues to work when the phone is locked or in silent mode. This is mandatory for any music app.

Managing Player States

Use KVO (Key-Value Observing) on AVPlayer’s timeControlStatus and AVPlayerItem’s status to react to playback state changes (playing, paused, waiting for network). Observing CMTime via addPeriodicTimeObserver drives your progress slider and elapsed time label.

Designing the User Interface with SwiftUI

While UIKit is mature, SwiftUI offers faster iteration and a declarative syntax perfect for music player UIs. Create a clean layout with album art, track info, progress slider, volume control, and playback buttons.

Key UI Components

  • Album Art View – an AsyncImage that loads cover artwork from a remote URL or local asset.
  • Track and Artist Labels – dynamic text that updates via @Published properties in your view model.
  • Progress Slider – a custom Slider styled with track and thumb colors, bound to the player’s current time.
  • Volume Slider – use MPVolumeView when targeting system volume, or build a custom slider that sets AVPlayer.volume (note: system volume is preferred for consistency).
  • Play/Pause, Forward, Backward Buttons – clearly labeled, with haptic feedback on touch.

Use a GeometryReader to adapt to different screen sizes, especially for the album art which should scale nicely on iPads.

Connecting UI to the Player

Let your view model conform to ObservableObject and publish the current track, playback time, and playing state. The SwiftUI view observes these:

class PlayerViewModel: ObservableObject {
    @Published var currentTime: TimeInterval = 0
    @Published var isPlaying = false
    @Published var currentTrack: Track?
    
    // … bindings to MusicPlayerManager
}

Use onReceive and Timer or the periodic time observer to update currentTime smoothly.

Integrating Remote Control and Now Playing Info

A professional music player must respond to remote control events (lock screen, control center, Apple Watch, AirPods) and display “Now Playing” info. This is done via MPNowPlayingInfoCenter and MPRemoteCommandCenter.

Setting Up Remote Commands

In AppDelegate or your player manager, configure the remote command center after the audio session is activated:

import MediaPlayer

func setupRemoteCommands() {
    let commandCenter = MPRemoteCommandCenter.shared()
    
    commandCenter.playCommand.addTarget { [weak self] _ in
        self?.player.play()
        return .success
    }
    
    commandCenter.pauseCommand.addTarget { [weak self] _ in
        self?.player.pause()
        return .success
    }
    
    commandCenter.nextTrackCommand.addTarget { [weak self] _ in
        self?.skipToNext()
        return .success
    }
    
    commandCenter.previousTrackCommand.addTarget { [weak self] _ in
        self?.skipToPrevious()
        return .success
    }
    
    commandCenter.changePlaybackPositionCommand.addTarget { [weak self] event in
        guard let event = event as? MPChangePlaybackPositionCommandEvent else { return .commandFailed }
        self?.player.seek(to: CMTime(seconds: event.positionTime, preferredTimescale: 1000))
        return .success
    }
}

Enable background modes in your Xcode project: select the target, go to Signing & Capabilities, add “Background Modes”, and check “Audio, AirPlay, and Picture in Picture”.

Updating Now Playing Info

Every time the track changes or playback status updates, call MPNowPlayingInfoCenter.default().nowPlayingInfo with a dictionary containing keys like MPMediaItemPropertyTitle, MPMediaItemPropertyArtist, MPMediaItemPropertyArtwork, MPNowPlayingInfoPropertyElapsedPlaybackTime, and MPNowPlayingInfoPropertyPlaybackRate. For artwork, convert your UIImage to MPMediaItemArtwork.

Handling Background Audio with Audio Sessions

Configure the audio session once at launch:

func configureAudioSession() {
    do {
        let session = AVAudioSession.sharedInstance()
        try session.setCategory(.playback, mode: .default, policy: .longFormAudio)
        try session.setActive(true)
    } catch {
        print("Failed to set audio session: \(error)")
    }
}

For apps that also record or use voice, be careful with the category. Music players should almost always use .playback to ensure audio continues in background. Test on a device: lock the screen and verify playback continues; the control center should show your “Now Playing” card.

Implementing Advanced Controls: Equalizer, Playlists, and AirPlay

Once the basics work, differentiate your player with advanced features.

Graphic Equalizer

Use AVAudioEngine with AVAudioUnitEQ to apply a 10-band or custom equalizer. This requires replacing AVPlayer with an engine-based approach or connecting an AVAudioPlayerNode to the engine. Apple’s documentation on AVAudioEngine provides sample code. Offer preset bands (Rock, Pop, Jazz) and allow custom sliders.

Playlist Management

Store playlists in Core Data or SwiftData. Each playlist contains an ordered list of track URLs (local or remote). Implement drag-and-drop reordering in a UITableView or List. Support offline downloads using URLSession background configuration so downloads continue outside the app.

AirPlay and External Devices

AVPlayer automatically supports AirPlay when the audio session is set to .playback and the route is available. For fine-grained control, use MPVolumeView’s route picker or AVRoutePickerView (iOS 11+). Provide a toggle to force local playback only when needed.

Testing and Performance Optimization

Music players must be responsive. Follow these tips to keep the app fast and battery-friendly.

  • Profile with Instruments – use the Time Profiler and Core Animation instruments to detect main-thread blockages. Avoid heavy UI updates on each frame; throttle slider updates to ~30 FPS.
  • Buffering Strategy – set preferredForwardBufferDuration on AVPlayerItem to balance start-up time vs. memory usage. A value of 10 seconds works for most streams.
  • Memory Management – when switching tracks, release old player items and artwork images. Use NSCache for album art.
  • Background Crash Handling – ensure your player manager does not deallocate while playing. Use strong references and reinitialize if needed.

Testing on Real Devices

Simulators cannot accurately test background audio, AirPlay, or volume control. Always test on physical iPhones and iPads running different iOS versions. Verify that the control center updates correctly and that the lock screen shows album art.

Best Practices for iOS Music Player Development

Drawing from years of production experience, here are additional recommendations.

  • Optimize performance – use lazy loading for large playlists and pre-decode artwork at thumbnail size.
  • Design intuitive UI – follow Apple’s Human Interface Guidelines for media playback. Use system icons for standard actions like shuffle and repeat.
  • Handle interruptions gracefully – listen for AVAudioSession.interruptionNotification and pause/resume accordingly. Show a temporary overlay if a phone call comes in.
  • Respect user privacy – only request microphone permission if you genuinely need it (e.g., voice commands). Always explain why.
  • Provide accessibility – label all buttons, support VoiceOver, and adopt dynamic type for labels.

Conclusion

Building a custom music player with advanced controls in iOS is rewarding but requires attention to detail in audio session management, remote control integration, and UI responsiveness. By leveraging AVFoundation, MPNowPlayingInfoCenter, and SwiftUI, you can create an app that delivers a polished listening experience. Start with the core player, add background playback, then layer on features like equalizers and playlists. Test thoroughly on real devices, and your music player will stand out in a crowded market.

For further reading, explore Apple’s AVFoundation Media Playback documentation, the MediaPlayer framework reference, and the Audio Interruptions guide. These resources will help you handle edge cases and deliver a robust product.