●FUNDING — Rork raised a $15M seed led by Left Lane Capital, with Peak XV, True Ventures, Goodwater, and a16z Speedrun joining●ENGINE — Rork Max runs on Claude Code and Claude Opus 4.6; it drew 8M+ views on X and doubled annual revenue in two weeks●SWIFT — Rork Max is the first web-based Swift app builder, positioned to replace Apple's traditional Xcode●PRODUCT — Rork Max covers the whole Apple ecosystem: iPhone, iPad, Apple Watch, Apple TV, Vision Pro, and iMessage●CLASSIC — The original Rork uses React Native (Expo), building iOS/Android apps from a plain-English description●PRICING — Start free; paid plans begin at $25/mo, and Rork Max is $200/mo●FUNDING — Rork raised a $15M seed led by Left Lane Capital, with Peak XV, True Ventures, Goodwater, and a16z Speedrun joining●ENGINE — Rork Max runs on Claude Code and Claude Opus 4.6; it drew 8M+ views on X and doubled annual revenue in two weeks●SWIFT — Rork Max is the first web-based Swift app builder, positioned to replace Apple's traditional Xcode●PRODUCT — Rork Max covers the whole Apple ecosystem: iPhone, iPad, Apple Watch, Apple TV, Vision Pro, and iMessage●CLASSIC — The original Rork uses React Native (Expo), building iOS/Android apps from a plain-English description●PRICING — Start free; paid plans begin at $25/mo, and Rork Max is $200/mo
Audio Apps That Survive Calls, Unplugged Headphones, and Other Apps Taking Over
How to handle AVAudioSession interruption and route-change notifications correctly so playback survives calls and headphone unplugs, with working Swift code.
When I shipped a small app, built as an indie developer, that plays meditation and study audio, the very first bug report read: "after a phone call, the sound doesn't come back." It is natural for audio to stop when a call comes in mid-playback, but even after the call ended, the app stayed paused, with the on-screen play button frozen in its "playing" look.
The cause was that I was not handling AVAudioSession interruptions at all. Playback was left entirely to AVAudioPlayer, and my code was not listening to the OS telling it "I've interrupted you now" and "you may resume." I learned the hard way that the quality of an audio app is decided less by whether the features run and more by how carefully it answers this kind of interruption.
This article walks through handling interruptions from calls and Siri, and route changes from plugging and unplugging headphones, in a form you can drop straight into the native Swift that Rork Max generates.
First, decide the audio session category
Before anything else, declare to the OS what kind of audio app this is. Skip it and your sound may vanish with the silent switch, or you may needlessly stop another app's music.
import AVFoundationfunc configureAudioSession() { let session = AVAudioSession.sharedInstance() do { // A playback-first app: plays regardless of the silent switch try session.setCategory(.playback, mode: .default) try session.setActive(true) } catch { print("Audio session setup failed: \(error)") }}
.playback declares "this app's sound is the content itself," letting it play while locked or in the background. Conversely, if you only want to layer a short effect over another app's music, you would choose .ambient. Choosing the right declaration up front is the foundation for every later behavior.
Subscribe to interruption notifications
Interruptions arrive via AVAudioSession.interruptionNotification. There are two phases — .began and .ended — and the end carries a hint about whether you may resume on your own.
final class PlaybackController { private var wasPlayingBeforeInterruption = false func observeInterruptions() { NotificationCenter.default.addObserver( self, selector: #selector(handleInterruption), name: AVAudioSession.interruptionNotification, object: nil) } @objc private func handleInterruption(_ note: Notification) { guard let info = note.userInfo, let raw = info[AVAudioSessionInterruptionTypeKey] as? UInt, let type = AVAudioSession.InterruptionType(rawValue: raw) else { return } switch type { case .began: // A call or Siri broke in. Remember the state. wasPlayingBeforeInterruption = player.isPlaying player.pause() case .ended: guard let optsRaw = info[AVAudioSessionInterruptionOptionKey] as? UInt else { return } let options = AVAudioSession.InterruptionOptions(rawValue: optsRaw) // Resume only if shouldResume is set AND we were playing before if options.contains(.shouldResume), wasPlayingBeforeInterruption { try? AVAudioSession.sharedInstance().setActive(true) player.play() } @unknown default: break } }}
The crux here is how you treat shouldResume. The OS distinguishes "interruptions you may resume from" and "interruptions you should not." For example, when another music app comes to the front by the user's action, you should not grab playback back on your own. Resuming when shouldResume is not set goes against the user's intent. Check whether you were playing before the interruption as well, and only restore quietly when both hold — that is the well-behaved implementation.
✦
Thank you for reading this far.
Continue Reading
What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.
WHAT YOU'LL LEARN
✦You can subscribe to AVAudioSession interruption (calls, Siri) and route-change (headphone unplug) notifications correctly so playback never breaks
✦You'll separate the cases where you may resume on your own from the ones where you should wait for the user, using the shouldResume flag
✦You'll wire up Now Playing and remote commands to ship a production-grade audio app controllable from the lock screen and Control Center
Secure payment via Stripe · Cancel anytime
✦
Unlock This Article
Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.
Unplug your headphones and have audio keep blasting from the speaker — Apple's Human Interface Guidelines explicitly call this out as behavior to avoid. Subscribe to AVAudioSession.routeChangeNotification and pause when the output route is lost.
func observeRouteChanges() { NotificationCenter.default.addObserver( self, selector: #selector(handleRouteChange), name: AVAudioSession.routeChangeNotification, object: nil)}@objc private func handleRouteChange(_ note: Notification) { guard let info = note.userInfo, let raw = info[AVAudioSessionRouteChangeReasonKey] as? UInt, let reason = AVAudioSession.RouteChangeReason(rawValue: raw) else { return } switch reason { case .oldDeviceUnavailable: // Headphones or Bluetooth device removed. Pause before it falls to the speaker. DispatchQueue.main.async { self.player.pause() } default: break }}
.oldDeviceUnavailable means "the output you were just using is gone," and it fires the instant you unplug headphones. By contrast, simply plugging in a new device yields a different reason code, so you do not pause there. Pausing uniformly without checking the reason creates the inconvenience of stopping unintentionally the moment the user switches to Bluetooth.
Connect Now Playing and remote commands
An app that plays in the background is only production-grade once it can be controlled from the lock screen and Control Center. Receive actions with MPRemoteCommandCenter and hand display info to MPNowPlayingInfoCenter.
import MediaPlayerfunc setupRemoteCommands() { let center = MPRemoteCommandCenter.shared() center.playCommand.addTarget { [weak self] _ in self?.player.play() return .success } center.pauseCommand.addTarget { [weak self] _ in self?.player.pause() return .success }}func updateNowPlaying(title: String, elapsed: TimeInterval, duration: TimeInterval) { var info = [String: Any]() info[MPMediaItemPropertyTitle] = title info[MPMediaItemPropertyPlaybackDuration] = duration info[MPNowPlayingInfoPropertyElapsedPlaybackTime] = elapsed info[MPNowPlayingInfoPropertyPlaybackRate] = player.isPlaying ? 1.0 : 0.0 MPNowPlayingInfoCenter.default().nowPlayingInfo = info}
Whenever an interruption or route change alters playback state, remember to call updateNowPlaying to refresh PlaybackRate. Skip it and you get a mismatch: paused inside the app but shown as playing on the lock screen. The "button frozen in playing state" bug I hit earlier was, at root, the same state inconsistency.
Enable background playback and test
Finally, enable Background Modes → Audio in Capabilities so sound keeps playing in the background. Projects generated by Rork Max sometimes lack this setting, so check it. Without it, audio stops the moment the screen closes.
Test on a real device by running through this sequence once: place and hang up a call to your own number during playback, unplug headphones during playback, open another music app during playback, and trigger Siri during playback. After each, confirm the three — sound, screen, and lock screen — do not disagree.
Interruption
Expected behavior
Call / Siri
Pause; resume after if shouldResume is set
Unplug headphones
Pause (do not blast the speaker)
Another music app
Yield and stop; do not grab it back
App to background
Keep playing
The order to fold this into an existing app
When retrofitting interruption handling, I find this order safest.
Call the audio session category setup exactly once, right at launch
Initialize the interruption, route-change, and remote-command subscriptions together
Run one real-device pass through call, headphone, and other-app interruptions to crush the combinations that occur in production
The caution is not to re-register subscriptions on every screen appearance. In my own case, adding the subscription inside a view's onAppear registered the notification handler multiple times, and a single interruption fired play and pause twice. I recommend enforcing a single subscription in one place as the fix.
I believe the polish of an audio app concentrates in how it answers these interruptions. It is quieter work than adding features, but tend to it carefully and users receive an impression of "a well-made app" without ever putting it into words. I hope this helps anyone working on the same problem.
Share
Thank You for Reading
Rork Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.