In this post, we’ll go through how you can build an alarm app for iOS.

Right off the bat, I have to warn you that this kind of functionality isn’t something that’s approved by Apple and we’ll be implementing a questionable method, that has been proven to be AppStore approved, based on the dozen of apps that are doing just that.

We’ll actually skip the simplest part of the alarm app – the actual alarm code itself, since it’s pretty straightforward. Whether you choose to go with a timer, local notification or something else – it doesn’t really matter. The issue with implementing a robust alarm app on iOS is reliably getting a sound notification to the user when the app’s not running, the ringer set to vibrate and the sound all the way down and that’s what we’ll focus on.

There are four methods to actually implement an alarm behavior in your app and we’ll go through them all.

Never sleep method

Some of you might remember a dark time when iOS had no multitasking. Back then if you switched an app, that app was killed and the new one was launched. With iOS4, Apple changed all that, so that when you switch apps, the current app would just go in a background state before being suspended.

In order to insure a smooth transition, they introduced an info.plist key, named UIApplicationExitsOnSuspend, which allowed developers to opt-out of the multitasking behavior.

You’re probably asking why we have to opt-out of multitasking, if we want to have the alarm work? Well, before iOS 4, if you wanted an app to continue working, while the phone’s display was off, you had to just lock your screen, while in that app. After iOS 4 that behavior changed and whenever the user locked his device, while an app was running, that app was put into the background and suspended.

Turning off multitasking, by setting the UIApplicationExitsOnSuspend will revert to the old functionality, so all you have to do is instruct your user to set their alarm, not press the home button and then just lock their device.

That way, you can continue executing code for the alarm, whether it’s a timer that you’ve set up or a local notification or something else.

It’s easy and straightforward, though it requires that you opt out of multitasking, which can be a big issue and the cooperation of your user, which you can’t really count on. A lot of people exit out of the current app before locking their phone and while you might implement a local notification to warn them that they’ve turned off the app, it’s just not a great user experience.

Location based method

The second method uses one of the approved multitasking background modes – the location updates.

Basically you implement a CLLocationManager, set it up so that it’ll deliver updates in the background and run your alarm logic in the update pings that it provides.

Though make sure that you enable Location updates in the Background modes section in Capabilities. Be sure to add the relevant privacy key in the info.plist, as well.

let manager = CLLocationManager() manager.desiredAccuracy = kCLLocationAccuracyBestForNavigation manager.activityType = .other manager.allowsBackgroundLocationUpdates = true manager.requestAlwaysAuthorization() 1 2 3 4 5 let manager = CLLocationManager ( ) manager . desiredAccuracy = kCLLocationAccuracyBestForNavigation manager . activityType = . other manager . allowsBackgroundLocationUpdates = true manager . requestAlwaysAuthorization ( )

We’re setting the desired accuracy to the highest possible, so that we can get updates as regularly as possible and we’re telling iOS that we’re an other type of activity type to prevent the system from automatically powering down the GPS and preventing updates from coming over to us.

You can probably figure out that constantly running the GPS radios with the highest possible accuracy isn’t something that’s great for the battery life of the device. Thus we come to the drawbacks of this method – the user might wake up to a drained battery, if the device doesn’t turn off before that.

Plugging in your device before going to bed is something that a lot of people do, which will mitigate the problem and you can even prompt the user to do so before activating the alarm, but I think we can agree that it’s not the best of user experiences.

Also with iOS 11, you’re reliant on the user to actually choose to share their location always, instead of just while using the app or the alarm won’t work.

Microphone method

Next up, we have the microphone method. You might remember a while ago when the Facebook iOS app “had a bug”, which caused it to play a silent file, while in the background, resulting in indefinite background execution.

That’s our next method, which actually requires some sort of a gimmick to successfully get through the AppStore approval process.

A lot of apps lean into this method, by providing such functionality as sleep monitoring – trying to figure out when you enter REM sleep, using the microphone to identify things like breathing patterns, movement, snoring, etc

It doesn’t really matter what sort of gimmick you choose, so long as it gets you through the AppStore. Frankly, we don’t even need the user to grant us microphone access, as them refusing it will just result in us getting silence, which is fine, since we’ll be executing our alarm logic, instead of anything to do with the microphone.

First off, you’ll have to enable Audio, AirPlay, and Picture in Picture in the Background modes section in Capabilities. Be sure to enter the relevant privacy key in the info.plist.

let soundFileURL = \* URL to file *\ let recordSettings = [AVEncoderAudioQualityKey: AVAudioQuality.min.rawValue, AVEncoderBitRateKey: 16, AVNumberOfChannelsKey: 2, AVSampleRateKey: 44100.0] as [String : Any] let audioSession = AVAudioSession.sharedInstance() do { try audioSession.setCategory( AVAudioSessionCategoryPlayAndRecord) } catch let error as NSError { print("audioSession error: \(error.localizedDescription)") } do { try audioRecorder = AVAudioRecorder(url: soundFileURL, settings: recordSettings as [String : AnyObject]) audioRecorder?.prepareToRecord() audioRecorder?.record() } catch let error as NSError { print("audioSession error: \(error.localizedDescription)") } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 let soundFileURL = \ * URL to file * \ let recordSettings = [ AVEncoderAudioQualityKey : AVAudioQuality . min . rawValue , AVEncoderBitRateKey : 16 , AVNumberOfChannelsKey : 2 , AVSampleRateKey : 44100.0 ] as [ String : Any ] let audioSession = AVAudioSession . sharedInstance ( ) do { try audioSession . setCategory ( AVAudioSessionCategoryPlayAndRecord ) } catch let error as NSError { print ( "audioSession error: \ ( error . localizedDescription ) " ) } do { try audioRecorder = AVAudioRecorder ( url : soundFileURL , settings : recordSettings as [ String : AnyObject ] ) audioRecorder ? . prepareToRecord ( ) audioRecorder ? . record ( ) } catch let error as NSError { print ( "audioSession error: \ ( error . localizedDescription ) " ) }

This method uses significantly less battery from the previous one, though it’s not free.

The main problem here is that you’ll have to make the user trust you with recording them for a lot of a time, which seems suspect.

Also the user will be getting a red bar, indicating that your app is recording them, while in the background, which is sure to annoy a lot of people, even if it’s letting you run your alarm logic during that time.

Thus this method also doesn’t pass the user experience bar.

Push notifications method

The final and most UX friendly method is the push notification method.

The idea here is to schedule a background update notification for the time the alarm is set, using it to trigger the alarm sound.

I know what you’re thinking – the user might say no, we can only play 30 seconds of audio with that, they’re not reliant, they’re coalesced, they don’t go through the ringer set to vibrate and the sound turned off.

We can circumvent most of these, in ways that make the app feel as close as possible to a real alarm app, with the best possible UX.

The user might say no

The good news on this point is that we don’t really care, if users choose not to give us access to push notifications.

While yes, it’ll result in a better user experience and we should nudge the user to accept them, some time during iOS 8, Apple actually made it so that even if the user declines the push dialog, you still get a device token to use with background fetch updates.

When the device receives a push notification with the “content-available” flag set to 1, the application:didReceiveRemoteNotification:fetchCompletionHandler: method will be called, allowing you to briefly run your alarm logic, which is all we need.

You should, of course, enable Background fetch and Remote notifications on the Capabilities tab.

The great thing about this method is that even if your app is in the background, it’ll be woken up and given some processing time.

A caveat of this method is that, if your user manually kills your app, by swiping up in the multitasking screen, you’ll never get the notification and won’t be able to process your alarm. You could implement a local notification that runs on applicationWillTerminate, that lets the user know that they should rerun the app, if they want their alarm, but keep in mind that applicationWillTerminate, may not get called when the user swipes away the app in the multitasking manager.

In that case, scheduling a couple of local notifications around the alarm time is always good, so that you’ll have a backup, even though they won’t go through silent mode. You can always cancel them when the time for the alarm comes.

They’re not reliant, they’re coalesced

Obviously there’s a possibility that if the user has airplane mode turned on, with no wi-fi, the device will never get a push notification, so that you can run your logic.

Shore up this case by implementing a local notification, if possible. In my testing, if the device is connected to the internet, there were no cases of failure to receive that push and miss the alarm.

In order to be extra reliable, since people will depend on this, you should schedule a couple of push notifications after the original time and cancel them, if the app manages to set off its alarm on time.

Actually playing the alarm sound

First off make sure to include the audio background mode in the capabilities, in order to play audio in the background.

do { try AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryPlayAndRecord, withOptions: [.DuckOthers, .DefaultToSpeaker]) try AVAudioSession.sharedInstance().setActive(true) UIApplication.sharedApplication().beginReceivingRemoteControlEvents() } catch { NSLog("Audio Session error: \(error)") } 1 2 3 4 5 6 7 do { try AVAudioSession . sharedInstance ( ) . setCategory ( AVAudioSessionCategoryPlayAndRecord , withOptions : [ . DuckOthers , . DefaultToSpeaker ] ) try AVAudioSession . sharedInstance ( ) . setActive ( true ) UIApplication . sharedApplication ( ) . beginReceivingRemoteControlEvents ( ) } catch { NSLog ( "Audio Session error: \ ( error ) " ) }

Here we set the shared audio session’s category to AVAudioSessionCategoryPlayAndRecord, so that we can play sound, while the device’s ringer switch is set to vibrate.

The .DuckOthers is specified to make other audio quieter, if there’s any mixable audio playing, so that our alarm can be heard. You can leave that out or use another option, if you prefer a different behavior.

The .DefaultToSpeaker is specified, so that the volume would go to the speaker, where it’ll be much louder and should wake up our user with ease.

beginReceivingRemoteControlEvents makes it so that the app handles the remote control options, like the play/pause buttons on the lock screen, in order to make it easier for our user to mute their alarm, once they wake up.

After you’ve set up your session, it’s as simple as creating your AVAudioPlayer object with a predetermined sound and playing it.

guard let player = /* load AVAudioPlayer player */ else { return } let volumeView = MPVolumeView() volumeView.volumeSlider.value = 1.0 player.play() 1 2 3 4 5 6 7 8 guard let player = /* load AVAudioPlayer player */ else { return } let volumeView = MPVolumeView ( ) volumeView . volumeSlider . value = 1.0 player . play ( )

The last piece of the puzzle is the volume level of the device. If the user has their volume level turned down low, the alarm won’t be heard.

There’s another workaround to enable you to programmatically control the volume level and it involves MPVolumeView

private extension MPVolumeView { var volumeSlider: UISlider { self.showsRouteButton = false self.showsVolumeSlider = false self.hidden = true var slider = UISlider() for subview in self.subviews { if subview.isKindOfClass(UISlider){ slider = subview as! UISlider slider.continuous = false (subview as! UISlider).value = 1 return slider } } return slider } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 private extension MPVolumeView { var volumeSlider : UISlider { self . showsRouteButton = false self . showsVolumeSlider = false self . hidden = true var slider = UISlider ( ) for subview in self . subviews { if subview . isKindOfClass ( UISlider ) { slider = subview as ! UISlider slider . continuous = false ( subview as ! UISlider ) . value = 1 return slider } } return slider } }

Using this extension on MPVolumeView allows us to get to the slider that adjusts the volume level and increase or decrease the volume.

For added affect you can gradually turn up the volume, so that the user is woken up more peacefully and you can send a local notification, so that the app is more accessible to the user, when the alarm sounds.

Conclusion

So there you have it, a relatively reliable way to implement an alarm app on iOS.

Feel free to mix and match all these methods for added stability.

Here’s hoping that Apple introduces a more robust way to achieve this kind of an app in iOS 12, until then let me know in the comments, if you know of a better way to handle any piece of the process.