Audio Recording in watchOS Tutorial
Learn how to record audio right in your own watchOS apps! By Soheil Azarpour.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Audio Recording in watchOS Tutorial
30 mins
In watchOS 2, Apple introduced a new API to play and record multimedia files on the Apple Watch. In watchOS 4, Apple greatly improved the multimedia API and created great opportunities to build innovative apps and enhance the user experience.
In this tutorial, you’ll learn about watchOS 4’s audio recording and playback APIs and how to use them in your apps. You’ll add audio recording to a memo app so that users can record and review their thoughts and experiences right from their wrists. Let’s get started!
Getting Started
Download the starter project for the tutorial here.
The starter project you’ll use in this tutorial is called TurboMemo. Open TurboMemo.xcodeproj in Xcode and make sure the TurboMemo scheme for iPhone is selected. Build and run in the iPhone simulator, and you’ll see the following screen:

Users can record audio diaries by simply tapping the plus (+) button. The app sorts the entries by date, and users can play back an entry by tapping it.
Try adding some entries to create some initial data.

Now, stop the app and change the scheme to TurboMemoWatch. Build and run in the Watch Simulator, and you’ll see the following screen:

The Watch app syncs with the iPhone app to display the same entries, but it doesn’t do anything else yet. You’re about to change that.
Audio Playback
There are two ways you can play an audio file in watchOS. You can either use the built-in media player, or build your own. You’ll start with the built-in media player as it’s simpler and more straightforward. In the next section, you’ll build your own media player.
The easiest way to play a media file is to present the built-in media player controller using the presentMediaPlayerController(with:options:completion:) method of WKInterfaceController. All you have to do is to pass in a file URL that corresponds to the index of the row selected by the user in WKInterfaceTable.
Open TurboMemoWatchExtension/InterfaceController.swift, find the implementation of table(_:, didSelectRowAt:) and update it as follows:
// 1
let memo = memos[rowIndex]
// 2
presentMediaPlayerController(
with: memo.url,
options: nil,
completion: {_,_,_ in })
Going through this step-by-step:
- You get the selected memo by passing the selected row index to the array of memos.
-
You present a media player controller by calling
presentMediaPlayerController(with:options:completion:)and passing in the URL of the selected memo. You can optionally pass in a dictionary of playback options. Since you don’t want any particular customization at this point, you passnil. In the completion block, you can check playback results based on your specific needs. Because the API requires a non-nil completion block, you simply provide an empty block.
That’s it! Build and run the app. Tap on a row in the table and you can now listen to the memos!

Building an Audio Player
The media player controller in watchOS is great for playing short media files but it comes with limitations: As soon as the user dismisses the player, playback stops. This can be a problem if the user is listening to a long audio memo, and you want to continue playing the file even when the user closes the media player.
The built-in media interface can’t be customized either. So if you want more control over the playback and appearance of the media player, you need to build your own.
You’ll use WKAudioFilePlayer to play long audio files. WKAudioFilePlayer gives you more control over playback and the rate of playback. However, you’re responsible for providing an interface and building your own UI.
WKAudioFilePlayer only through a connected Bluetooth headphone or speaker on a real device. You won’t be able to hear the audio using WKAudioFilePlayer either in watchOS simulator or via Apple Watch speaker. Therefore, to follow along with this section, you’ll need an Apple Watch that’s paired with Bluetooth headphones.
The starter project includes AudioPlayerInterfaceController. You’ll use AudioPlayerInterfaceController as a basis for your custom audio player. But before you go there, while you’re still in InterfaceController, you can rewire the code to call the AudioPlayerInterfaceController instead.
Once again, find the implementation of table(_:didSelectRowAtIndex:) in InterfaceController.swift, and update it as follows:
override func table(
_ table: WKInterfaceTable,
didSelectRowAt rowIndex: Int) {
let memo = memos[rowIndex]
presentController(
withName: "AudioPlayerInterfaceController",
context: memo)
}
Make sure you place the existing code entirely. Here, instead of using the built-in media player, you call your soon-to-be-made custom media player. If you build and run at this point, and select a memo entry from the table, you’ll see the new media player that does … nothing! Time to fix that.

Open AudioPlayerInterfaceController scene in TurboMemoWatch/Interface.storyboard. AudioPlayerInterfaceController provides a basic UI for audio playback.

This has:
-
titleLabelwhich is blank by default -
playButtonthat’s hooked up toplayButtonTapped(). -
a static label that says
Time lapsed:. -
interfaceTimerthat is set to0by default.
Now, open AudioPlayerInterfaceController.swift and add the following properties at the beginning of AudioPlayerInterfaceController:
// 1
private var player: WKAudioFilePlayer!
// 2
private var asset: WKAudioFileAsset!
// 3
private var statusObserver: NSKeyValueObservation?
// 4
private var timer: Timer!
Taking this line-by-line:
-
playeris an instance ofWKAudioFilePlayer. You’ll use it to play back an audio file. -
assetis a representation of the voice memo. You’ll use this to create a newWKAudioFilePlayerItemto play the audio file. -
statusObserveris your key-value observer for the player’sstatus. You’ll need to observer thestatusof theplayerand start playing only if the audio file is ready to play. -
timerthat you use to update the UI. You kick off the timer at the same time you start playing. You do this because currently there’s no other way to know when you’re finished playing the audio file. You’ll have to maintain your own timer with the same duration as your audio file.
You’ll see all these in action in a moment.
Now, add the implementation of awakeWithContext(_:) to AudioPlayerInterfaceController as follows:
override func awake(withContext context: Any?) {
super.awake(withContext: context)
// 1
let memo = context as! VoiceMemo
// 2
asset = WKAudioFileAsset(url: memo.url)
// 3
titleLabel.setText(memo.filename)
// 4
playButton.setEnabled(false)
}
Again, taking this line-by-line:
-
After calling
superas ordinary, you know for sure thecontextthat’s being passed to the controller is aVoiceMemo. This is Design by Contract! -
Create a
WKAudioFileAssetobject with the voice memo and store it inasset. You’ll reuse the asset to replay the same memo when user taps on the play button. -
Set the
titleLabelwith the filename of the memo. -
Disable the
playButtonuntil the file is ready to be played.
You prepared the interface to playback an audio file, but you haven’t done anything to actually play it. You’ll kick off the playback in didAppear() so that playback starts when the interface is fully presented to the user.
Speaking of didAppear(), add the following to AudioPlayerInterfaceController:
override func didAppear() {
super.didAppear()
prepareToPlay()
}
Here, you simply call a convenient method, prepareToPlay(). So let’s add that next:
private func prepareToPlay() {
// 1
let playerItem = WKAudioFilePlayerItem(asset: asset)
// 2
player = WKAudioFilePlayer(playerItem: playerItem)
// 3
statusObserver = player.observe(
\.status,
changeHandler: { [weak self] (player, change) in
// 4
guard
player.status == .readyToPlay,
let duration = self?.asset.duration
else { return }
// 5
let date = Date(timeIntervalSinceNow: duration)
self?.interfaceTimer.setDate(date)
// 6
self?.playButton.setEnabled(false)
// 7
player.play()
self?.interfaceTimer.start()
// 8
self?.timer = Timer.scheduledTimer(
withTimeInterval: duration,
repeats: false, block: { _ in
self?.playButton.setEnabled(true)
})
})
}
There’s a lot going on here:
-
Create a
WKAudioFilePlayerItemobject from theassetyou set earlier inawake(withContext:). You have to do this each time you want to play a media file, sinceWKAudioFilePlayerItemcan’t be reused. -
Initialize the
playerwith theWKAudioFilePlayerItemyou just created. You’ll have to do this even if you’re playing the same file again. -
The
playermay not be ready to play the audio file immediately. You need to observe thestatusof theWKAudioFilePlayerobject, and whenever it’s set to.readyToPlay, you can start the playback. You use the new Swift 4 key-value observation (KVO) API to listen to changes inplayer.status. -
In the observer block, you check for the player’s
statusand if it’s.readyToPlay, you safely unwrapdurationof theassetand continue. Otherwise, you simply ignore the change notification. -
Once the item is ready to play, you create a
Dateobject with the duration of the memo, and updateinterfaceTimerto show the lapsed time. -
Disable the
playButtonwhile you’re playing the file. -
Start playing by calling
player.play(), and at the same time, start the countdown in the interface. -
Kick off an internal timer to re-enable the
playButtonafter the playback is finished so the user can start it again if they wish.
That was a big chunk of code, but as you see, it’s mostly about maintaining the state of the WKAudioFilePlayer and keeping the interface in sync.
currentTime of WKAudioFilePlayerItem is not KVO-complaint so you can’t add an observer. Ideally, you would want to observe currentTime instead of maintaining a separate timer on your own.
Before you build and run, there’s one more thing to add!
When the timer is up and playButton is enabled, the user should be able to tap on Play to restart playing the same file. To implement this, find the implementation of playButtonTapped() in AudioPlayerInterfaceController.swift and update it as follows:
@IBAction func playButtonTapped() {
prepareToPlay()
}
It’s that simple! Merely call the convenient method, prepareToPlay(), to restart the playback.
Next, build and run, and select a voice memo from the list. The app will present your custom interface. The interface will automatically start playing the audio file, and once it’s stopped, the Play button will be re-enabled and you can play it again.

If you have more than one item to play, such as in a playlist, you’ll want to use WKAudioFileQueuePlayer instead of WKAudioFilePlayer and queue your items. The system will play queued items back-to-back and provide a seamless transition between files.