Category Archives: Swift Programming

This Old App Episode 1: The Evaluation Stage

Before anyone thought that home improvement could be sexy, WGBH Boston started a series on house restoration. This Old House is  still going strong after 36 seasons. If you’ve never watched, each season a house in bad shape undergoes restoration. The cast goes through the paces of find what’s wrong, finding solutions and doing the work to make a new, beautiful home.

Recently, I headed over to my often neglected iTunes connect account, logged in and was met with the bright and friendly message:

 The following apps have unresolved issues and have been removed from the App Store:

Interval RunCalc

Going into the app, I found the message I expected to see:

Dear Developer,

On September 1, 2016, we announced that we’re implementing an ongoing process of evaluating and removing apps that no longer function as intended, don’t follow current review guidelines, or are outdated.

We noticed that your app has not been updated in a significant amount of time.

Next Steps

To keep your app on the App Store, submit an updated version for review and make sure it follows the latest App Review Guidelines. If you are unable to submit an update within 30 days, your app will be removed from the App Store until you submit an update and it is approved.

If Your App is Removed

Your app will remain fully functional for current users. They will experience no interruption to services, will still be able to buy in-app purchases, and can re-download the app while restoring from an iCloud or iTunes backup. However, we recommend that you update your app as soon as possible to reinstate it on the App Store and ensure that it remains functional and engaging for new and existing customers.

You can continue to use your current app name, as your app has not been deleted from your account.

Yep my app was one of the one removed in the great culling Apple threatened back in the fall of 2016. IntervalRunCalc was one of the apps that hit the chopping block. I wasn’t upset since the app had  only 13 sales in 2016. This isn’t big money, so really its no big deal if it lives or dies. If it had been more popular,  I would have been doing the revisions necessary to keep the app

I’ve just finished writing a piece for LinkedIn on Peanut butter and programming, which was a metaphor on what developers go through on a regular basis. I thought to follow up on that metaphor, I ‘d go through the process with a real app. I could go through the whole development process of Interval RunCalc, rebuilding the app using Swift instead Objective-C. Unlike my usual API of the week column, I thought of a This Old House type series on rebuilding an old app.   So for the next couple of weeks, I’ll be doing just that. We’ll explore the old application, then build models, new interfaces and putting it all together in controllers. We’ll go through the debugging process for the new app, and my adventure in getting the new app processed for Apple.  Along the way I’ll go through some good habits, find some old bad habits, and let you see my decisions as I rebuild the app.

The Original Interval RunCalc

Before we begin, you need to know the story of the app. Knowing the story of your app sets a lot of your design decisions.  You’ll need to know a little bit about running and one particular running coach, Jeff Galloway.  Running is a very popular form of physical exercise around the world. There is a lot of concern among non-runners and beginner runners about injury. Jeff Galloway, a 1972 Olympics  10,000 meter competitor for the U.S. and later running coach formulated a simple solution to the injury problem: don’t run so much by walking a little. His Run/Walk/Run method adds walk breaks or intervals during a run. You could for example run one minute, recover for thirty seconds, and the run another minute. You can workout on more complex patterns.  My current training runs for the upcoming race season is run four sets of  30 seconds run, 30 seconds walk, then pause for an full minute walk break, then repeat those for the length of the run.

For beginner runners, this has made entry into running easier, and has probably been one of the contributing factors to the popularity of race running. For older runners, the impact on bones is far less. There is a lot of evidence for cardio benefits of alternating between high intensity to low intensity activities repeatedly. Some faster distance runners are beginning to experiment with 30-second and less walk breaks once a mile to combat fatigue and run faster late in the race.

If you want to plan out a run walk run interval pattern to achieve a specific time or pace, there was no calculator to do it.  As a runner, I originally did it on spreadsheets, but the measurements systems necessary for pace, time and speed made such efforts clumsy. I wanted a tool, and figured others did too that did the calculations for me quickly. Input some numbers for intervals and walk breaks. Get back a pace, time and distance traveled. That’s why I wrote version 1.0 of Interval RunCalc.

The home screen contained basic information on pace, distance, time and speed. By changing one of these four variables you could see how it affected the other four.

This interface was very confusing, since you need to press the correct button and enter the figures correctly. Even for me, the developer, this was confusing. I fell victim to a very easy trap many developers fall into: Not testing the UI and believing what works is good enough.

Unless you know better, there were two more views in the app.  They were hidden in a button at the top.

 

Tap the button and you got a menu:

The Splits Calculator gave the splits at mile intervals and calculated your time if you decided to run some of those miles slower or faster.

  The interval calculator was the reason for the app. The user would input a series of intervals for the Pace,Speed,distance,and time.The calculator would give average pace, distance, and elapsed time back. The idea was to plan interval training runs using the app. Again, these interfaces worked, but was not easy to use.  I also added a few buttons at the bottom of the app that were confusing for adding and duplicating the intervals.

The last series of scenes I made were input scenes. Users would enter in values for HH:MM:SS time or MM:SS pace. A decimal keypad would enter  distance and speed. These all presented huge data validation problems, particularly the time.

    

Enter the Watch

A few months after the launch of Interval runcalc the Apple Watch came out. As fitness watch, it worked well with Apple’s products but not other companies. As of this writing, many still have annoying latency problems. You might  look at your watch for several seconds before getting an accurate number for your pace, time and distance. When I moved to an Apple watch from my Nike+  GPS watch, this drove me nuts. There is one exception to this latency problem: Apple’s native fitness app, which also has the active heart rate monitor data. Apple’s watch app however has no interval notifications. You can find those on a few of the big run-tracking apps, but not apple’s fitness activity app.

One solution did present itself. One of the 3rd party apps I used did intervals and sent a local notification to the phone when it was time to change intervals. When the phone went to sleep two minutes into a run, the notifications went to the watch — I had my notification by a tap on my wrist. I’d run both the app and the the apple fitness app at the same time.

If we’re going to update runcalc, there’s this one active thing it could do: send local notifications at the run/walk/run intervals. That way you can run this app in the background while using an apple watch to do the run.

Other Possible Changes

The original app was for iPhone only, I’d like to get it working well on a iPad, and iPhone plus in portrait might need a different look. These are both the regular size width class, and thus the issues would be related.

There’s one more feature I might want that isn’t there. Most of the running apps that allow additions of intervals for your workout want to know how many repetitions of the interval you want. For a one minute walk and one minute run, this is relatively easy. But for a 30 second run and 16 second walk, it isn’t so easy.  Knowing how many repetitions of this interval set make up a workout would be a helpful number.

Evaluating the Changes

Given the app’s current condition, let’s summarize the problems and ideas so far:

  1. The initial scene is confusing to use
  2. The two secondary scenes, splits and intervals are hidden
  3. Intervals are not easy to read
  4. I never use the splits
  5. The intervals are hard to use
  6. Input of data is difficult
  7. Add notifications for intervals
  8. Repetition calculation
  9. iPad use(regular width size classes)

I’ll find more as I go on, but this will give me my starting point, and set me planning.

There’s a bit of advice I’d like to start with before evaluating these. When writing an app, keep to your story, the reason your app does what it does. Consistent stories make stronger products. The story of Interval RunCalc is a calculator for running interval calculations. I’m working with the number for planning purposes, not running with this. With that story firmly in my mind, I start to make decisions.

The story is about calculations for intervals, yet I set intervals on a secondary page, not the main page. It needs to be up front. Looking at the first page , I have a lot of extra whitespace. What if I added the intervals in that whitespace?

Here’s another thought: a simple continuous run is a single interval. As long as I always have one interval in the table the data on top summarizes all runs. I’m now doing two pages on a single page.

I’ll start wire framing, sketching what this might look like:

I used the simplest, fastest way to wireframe. In this stage, don’t get overwhelmed by apps and templates letting you lay out a wireframe app. Paper,  pencil, and a few highlighters works fine. As I did here I used the sketching mode of the Apple Notes app. I can clean this all up later. Work fast, or you’ll get so bogged down in distractions you’ll never get anywhere.  I’ll add two buttons on the bottom for adding an interval and clearing the calculator:

Then I get into a debate with myself: Should there be a third button, a totals button? If I select an interval on the lower half to edit it, Should I edit it in the upper Half? If so, I’ll need a button to deselect all intervals and set the upper half to totals? While compact, It again presents me with a problem: A hidden or confusing interface. It would be better to select a row in the intervals, then go to a second view to edit and enter there.

 

This would look nice and consistent on the home screen too.Each might be a button, and tapping it could change it.  I’ll also add a button for the repetitions as well.

In a few user interface changes, I cover most of the issues with 1-5.

What to Cut

My first cut is notifications. That should be a separate app.  I do have data that would make the notifications easy to add, but that does not mean I add it. Notifications are not in the story.  This happens a lot in development, often referred to as feature creep.  One keeps adding functions to an app that it becomes, slow, unwieldy and overall unusuable. One classic example is Microsoft Word, which has so many extra features outside its core story of typing out a document. I’d rather use a simpler general word processor like Pages or Google Docs instead of Word for most documents. In updating an app, unless its a feature demanded by users, avoid adding anything outside the story. If you are an independent developer, this is a lot easier than being in an organization however. You might have teammates, upper management or marketing telling you to put something in that makes no sense. The advantage of getting the entire organization to  buy into the app story is keeping everyone focused.

For the same reasons, I’m dropping the splits calculator.  Since this is an Interval Run calculator, Splits are  not in the story.

Input Views

I’ll make one more change that I have been thinking about. I didn’t like the input in V1.0.  My original notes for 1.0 tell me I hated the time it takes to use a UIPickerview as a user.  In V2.0 I’ll change all my input to picker views to restrict the user, which they are very good at doing. Unlike other picker views I’ve seen, I’ll speed up the picker with more components. Instead of 60 rows in the seconds of minutes picker component, ill have components of 0 to 5 in one and 0 to 9 in the other, so I can quick dial any number.  My time pickers would look like this:

This solves a lot of the validation problems I had with the text fields by restricting input, but speeding up the input.

The Regular Width Devices

Our final problem in our list is the regualr width devices of iPad and iPhone plus in landscape. I have a simple solution to that. I’ll have two major views, the intervals and the stats. In regular width mode, they will be next to each other instead of on top of each other

With all that decided. I have the beginnings of a new version of this app. Many of the user interface decisions were made and those influence the structure. I was a good boy and for the moment I added only one more new feature, and killed several features. I have a few more user interface decisions to make, but I’ll get to those a little later.  My next step is to get into the code, and I usually start that with the models I’ll be using in the app. In the next lesson, we’ll build those new models, and look at the old code to find some of the problems I encountered with the old model I might want to change in the new one.

Short Repeating Notifications in iOS

In an earlier post, I discussed the repeating local notifications in the UserNotification framework, but that discussion misses one detail: repeating notifications less than 60 seconds. The framework will very stubbornly refuse to have a repeating notification less than a minute, giving a runtime error if you do.

In my LinkedInLearning course Learning iOS Notifications, I mentioned a workaround to get around this problem, but didn’t show how to in that course. Today, I’ll show you how to get repeating notifications for times less than 30 seconds.2017-02-27_06-16-49

Before I do, I will give a big warning from the user experience perspective. Rapid-fire notifications are annoying to the user under most circumstances. Don’t do this unless the functionality of the application is such that the user needs the frequent interruptions. Almost all the cases I can think of  are interval timers.

In fitness,  interval training is a recommended training regimen with lots of benefits .  There’s even a movement  created by U.S. Olympian Jeff Galloway among distance runners  to do run/walk/run a interval-based distanced running strategy to reduce injury. There’s also speedwork intervals for runners. It’s here that you’ll often see those less than 60 second time intervals repeated. Often it will be 30 seconds of intense activity repeated with lesser activity for a rest period. In this lesson, We’ll set up a 30 second repeat timer to show you the workaround to the 60 second limitation.

I’m going to assume you have a good idea of how to use local notifications for this lesson. If you don’t, try this lesson first or even better take my video course Learning iOS Notifications on Lynda.com or LinkedInlearning.

Make a New Project

I’ve made a starting file for this lesson which you can download here: shortrepeat_start If making this yourself, Make a new single view project ShortRepeat.  When the project loads change the display name to Repeat 30s2017-02-24_05-47-53

Go to the storyboard. Drag out two buttons and a label. Title and arrange them like this:

2017-02-24_05-53-15

You can leave this like this for this lesson if you want to code. I’ll clean them up in a little auto layout and color:

To see how I did this, you can download the start file. 2017-02-24_06-06-31

Open the assistant editor and add two actions to the code:

@IBAction func startButton(sender:UIButton) {
}
@IBAction func stopButton(sender:UIButton) {
}

Drag from the startButton‘s circle to the to the Start button. Drag from the stopButton‘s  circle to the Stop button.

You’re ready to code. You can download a starter version of this here: shortrepeat_start

Set Up a Notification

There’s a few steps we’ll need to set up before using a notification. Close the assistant editor and go to the ViewController class code. You’ll need to include the UserNotification framework first. Under import UIKit, add this

import UserNotifications

You’ll be using the user notification delegate too. Add its protocol to the class

class ViewController: UIViewController,UNUserNotificationCenterDelegate {

At the bottom of the class, add this to make the two delegates:

//MARK: - User Notification Center Delegates
    func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
        completionHandler()
    }
    
    func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
        completionHandler([.alert,.sound])
    }

The usernotificationCenter:willPresent notification method assures us that the notification will show up and sound the alert when we are viewing the application. We’ll use the usernotificationCenter:didRecieve response: a little later to code a stop action, but for now I added the required completion handler.

Go to ViewDidload to point the delegate to the delegate methods.

UNUserNotificationCenter.current().delegate = self

Notifications require user permission, so add this to viewDidLoad as well:

UNUserNotificationCenter.current().requestAuthorization(
    options: [.alert,.sound]) 
    {(granted, error) in
         self.isGrantedAccess = granted
    }

You’ll set the notification for alert and sound, and set a variable isGrantedAccess as a flag to decide if you have been granted access for notifications.

Of course you haven’t defined isGrantedAccess yet. Add it to the top of the class.

var isGrantedAccess = false

You’ll also need a category with a Stop button action for the notification. Add this to viewDidLoad

let stopAction = UNNotificationAction(identifier: "stop.action", title: "Stop", options: [])
        let timerCategory = UNNotificationCategory(identifier: "timer.category", actions: [stopAction], intentIdentifiers: [], options: [])
        UNUserNotificationCenter.current().setNotificationCategories([timerCategory])
        

This adds a stop action, which will display a stop button on the notification.

Making a Notification

We’ve done much of the setup, it’s time to make the notification. The secret to the notification is to make a time interval trigger of near zero, to launch an immediate notification. You’ll use a timer to send the notification.

Make a new function sendNotification. Add an if clause to check if we are granted access

func sendNotification(){
    if isGrantedAccess{
    }
}

In the if clause’s block, add some content:

let content = UNMutableNotificationContent()
content.title = "HIIT Timer"
content.body = "30 Seconds Elapsed"
content.sound = UNNotificationSound.default()
content.categoryIdentifier = "timer.category"

This will print a simple message and will play the default sound when 30 seconds is up. It also will use the timer.category to display buttons.

Next add the trigger. The strategy is to use a trigger at zero, but a timeInterval of zero is not allowed either. Use a time interval trigger set to a small interval such as 0.001, with no repeat to send one notification immediately.

let trigger = UNTimeIntervalNotificationTrigger(
    timeInterval: 0.001, repeats: false) //close to immediate as we can get. 

Finally make and add the notification request to the current user notification center. For error handling, I just print a message to the console.

        let request = UNNotificationRequest(identifier: "timer.request", content: content, trigger: trigger)
        UNUserNotificationCenter.current().add(request) { (error) in
            if let error = error{
                print("Error posting notification:\(error.localizedDescription)")
            }
        }

To test out this function, add it to the startButton method

@IBAction func startButton(_ sender: UIButton) {
        sendNotification()
    }

Build and Run. you’ll get the permissions alert:

2017-02-24_07-30-38

Tap Allow.  Hit the Start button and you immediately get a notification

2017-02-24_07-31-57

Repeat the Notification

I’ve gotten the notification to fire pretty close to immediately. Now I’ll add a timer to fire repeatedly every 10 seconds. Add to the top of the view controller class a timer

private var timer = Timer()

Code the following function to start a timer:

func startTimer(){
        let timeInterval = 10.0
        if isGrantedAccess && !timer.isValid { //allowed notification and timer off
            timer = Timer.scheduledTimer(withTimeInterval: timeInterval, repeats: true, block: { (timer) in
                self.sendNotification()
            })
        }
    }

For this code, I used the Timer initializer with a closure named block, instead of the one with a target. We have only one line of code to send the notification, to execute every time the timer completes an interval, so using a closure is more compact and easier to read than calling another function. I placed the timer’s start in a if clause to prevent the timer from starting if we don’t have permission for notifications and if the timer is already started. We don’t want to re-start the timer once started because that will mess up the intervals we are timing, and it is possible for someone working out to accidentally hit the Start button on their phone. I also set the time interval to a shorter ten seconds for testing purposes. I’ll move it back to 30  as shown in the notification when the app finishes testing.

Stopping the Notification

I’ll need to stop the notifications firing when I’m done with my workout. To do that, I’ll make another function to invalidate the timer. I’ll also clean out all the notifications

func stopTimer(){
    //shut down timer
    timer.invalidate()
    //clear out any pending and delivered notifications
    UNUserNotificationCenter.current().removeAllPendingNotificationRequests()
    UNUserNotificationCenter.current().removeAllDeliveredNotifications()
} 

There’s nothing useful about having a timer notifications floating around, so getting rid of them when you stop the timer makes things nice and tidy. There can be at most two here, because all pending notifications replace each other. Therefore you may have one pending notification, and very likely will have one delivered notification at most.

I’ll place the stopTimer method in two places. First, I’ll place it in the stopButton

@IBAction func stopButton(_ sender: UIButton) {
        stopTimer()
    }

Secondly, I’ll add it as the action for the stop.action on the notification. Drop down to the user notification center delegates at the bottom of your code and change the userNotificationCenter: didReceive response: to this:

func userNotificationCenter(_ center: UNUserNotificationCenter, 
    didReceive response: UNNotificationResponse, 
    withCompletionHandler completionHandler: @escaping () -> Void) {
        if response.actionIdentifier == "stop.action"{
            stopTimer()
        }
        completionHandler()
    }

Set your simulator device to an iPhone6. Phones later than this one use 3D touch in the simulator and you may not be able to get the action to appear otherwise with mice and older trackpads in the simulator. Build and run.

Unless you were using an iPhone 6 simulator earlier in this tutorial, the phone should boot and then ask permission again.

2017-02-24_09-34-15

Select Allow, and then Start. Notifications will start to appear every ten seconds.

2017-02-24_09-24-54

Press Stop The notifications stop. Press Start again and then Command-L to lock the phone. You’ll start to see notifications on the lock screen.

2017-02-24_09-35-36

Swipe toward the left and you’ll see two buttons

2017-02-24_09-35-54

Tap the View button.  You’ll see your notification and the action button. Tap Stop and the notifications stop.

2017-02-24_09-36-11

Bonus:The Apple Watch and Repeat Notifications

The Stop action button is something you should definitely put into your application. On a long run, it takes a while to unlock the phone and hit Stop in the app.  Opening the notification is faster. There is the other place this notification might show up: An Apple Watch

2017-02-24_09-49-41

Local phone notifications appear on an Apple watch paired to a phone that is locked or asleep. There may be a slight time delay, but they will show up. On a workout where you start your app, the phone will eventually lock during the workout.  The paired Apple watch will continue to get the notifications. Since we included sound as part of the content, that will mean the watch will include a haptic touch for the notification.

This method of repeat notification does have it limitations. It does run in the foreground and does use more processor time and power than the repeat notifications. You’ll need to set it up for background timing if you turn the app off or the device times out. If you do not have to do time intervals less than  60 seconds use the framework instead of this.

The Whole Code

You will find the complete project here: shortrepeat_end It includes some extra watchOS files and some graphics  to make the watch and project look a little nicer.

//
//  ViewController.swift
//  ShortRepeat
//
//  Created by Steven Lipton on 2/24/17.
//  Copyright © 2017 Steven Lipton. All rights reserved.
//

import UIKit
import UserNotifications

class ViewController: UIViewController,UNUserNotificationCenterDelegate {
    var isGrantedAccess = false
    private var timer = Timer()
    
    func startTimer(){
        let timeInterval = 30.0
        if isGrantedAccess && !timer.isValid { //allowed notification and timer off
            timer = Timer.scheduledTimer(withTimeInterval: timeInterval, repeats: true, block: { (timer) in
                self.sendNotification()
            })
        }
    }
    
    func stopTimer(){
        //shut down timer
        timer.invalidate()
        //clear out any pending and delivered notifications
        UNUserNotificationCenter.current().removeAllPendingNotificationRequests()
        UNUserNotificationCenter.current().removeAllDeliveredNotifications()
    }
    
    func sendNotification(){
        if isGrantedAccess{
            let content = UNMutableNotificationContent()
            content.title = "HIIT Timer"
            content.body = "30 Seconds Elapsed"
            content.sound = UNNotificationSound.default()
            content.categoryIdentifier = "timer.category"
        
            let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 0.001, repeats: false)
            let request = UNNotificationRequest(identifier: "timer.request", content: content, trigger: trigger)
            UNUserNotificationCenter.current().add(request) { (error) in
                if let error = error{
                    print("Error posting notification:\(error.localizedDescription)")
                }
            }
        }
    }
    
    
    @IBAction func startButton(_ sender: UIButton) {
        startTimer()
    }
    @IBAction func stopButton(_ sender: UIButton) {
        stopTimer()
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        UNUserNotificationCenter.current().delegate = self
        UNUserNotificationCenter.current().requestAuthorization(options: [.alert,.sound]) { (granted, error) in
                self.isGrantedAccess = granted
        }
        let stopAction = UNNotificationAction(identifier: "stop.action", title: "Stop", options: [])
        let timerCategory = UNNotificationCategory(identifier: "timer.category", actions: [stopAction], intentIdentifiers: [], options: [])
        UNUserNotificationCenter.current().setNotificationCategories([timerCategory])
        
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }
    
//MARK: - User Notification Center Delegates
    func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
        if response.actionIdentifier == "stop.action"{
            stopTimer()
        }
        completionHandler()
    }
    
    func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
        completionHandler([.alert,.sound])
    }

}


Introducing Core Motion: Make A Pedometer

Apple packed a lot of sensors into the small packages that are iOS devices. You as a developer can detect movement in three dimensions, acceleration, rotations, and impact and use those in your applications. All of this brought to your application in the CoreMotion framework.

CoreMotion has many uses. Game developers use it to make a phone into a gaming device where the phone’s motion controls the game play. Fitness developers use core motion to measure types of movements common to a sport on both the iPhone and Apple Watch. Using the sensors and Core motion could measure a swing of a baseball bat or golf club for example.  Basic movements can also let other application execute a function. The Apple watch wakes up to a raise of the wrist for example.

Core motion is a complex  framework to use, not only for coding reasons. As it deals with motion, a developer might need to know more about that motion before developing an application. The framework has some low-level objects for working directly with the sensors.  There are, however, some motions that are so universal, core motion combines several sensors to get more bottom-line data. One good example of this kind of motion is putting one foot in front of the other. For walking and running motions CoreMotion has the CMPedometer class. The class gives you direct data about the number of steps,   To give you a taste of some of the issues involved in developing for Core motion, let’s make a pedometer from the CMPedometer class. You’ll need to hook up a phone to Xcode since the simulator does not work with Core Motion.

Setting Up the Project

Create a new single-view project named CoreMotionPedometer. Use Swift as a language and a iPhone for a device.  Save the project.

When the applications loads, change the Display Name to CMPedometer

2017-02-10_06-03-10

This will give us a smaller name for the Icon. In this application you may need to test off of Xcode, and this will help you find your app better.

Core motion in general and the pedometer more specifically uses several motions that Apple considers private data. On a pedometer, you’re also using location data, so privacy is an even bigger issue. Developers need to add a key NSMotionUsageDescription to the info.plist to ask permission of the user. You can add the key in XML like this:

<key>NSMotionUsageDescription</key>
<string>This is a step counter, and we&apos;d like to track motion. </string>

Most people will just go to the info.plist and add a entry to the dictionary.
2017-02-10_06-11-09

In the drop down, select Privacy – Motion Usage Descriptor
2017-02-10_06-10-45

This creates a key with a String value. The string will be the body of an alert to accept the privacy settings for the pedometer. Add This is a step counter, and we’d like to track motion.
2017-02-10_06-17-55

Designing the Storyboard

Go to the main storyboard. Add Five labels and a button. Place the button towards the bottom of the scene and the labels towards the top. Make the labels white, the button a green background and the scene background to black like this:

2017-02-10_06-26-20

Layout the button on the bottom of the phone a quarter of the height of the device. Change the tile color for the Start button to White, and the title font to System Black 28pt. Using the auto layout pin tool pinMenuButton, turn off the Constrain to Margins  and set the left to 0, bottom to 0 and right to 0.

2017-02-10_06-31-35

Add the three constraints. Control drag from the green of the button up to the black of the background.

2017-02-10_06-39-09

When you release the mouse button, select Equal Heights in the menu that appears

2017-02-10_06-37-13

The start button will fill the screen.  Go to the size inspector. Find the Equal Height to Superview constraint, and click the Edit button there.

2017-02-10_06-41-07

In the edit dialog that appears change the Multiplier to 1:4.

2017-02-10_06-45-07

You’ll have a nice big button on the bottom.

2017-02-10_06-48-00

I like having a big start/stop button. If you are doing a workout, it may be difficult to hit a small button, and this helps the user hit the button. It’s on the bottom in the thumb zone so the user can hit it one handed.

Select the Pedometer Demo label. In the attribute inspector, set the font to 22pt System heavy.  Using the auto layout pin tool pinMenuButton, turn  Using the auto layout pin tool pinMenuButton, turn on the Constrain to Margins  and set the Top to 20,  Left to 0 and Right to 0. Add the constraints.

Select the other four labels.  Set the font to System Heavy 17pt, Lines to 2 and Line Break to Word Wrap

2017-02-10_06-53-00

You’ll be using two lines of text in this demo, one for a metric measurement and one for an Imperial measurement used in most sporting events.  You’ll place them on two lines in the same label.

With all four labels selected, select the Stackview tool  stack view button from the autolayout toolbar in the lower right. The labels will embed in a vertical stack view.  Change the stack view to these attributes:

2017-02-10_07-02-31

Use the pin tool to pin all four sides of the stack view 10 points. Update the constraints.  You’ll have a storyboard like this one.

2017-02-10_07-04-04

Close the attributes inspector and open the assistant editor. Control drag from the labels and make the following outlets, with the statusTitle outlet to the Pedometer Demo label and the rest to their respective labels :

@IBOutlet weak var statusTitle: UILabel!
@IBOutlet weak var stepsLabel: UILabel!
@IBOutlet weak var avgPaceLabel: UILabel!
@IBOutlet weak var paceLabel: UILabel!
@IBOutlet weak var distanceLabel: UILabel!

Now add at the action. Control-drag from the button and add an action

@IBAction func startStopButton(_ sender: UIButton) {
}

Some Preliminary Coding

Before we code the pedometer, I’m going to add a few properties and functions that will help us later. Close the assistant editor and go to the ViewController.swift file.

A very critical line goes just under the import UIKit line, where we import core motion.

import CoreMotion

Then add these properties and constants.

    
    let stopColor = UIColor(red: 1.0, green: 0.0, blue: 0.0, alpha: 1.0)
    let startColor = UIColor(red: 0.0, green: 0.75, blue: 0.0, alpha: 1.0)
    // values for the pedometer data
    var numberOfSteps:Int! = nil
    var distance:Double! = nil
    var averagePace:Double! = nil
    var pace:Double! = nil

The constants will change the color of the stat stop button. You’ll store values in these for the properties of the pedometer object. Of course you need a pedometer object. Add this after these properties:

var pedometer = CMPedometer()

Pedometers don’t compute elapsed time. You might want that too in your pedometer. Add these two more properties to use a timer. :

    // timers
    var timer = Timer()
    let timerInterval = 1.0
    var timeElapsed:TimeInterval = 0.0

As you’ll find out, there’s more to this timer than just recording time.

You’ll need some more  methods to handle some unit conversions you’ll do later, so add these:

   //MARK: - Display and time format functions
    
    // convert seconds to hh:mm:ss as a string
    func timeIntervalFormat(interval:TimeInterval)-> String{
        var seconds = Int(interval + 0.5) //round up seconds
        let hours = seconds / 3600
        let minutes = (seconds / 60) % 60
        seconds = seconds % 60
        return String(format:"%02i:%02i:%02i",hours,minutes,seconds)
    }
    // convert a pace in meters per second to a string with
    // the metric m/s and the Imperial minutes per mile
    func paceString(title:String,pace:Double) -> String{
        var minPerMile = 0.0
        let factor = 26.8224 //conversion factor
        if pace != 0 {
            minPerMile = factor / pace
        }
        let minutes = Int(minPerMile)
        let seconds = Int(minPerMile * 60) % 60
        return String(format: "%@: %02.2f m/s \n\t\t %02i:%02i min/mi",title,pace,minutes,seconds)
    }
    
func computedAvgPace()-> Double {
    if let distance = self.distance{
        pace = distance / timeElapsed
        return pace
    } else {
        return 0.0
    }
}

func miles(meters:Double)-> Double{
        let mile = 0.000621371192
        return meters * mile
    }

The pedometer works in Metric, returning meters and meters per second. If you want other units such as a distance in miles, or a pace in minute per mile, you’ll need these to calculate the values. I convert times to strings in hh:mm:ss here too.

Coding the Pedometer

With all these functions in place, your’e ready to code the pedometer. You’ll use the startStopButton‘s action to toggle between starting and stopping the pedometer. Add this to the startStopButton code

if sender.titleLabel?.text == "Start"{
    //Start the pedometer
   //Toggle the UI to on state
   statusTitle.text = "Pedometer On"
   sender.setTitle("Stop", for: .normal)
   sender.backgroundColor = stopColor
} else {
   //Stop the pedometer
   //Toggle the UI to off state
   statusTitle.text = "Pedometer Off: "
   sender.backgroundColor = startColor
   sender.setTitle("Start", for: .normal)
}

You have button that toggles and appears correctly on the user interface. Below the Start the pedometer comment add this:

pedometer = CMPedometer()
pedometer.startUpdates(from: Date(), withHandler: { (pedometerData, error) in
    if let pedData = pedometerData{
            self.stepsLabel.text = "Steps:\(pedData.numberOfSteps)"
    } else {
            self.stepsLabel.text = "Steps: Not Available"
    }
})

The first line of this code clears the pedometer by creating a new instance in the pedometer property. The startUpdates:FromDate:withHandler: method starts sending updates from the pedometer. When an update occurs the handler within the closure executes. It checks of the pedometer is nil. If nil, the pedometer is not available. There are several other ways of checking this, but this is the most direct. If there is a value, the pedometer property number of steps is sent to the stepsLabel‘s text property.

To stop a pedometer, add the following under Stop the pedometer comment.

pedometer.stopUpdates()

Run the Application

You are ready to run our first iteration. However, core motion only works on a real device, not the simulator. Plug your phone into a device, and select the device in the run schemes. I’ll assume you know how to do this and how to set up your device for testing. Run the application.
You’ll get a screen like this:

052fa7ca-aa18-46fd-9127-06023afccf80

Press Start. You’ll get a screen asking for permission. Tap OK

112f8982-77fb-40af-b2ef-35842bc3e2f4

Pick up your phone. Move your arms back and forth for 30 seconds like you are jogging. This is one of the motions that the pedometer uses to count steps. Press STOP.

0a9e7fae-7abc-4372-b9b4-311ac9d13e9f

While you see a few steps on the pedometer, you also see a lot of errors in the console Most of them you can ignore — they are errors related to internal caches you can’t touch. The very last error on the other hand is very important:
This application is modifying the autolayout engine from a background thread after the engine was accessed from the main thread. This can lead to engine corruption and weird crashes.

What’s happening here? Remember the code for updating is a closure, and runs on a separate thread from the main thread where UI updates happen. The closure tries to access the main thread when it really shouldn’t, because its timing is not the same as the main thread. This can crash the app. While this specifically mentions auto layout, I’d suggest never directly changing outlets from the closure to avoid any bad behavior.

Using Timer Loops

Instead of setting the labels in the closure, you set a property. Change this:

self.stepsLabel.text = "Steps:\(pedData.numberOfSteps)"

to this:

self.numberOfSteps = Int(pedData.numberOfSteps)

The pedometer numberOfSteps property is of type NSNumber. You must covert it to an Int to use in the ViewController numberOfSteps property.
You might think you can use a property observer to change the display. For example change the numberOfSteps property to this:

// values for the pedometer data
var numberOfSteps:Int! = nil{
    didSet{
        stepsLabel.text = "Steps:\(numberOfSteps)"
    }
}

When the property changes, the label changes. You can run this code and do a simulated jog with your device. IF you do, you get that error message again. In code this is still updating the pedometer in the handler thread. You need a thread that has no problem updating to the view. That’s a timer.

Comment out the property observer:

// values for the pedometer data
var numberOfSteps:Int! = nil
/*{  //this does not work. 
    didSet{
        stepsLabel.text = "Steps:\(numberOfSteps)"
    }
}*/

We earlier declared some timer properties. You’ll use that to set up a timer with these functions:

    //MARK: - timer functions
        func startTimer(){
        if timer.isValid { timer.invalidate() }
        timer = Timer.scheduledTimer(timeInterval: timerInterval,target: self,selector: #selector(timerAction(timer:)) ,userInfo: nil,repeats: true)
    }
    
    func stopTimer(){
        timer.invalidate()
        displayPedometerData()
    }
    
    func timerAction(timer:Timer){
        displayPedometerData()
    }

I discuss timers in more detail here. Basically startTimer starts a timer with a 1 second interval that repeats. I’m being course here, you can set the interval to a finer resolution if you wish. Every second it calls the function timerAction from the selector. In timerAction I call a function displayPedometerData I’ve yet to define which will display my pedometer data. The stopTimer function shuts down the timer and updates the display one last time. Add the startTimer and stopTimer functions to the button action so it starts and stop the timer when the pedometer starts and stops.

@IBAction func startStopButton(_ sender: UIButton) {
        if sender.titleLabel?.text == "Start"{
            //Start the pedometer
            pedometer = CMPedometer()
            startTimer() //start the timer
            pedometer.startUpdates(from: Date(), withHandler: { (pedometerData, error) in
                if let pedData = pedometerData{
                    self.numberOfSteps = Int(pedData.numberOfSteps)
                    //self.stepsLabel.text = "Steps:\(pedData.numberOfSteps)"
                } else {
                    self.stepsLabel.text = "Steps: Not Available"
                }
            })
            //Toggle the UI to on state
            statusTitle.text = "Pedometer On"
            sender.setTitle("Stop", for: .normal)
            sender.backgroundColor = stopColor
        } else {
            //Stop the pedometer
            pedometer.stopUpdates()
            stopTimer() // stop the timer
            //Toggle the UI to off state
            statusTitle.text = "Pedometer Off: "
            sender.backgroundColor = startColor
            sender.setTitle("Start", for: .normal)
        }
    }

Create a new function to update the view. For now we’ll update just the steps again.

func displayPedometerData(){  
    //Number of steps
    if let numberOfSteps = self.numberOfSteps{
        stepsLabel.text = String(format:"Steps: %i",numberOfSteps)
     }
}

I did two more things than I did with the property observer. I used an optional chain to unwrap numberOfSteps, and then used as String initializer to format the string.
If you run the application and do your little in-place run, you’ll notice two differences: the step count updates faster than before and the error message disappears. We only have that CoreLocation cache warning on the console. The timer thread indirectly updated the display separating the pedometer thread from the main thread.

Adding Elapsed Time

One advantage to a timer loop is we have a timer. Usually I use a higher resolution timer(0.1 seconds for example), but for this lesson I’ll leave it at a one second interval.

I can change the displayPedometerData function to this:

func displayPedometerData(){
    //Time Elapsed
    timeElapsed += self.timerInterval
    statusTitle.text = "On: " + timeIntervalFormat(interval: timeElapsed)
        //Number of steps
        if let numberOfSteps = self.numberOfSteps{
            stepsLabel.text = String(format:"Steps: %i",numberOfSteps)
        }
    }

I increment the pedometer with a property timeElapsed I created earlier. It keeps a count of the number of seconds elapsed since I started the pedometer. I display it using one of the formatting functions we added earlier that displays the time as hh:mm:ss.

To keep this time after you stop the timer, append the timeIntervalFormat function to the status title label

statusTitle.text = "Pedometer Off: " + timeIntervalFormat(interval: timeElapsed)

Build and run. Start the pedometer. You’ll get both a timer and step count now.

9872d97b-a238-4012-8597-a74b3732a053

Stop the pedometer.

Adding Other Pedometer Properties

There’s several other properties of pedometers. I selected three more to show on our pedometer: current pace, Average pace and distance. Why you are getting that core location cache message makes sense now: the Pedometer checks your location repeatedly using CoreLocation. You have no control over that which is why I said to ignore the warning message. With that location data, the pedometer computes distance, and from the steps, distance, and it’s own timer pace and average pace.

All of these properties are optional. If the hardware or property is unavailable or nonexistent, the property returns nil. However if you can’t get pace from a pedometer, you can compute the average pace from the distance and time. I made a function earlier computedAvgPace that will compute an average pace or leave it as 0 if core location is not available.

To implement the other properties, change the startUpdates closure to add the pedometer data to the viewController’s properties:

  pedometer.startUpdates(from: Date(), withHandler: { (pedometerData, error) in
                if let pedData = pedometerData{
                    self.numberOfSteps = Int(pedData.numberOfSteps)
                    //self.stepsLabel.text = "Steps:\(pedData.numberOfSteps)"
                    if let distance = pedData.distance{
                        self.distance = Double(distance)
                    }
                    if let averageActivePace = pedData.averageActivePace {
                        self.averagePace = Double(averageActivePace)
                    }
                    if let currentPace = pedData.currentPace {
                        self.pace = Double(currentPace)
                    }
                } else {
                    self.numberOfSteps = nil
                }
            })

Each pedometer property, if a number, is converted from NSNumber! to a Double for use in the classes, like we did for the integer numberOfSteps.

In the displayPedometerData function, change it to this to include the other properties:

 func displayPedometerData(){
        timeElapsed += 1.0
        statusTitle.text = "On: " + timeIntervalFormat(interval: timeElapsed)
        //Number of steps
        if let numberOfSteps = self.numberOfSteps{
            stepsLabel.text = String(format:"Steps: %i",numberOfSteps)
        }
        
        //distance
        if let distance = self.distance{
            distanceLabel.text = String(format:"Distance: %02.02f meters,\n %02.02f mi",distance,miles(meters: distance))
        } else {
            distanceLabel.text = "Distance: N/A"
        }
        
        //average pace
        if let averagePace = self.averagePace{
            avgPaceLabel.text = paceString(title: "Avg Pace", pace: averagePace)
        } else {
            avgPaceLabel.text =  paceString(title: "Avg Comp Pace", pace: computedAvgPace())
        } 
        
        //pace
        if let pace = self.pace {
            paceLabel.text = paceString(title: "Pace:", pace: pace)
        } else {
            paceLabel.text =  paceString(title: "Avg Comp Pace", pace: computedAvgPace())
        }
    }

For each of these properties we optionally chain the property. A nil vaule shows there was no reading for some reason. For the two pace strings, if we do not get a pace, I calculate the pace from the distance. I use the paceString function defined in the conversion functions to make a string of both meters per second and minutes per mile.

Run again, and start the pedometer. Make the running motion and the device will begin to display data.

6bbd7e99-1d47-4fb3-a8f1-cd886ebfc705

The Main Points for Core Motion

Core motion has a lot more to it, but this introduction give you some of the basics all core motion methods use. THis one is high level, the lower level, closer to the sensors require more tweaking than this virtually automatic method. However there are several points you should remember with Core Motion:

  • Add the entry to the info.plist for security permissions
  • Include the Core Motion LIbrary
  • Check for availability of the device and the functions by looking for nil on properties.
  • Don’t directly update outlets from a core motion closure
  • Do indirectly update outlets from a timer loop.

The Whole Code

You’ll find all but the info.plist entry in this code. if you run this, make sure to include that. There is a download file  coremotionpedometer with the completed project, including some app icons if you want to test off of Xcode. Go ahead, run a mile. It’s good for you.

ViewController.swift

//
//  ViewController.swift
//  CoreMotionPedometer
//
//  Created by Steven Lipton on 2/10/17.
//  Copyright © 2017 Steven Lipton. All rights reserved.
//

import UIKit
import CoreMotion

class ViewController: UIViewController {

//MARK: - Properties and Constants
    let stopColor = UIColor(red: 1.0, green: 0.0, blue: 0.0, alpha: 1.0)
    let startColor = UIColor(red: 0.0, green: 0.75, blue: 0.0, alpha: 1.0)
    // values for the pedometer data
    var numberOfSteps:Int! = nil
    /*{ //this does not work. for demo purposes only.
        didSet{
            stepsLabel.text = "Steps:\(numberOfSteps)"
        }
    }*/
    var distance:Double! = nil
    var averagePace:Double! = nil
    var pace:Double! = nil
    
    //the pedometer
    var pedometer = CMPedometer()
    
    // timers
    var timer = Timer()
    var timerInterval = 1.0
    var timeElapsed:TimeInterval = 1.0
    

    
//MARK: - Outlets
    
    @IBOutlet weak var statusTitle: UILabel!
    @IBOutlet weak var stepsLabel: UILabel!
    @IBOutlet weak var avgPaceLabel: UILabel!
    @IBOutlet weak var paceLabel: UILabel!
    @IBOutlet weak var distanceLabel: UILabel!
    
    @IBAction func startStopButton(_ sender: UIButton) {
        if sender.titleLabel?.text == "Start"{
            //Start the pedometer
            pedometer = CMPedometer()
            startTimer()
            pedometer.startUpdates(from: Date(), withHandler: { (pedometerData, error) in
                if let pedData = pedometerData{
                    self.numberOfSteps = Int(pedData.numberOfSteps)
                    //self.stepsLabel.text = "Steps:\(pedData.numberOfSteps)"
                    if let distance = pedData.distance{
                        self.distance = Double(distance)
                    }
                    if let averageActivePace = pedData.averageActivePace {
                        self.averagePace = Double(averageActivePace)
                    }
                    if let currentPace = pedData.currentPace {
                        self.pace = Double(currentPace)
                    }
                } else {
                    self.numberOfSteps = nil
                }
            })
            //Toggle the UI to on state
            statusTitle.text = "Pedometer On"
            sender.setTitle("Stop", for: .normal)
            sender.backgroundColor = stopColor
        } else {
            //Stop the pedometer
            pedometer.stopUpdates()
            stopTimer()
            //Toggle the UI to off state
            statusTitle.text = "Pedometer Off: " + timeIntervalFormat(interval: timeElapsed)
            sender.backgroundColor = startColor
            sender.setTitle("Start", for: .normal)
        }
    }
    //MARK: - timer functions
    func startTimer(){
        if timer.isValid { timer.invalidate() }
        timer = Timer.scheduledTimer(timeInterval: timerInterval,target: self,selector: #selector(timerAction(timer:)) ,userInfo: nil,repeats: true)
    }
    
    func stopTimer(){
        timer.invalidate()
        displayPedometerData()
    }
    
    func timerAction(timer:Timer){
        displayPedometerData()
    }
    // display the updated data
    func displayPedometerData(){
        timeElapsed += 1.0
        statusTitle.text = "On: " + timeIntervalFormat(interval: timeElapsed)
        //Number of steps
        if let numberOfSteps = self.numberOfSteps{
            stepsLabel.text = String(format:"Steps: %i",numberOfSteps)
        }
        
        //distance
        if let distance = self.distance{
            distanceLabel.text = String(format:"Distance: %02.02f meters,\n %02.02f mi",distance,miles(meters: distance))
        } else {
            distanceLabel.text = "Distance: N/A"
        }
        
        //average pace
        if let averagePace = self.averagePace{
            avgPaceLabel.text = paceString(title: "Avg Pace", pace: averagePace)
        } else {
            avgPaceLabel.text =  paceString(title: "Avg Comp Pace", pace: computedAvgPace())
        }
        
        //pace
        if let pace = self.pace {
            print(pace)
            paceLabel.text = paceString(title: "Pace:", pace: pace)
        } else {
            paceLabel.text = "Pace: N/A "
            paceLabel.text =  paceString(title: "Avg Comp Pace", pace: computedAvgPace())
        }
    }
    
    //MARK: - Display and time format functions
    
    // convert seconds to hh:mm:ss as a string
    func timeIntervalFormat(interval:TimeInterval)-> String{
        var seconds = Int(interval + 0.5) //round up seconds
        let hours = seconds / 3600
        let minutes = (seconds / 60) % 60
        seconds = seconds % 60
        return String(format:"%02i:%02i:%02i",hours,minutes,seconds)
    }
    // convert a pace in meters per second to a string with
    // the metric m/s and the Imperial minutes per mile
    func paceString(title:String,pace:Double) -> String{
        var minPerMile = 0.0
        let factor = 26.8224 //conversion factor
        if pace != 0 {
            minPerMile = factor / pace
        }
        let minutes = Int(minPerMile)
        let seconds = Int(minPerMile * 60) % 60
        return String(format: "%@: %02.2f m/s \n\t\t %02i:%02i min/mi",title,pace,minutes,seconds)
    }
    
    func computedAvgPace()-> Double {
        if let distance = self.distance{
            pace = distance / timeElapsed
            return pace
        } else {
            return 0.0
        }
    }
    
    func miles(meters:Double)-> Double{
        let mile = 0.000621371192
        return meters * mile
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }


}


How to Repeat Local Notifications

I’ve done several past posts about local and remote notifications using the new UserNotifications framework. One of the new features of the new framework is tiny, but  has a big impact: repeat notifications. While you used to need timer loops to make notifications they become insanely easy in most cases to get a notification to repeat. I also glossed over calendar triggers in other lessons. In this lesson, I’ll show you how to make a repeating calendar based trigger, and show you that while making a trigger might be easy, stopping one is not so simple.

Along the way I’ll give you a few more tips about local notifications.

Make the Project

Unlike my earlier notification projects, I’m going to start from scratch on this one.  Make a new project called RepeatNoificationDemo with Swift for the Language and a Universal device.  On the storyboard add two buttons and a label like this:

2017-01-29_09-00-06

In the assistant editor make two actions from the buttons: add notification and showNotificationStatus.

At the top of the viewController.swift code, just under Include UIKit add this:

include UserNotifications

Check for Permission

With any notification app you must check for permission. This must be one of the first things you do in an application. There’s two places you can check this permission: one is in the App delegate, the other in the root or initial view controller. The earlier you do this the better. With a remote notification, place this in the app delegate. For a local notification, place this in the root view controller. There’s a reason for this: For a local notification the system will think the user did not grant permission on the first run of the application. the AppDelegate and View controller don’t run sequentially. The app delegate will get one answer and the view controller will get another, confusing everything. For local notifications avoid this and place the permission in viewDidLoad like this:

override func viewDidLoad() {
    super.viewDidLoad()
    UNUserNotificationCenter.current().requestAuthorization(
        options: [.alert,.sound]) 
        { 
            (granted, error) in
            self.notificationGranted = granted
            if let error = error {
                print("granted, but Error in notification permission:\(error.localizedDescription)")
        }
}  

You’ll need notificationGranted as a property of the viewController class, so add this:

var notificationGranted = false

Make the Notification Content

I’ll make a function to add the notification to the notification center. Above viewDidLoad add at the following function:

func repeatNotification(){
}

To the function add the following content.

let content = UNMutableNotificationContent()
content.title = "Pizza Time!!"
content.body = "Monday is Pizza Day"
content.categoryIdentifier = "pizza.reminder.category"

This gives us a notification with a title Pizza Time!! and the message Monday is Pizza Day

How to Easily Repeat a Notification

Repeating a notification is easy. You can repeat either a TimeInterval or a Calendar Notification. Both have a Bool parameter repeats. If you set this to true, you get a repeating notification. However the repetitions must be 60 seconds or more apart. This is the smallest time interval repeating trigger:

let  trigger = UNTimeIntervalNotificationTrigger(
    timeInterval: 60.0,
    repeats: true)

I’m not going to add a time interval to the demo. Instead I’ll add a calendar notification. Calendar notifications use DateComponents objects to specify the date. For a repeating notification, add the components you need and nothing else. for example to set data components to Monday at 11:30 AM add this

var dateComponents = DateComponents()
// a more realistic example for Gregorian calendar. Every Monday at 11:30AM
dateComponents.hour = 11
dateComponents.minute = 30
dateComponents.weekday = 2
ddateComponents.second = 0

DateComponents are just numbers, dependent on the calendar they are associated with. weekday in this example may be Monday or Tuesday depending if Sunday is the first day of the week in the calendar. I’m using a U.S. Gregorian calendar so it is Monday. While this is how you set up a calendar event, this is difficult to test or demo. So comment out the hour, minute and weekday to run the example at the top of the minute.

//Date component trigger
var dateComponents = DateComponents()
// a more realistic example for Gregorian calendar. Every Monday at 11:30AM
//dateComponents.hour = 11
//dateComponents.minute = 30
//dateComponents.weekday = 2
// for testing, notification at the top of the minute.
dateComponents.second = 0

Make a repeating calendar trigger like this:

let trigger = UNCalendarNotificationTrigger(
    dateMatching: dateComponents, 
    repeats: true)

Add the Request

Add the request like any other local notification. Add the following code:

        let request = UNNotificationRequest(identifier: "pizza.reminder", content: content, trigger: trigger)
        
        UNUserNotificationCenter.current().add(request) { (error) in
            if let error = error {
                print("error in pizza reminder: \(error.localizedDescription)")
            }
        }
        print("added notification:\(request.identifier)")
 

For explanation see the local notification lesson I posted earlier.

Now you can use the function to schedule the notification. Modify the addNotification action to this to check for user permission. If so, add the notification:

 @IBAction func addNotification(_ sender: UIButton) {
        if notificationGranted{
            repeatNotification()
        }else{
            print("notification not granted")
        }
        
    }

Adding the In-App Presentation Delegate

You are ready to run the code to make the notification, but I like to add a few more things thing to the application before I do: The UNUserNotificationCenterDelegate. Once again, you can add it to the root view controller or the app delegate. For this, I’ll habitually add it to the app delegate, because it should be run as soon as possible. Go to the appDelegate.swift file. Adopt the protocol:

class AppDelegate: UIResponder, UIApplicationDelegate,UNUserNotificationCenterDelegate {

Change the application:didFinishLaunching to point to the delegate methods:

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
    UNUserNotificationCenter.current().delegate = self
    return true
 }

Add the following delegate method at the bottom of the class

    //MARK: - Delegates
    func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
        completionHandler([.alert,.sound])
        
    }

This delegate can do a lot, but its core is the completion handler. You specify how you want notifications to appear if you are in the app generating them. The code completionHandler([.alert,.sound]) shows a notification and gives a sound if in the app. Usually this is the current default behavior, so if you don’t use this delegate method at all, you’ll get this behavior. If you add it and don’t set completionHandler, or set completionHandler to empty options, you suppress the notification in-app.

Checking pending notifications

As I discussed in more detail in another post, The UserNotifications framework can monitor pending notifications, the notifications waiting to appear. Theres a UNUserNotificationCenter method to check this. The getPendingNotificationRequests method uses a closure to handle the collection of requests it returns. To list them, iterate through them with a for loop.

@IBAction func showNotificationStatus(_ sender: UIButton) {
    var displayString = "Current Pending Notifications "
    UNUserNotificationCenter.current().getPendingNotificationRequests {
    (requests) in
        displayString += "count:\(requests.count)\t"
        for request in requests{
            displayString += request.identifier + "\t"
         }
         print(displayString)
}

This prints a single line in the console to tell you how many notifications are pending, and what they are by their request identifier specified in the UNNotificationRequest object.

Stopping a Notification

I’d be tempted to run right now, but there’s one little problem: Stopping the notification. The code to stop a pending notification is simple. The UNUserNotificationCenter object does have a removePendingNotificationRequests method. If I were to use it literally it would looks like this:

UNUserNotificationCenter.removePendingNotificationRequests(withIdentifiers: ["pizza.reminder"])

That’s all you need. You specify the identifier of the pending notification. If there, the center removes the pending notification. However there’s two problems with this: When do you remove the repeating notification and where in code do you do this?

A Stop Action

One answer to both of these questions is when a user says so. With repeating notifications, this is always a good idea, so your notification never gets annoying, and the user dumps the app or shuts off notifications. You can code an action to stop repeating. I tend to code my actions in the AppDelegate. Go to the AppDelegate.swift file.
Add a function setCategories in the AppDelegate class

func setCategories(){
}

Actions in notifications are pointers to some code that runs later in a delegate. You store a collection of actions in a category, and add the categories to the notification center. You’ll see above we set in the content of the notification a categoryIndentifier. When the notification presents, the notification center checks this identifier against the categories it has listed. If it exists, it turns the actions in the category into buttons on the notification.
In the function make the action using the UNNotificationAction constructor.

let clearRepeatAction = UNNotificationAction(
   identifier: "clear.repeat.action",
   title: "Stop Repeat",
   options: [])

This has three parameters: the identifier of the action, a title for the button and some options we’ll ignore here. You’ll come back to this identifier shortly.Under that, add the category, adding the action you just defined as part of the category.

let pizzaCategory = UNNotificationCategory(
    identifier: "pizza.reminder.category",
    actions: [clearRepeatAction],
    intentIdentifiers: [],
    options: [])
}

The category constructor also contains an indentifier, the one that must match some categoryIdentifier in a notification’s content. The actions parameter can  list up to four actions in an array. The intent identifiers has to do with Siri, and again we’ll skip it and the options.
Finally add the category to the notification center.

UNUserNotificationCenter.current().setNotificationCategories([pizzaCategory])

The function should look like this:

func setCategories(){
    let clearRepeatAction = UNNotificationAction(
        identifier: "clear.repeat.action",
        title: "Stop Repeat",
        options: [])
    let pizzaCategory = UNNotificationCategory(
        identifier: "pizza.reminder.category",
        actions: [clearRepeatAction],
        intentIdentifiers: [],
        options: [])
    UNUserNotificationCenter.current().setNotificationCategories([pizzaCategory])
}

Then add setCategories to the application:didFinishLaunchingWithOptions method.

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
        // Override point for customization after application launch.
        UNUserNotificationCenter.current().delegate = self
        setCategories()
        return true
    }

That defines the action but does nothing. Go to the bottom of the code. You’ll add the second UNUserNotificationCenterDelegate method under the first one for in-app notifications.

func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
        print("Did recieve response: \(response.actionIdentifier)")
        if response.actionIdentifier == "clear.repeat.action"{
            UNUserNotificationCenter.current().removePendingNotificationRequests(withIdentifiers: [response.notification.request.identifier])
        }
        completionHandler()
    }

This method fires when a user selects some action in a notification. The UNNotificationResponse object response contains the actionIdentifier and the UNNotification object for the user interaction. This code is for any notification that uses this action. You are not restricted to using one action to one notification. It checks if the actionIdentifier is the correct one for removing the pending notification. If it is, the center removes it, using the request. At the end of this method you must call the completionHandler.

You can build and run now. You’ll first be asked if the user grants permission to use notifications:

2017-01-30_08-15-22

Then you have the the main screen. Add a notification.

2017-01-30_08-09-26

At the top of the minute, you’ll get the notification:

2017-01-30_08-10-19

You can wait a few minutes and see the notification repeat. You can tap the Notification Status button and see the pending notification:

added notification:pizza.reminder
Current Pending Notifications count:1 pizza.reminder

When you are done, Swipe down on the notification to view the full notification:

2017-01-30_08-13-11

Tapping the Stop Repeat button stops the notification.

Using userInfo

Besides the user removing the repeat notification, when does your code remove the pending notification automatically? You’ll need some time when you end the notifications. You’ll store that somewhere, compare it to the current date, and if the current date is past the end date, remove the notification. It’s best to store the end date in the notification itself. For this you’ll use another content property of UNUserNotificationContent. The userInfo property is a dictionary where you store whatever you want. It may be extra content, or control values for the notification. for example, I could store a date 90 days into the future like this:

// Set the end date to a number of days
let dayInSeconds = 86400.0
content.userInfo["endDate"] = Date(timeIntervalSinceNow: dayInSeconds * 90.0)       

This uses a constant of the number of seconds in a day, and multiplies it by 90 days. Using the Date:TimeIntervalSinceNow method, we add a date 90 days from now to a new dictionary entry endDate.
Again that’s hard to test, so I’ll set this for a minute from the time the notification is created. Add this to your repeatNotification function under the content.categoryIdentifier line.

content.userInfo["endDate"] = Date(timeIntervalSinceNow: 60.00)

You’ve stored the notification’s end date in the notification. Now you need to compare it to the current date.

An Extension to Clean Repeating Notifications

As you’ll see there some issues about where in the code we should try killing the notification. For that reason, I’ll create a new function, extending the UNNotificationCenter class so I can use this function anywhere in my code.

You might not have used an extension before, so it needs a small amount of explanation what I’m about to do. Swift contains a structure called an extension which tacks on more properties and methods to already existing classes, especially factory ones. Go to the ViewController.swift file just below the import UserNotifications line. Add the following:

extension UNUserNotificationCenter{

}

This adds an extension to the UNNotification center. You add methods to this extension, and then use the method as if it is in the class you extended. Add the following method to the extension:

extension UNUserNotificationCenter{
    func cleanRepeatingNotifications(){
    }
}

This will add the function cleanRepeatingNotifications to the UNUserNotificationCenter. In this function we’ll get the pending notifications, look at them and see if any have and expired date. If they do, we’ll remove them form pending notifications. First you get the list of pending notifications in a array called requests.

extension UNUserNotificationCenter{
    func cleanRepeatingNotifications(){
        //cleans notification with a userinfo key endDate
        //which have expired.
        var cleanStatus = "Cleaning...."
        getPendingNotificationRequests {
            (requests) in
       }
    }
}

The array exists in a closure. Add code in the closure to iterate through the requests, cleaning as you go.

for request in requests{
    if let endDate = request.content.userInfo["endDate"]{
         if Date() >= (endDate as! Date){
              cleanStatus += "Cleaned request"
              let center = UNUserNotificationCenter.current()
              center.removePendingNotificationRequests(
                   withIdentifiers: [request.identifier])          
         } else {
              cleanStatus += "No Cleaning"
         }
         print(cleanStatus)
     }
}

I want to point out two lines in this code.

    if let endDate = request.content.userInfo["endDate"]{
         if Date() >= (endDate as! Date){

This is how we get to the user dictionary. I used if let optional chaining so if there is an endDate entry, the function checks if it needs to clean, otherwise it skips the process. if it does need to clean, userInfo‘s value is of type Any, so I downcast it to Date in the comparison afterwards.
The complete extension looks like this:

extension UNUserNotificationCenter{
    func cleanRepeatingNotifications(){
        //cleans notification with a userinfo key endDate
        //which have expired.
        var cleanStatus = "Cleaning...."
        getPendingNotificationRequests {
            (requests) in
            for request in requests{
                if let endDate = request.content.userInfo["endDate"]{
                    if Date() >= (endDate as! Date){
                        cleanStatus += "Cleaned request"
                        let center = UNUserNotificationCenter.current()
                        center.removePendingNotificationRequests(
                             withIdentifiers: [request.identifier])
                    } else {
                        cleanStatus += "No Cleaning"
                    }
                    print(cleanStatus)
                }
            }
        }
    }    
}

Where to Put cleanRepeatingNotifications

Here’s a big question that makes repeat notifications so difficult: where do you put cleanRepeatingNotifications? Ideally, you would would run it every time the notification gets delivered. But there’s no way to write code there — at least of this writing. Push notifications have the service extension to do that, but local notifications don’t have that mechanism. Of course you don’t need any of this for a push notification because the repetition and termination of the repetition is set on the server, not your device.
Since we have no background place to call it, You have to call it in a few places that users interact with the application, and hope they hit one of those. In each you’d add the line

UNUserNotificationCenter.current().cleanRepeatingNotifications()

I haven’t yet found a good place that works 100%. A user can keep getting these messages if they never interact with the app or the notification. I’d suggest the following places to add this code:

  • On launch. You can add this to View did load in the initial view controller, or in the app delegate  method application.
  • On interaction with the notification. Add to the delegate method userNotificationCenter:DidRecieveResponse.
  • On loading a custom notification layout from the Content Extension. I covered Content extensions last time, but when a user presents the full content of a custom notification, you can check the notification for expiration. I gave an example in the download file and whole code, which directly cleans the current notification. Setting up here would take too long.

I’d love to have code in the background do this, but I haven’t figured out how.

The Whole Code

I mentioned up above there is a download file for this project you can get here: repeatnotificationdemo. I added a content extension to this project’s download file and activated it, so the notification will look like this in the download compared to the project described above.

2017-01-31_07-06-44

AppDelegate.swift

//
//  AppDelegate.swift
//  RepeatNotificationDemo
//
//  Created by Steven Lipton on 1/22/17.
//  Copyright © 2017 Steven Lipton. All rights reserved.
//

import UIKit
import UserNotifications

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate,UNUserNotificationCenterDelegate {

    var window: UIWindow?
    
    func setCategories(){
        let clearRepeatAction = UNNotificationAction(
            identifier: "clear.repeat.action",
            title: "Stop Repeat",
            options: [])
        let pizzaCategory = UNNotificationCategory(
            identifier: "pizza.reminder.category",
            actions: [clearRepeatAction],
            intentIdentifiers: [],
            options: [])
        UNUserNotificationCenter.current().setNotificationCategories([pizzaCategory])
    }

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
        UNUserNotificationCenter.current().delegate = self
        setCategories()
        UNUserNotificationCenter.current().cleanRepeatingNotifications()
        return true
    }

    func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
        completionHandler([.alert,.sound])
        
    }
    
    func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
        UNUserNotificationCenter.current().cleanRepeatingNotifications()
        print("Did recieve response: \(response.actionIdentifier)")
        if response.actionIdentifier == "clear.repeat.action"{
            UNUserNotificationCenter.current().removePendingNotificationRequests(withIdentifiers: [response.notification.request.identifier])
        }
        completionHandler()
    }
}

ViewController.swift

//
//  ViewController.swift
//  RepeatNotificationDemo
//
//  Created by Steven Lipton on 1/25/17.
//  Copyright © 2017 Steven Lipton. All rights reserved.
//

import UIKit
import UserNotifications

extension UNUserNotificationCenter{
    func cleanRepeatingNotifications(){
        //cleans notification with a userinfo key endDate
        //which have expired.
        var cleanStatus = "Cleaning...."
        getPendingNotificationRequests {
            (requests) in
            for request in requests{
                if let endDate = request.content.userInfo["endDate"]{
                    if Date() >= (endDate as! Date){
                        cleanStatus += "Cleaned request"
                        let center = UNUserNotificationCenter.current()
                        center.removePendingNotificationRequests(withIdentifiers: [request.identifier])
                    } else {
                        cleanStatus += "No Cleaning"
                    }
                    print(cleanStatus)
                }
            }
        }
    }

    
}

class ViewController: UIViewController {
    var notificationGranted = false
    
   
    @IBAction func addNotification(_ sender: UIButton) {
        if notificationGranted{
            repeatNotification()
        }else{
            print("notification not granted")
        }
        
    }
    
    @IBAction func showNotificationStatus(_ sender: UIButton) {
        var displayString = "Current Pending Notifications "
        UNUserNotificationCenter.current().getPendingNotificationRequests {
            (requests) in
            displayString += "count:\(requests.count)\t"
            for request in requests{
                displayString += request.identifier + "\t"
            }
            print(displayString)
        }
        
        //cleanNotifications()
        
    }
    
    
    func repeatNotification(){
        let content = UNMutableNotificationContent()
        content.title = "Pizza Time!!"
        content.body = "Monday is Pizza Day"
        content.categoryIdentifier = "pizza.reminder.category"
        
        //for testing
        content.userInfo["endDate"] = Date(timeIntervalSinceNow: 60.00)
        
        // Set the end date to a number of days
        //let dayInSeconds = 86400.0
        //content.userInfo["endDate"] = Date(timeIntervalSinceNow: dayInSeconds * 90)
        
        //A repeat trigger for every minute
        //You cannot make a repeat shorter than this.
        //let  trigger = UNTimeIntervalNotificationTrigger(timeInterval: 60.0, repeats: true)
        
        //Date component trigger
        var dateComponents = DateComponents()
        
        // a more realistic example for Gregorian calendar. Every Monday at 11:30AM
        //dateComponents.hour = 11
        //dateComponents.minute = 30
        //dateComponents.weekday = 2
        
        // for testing, notification at the top of the minute.
        dateComponents.second = 0
        
        let trigger = UNCalendarNotificationTrigger(dateMatching: dateComponents, repeats: true)
        
        let request = UNNotificationRequest(identifier: "pizza.reminder", content: content, trigger: trigger)
        
        UNUserNotificationCenter.current().add(request) { (error) in
            if let error = error {
                print("error in pizza reminder: \(error.localizedDescription)")
            }
        }
        print("added notification:\(request.identifier)")
    }
    
    
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
        UNUserNotificationCenter.current().requestAuthorization(options: [.alert,.sound]) { (granted, error) in
            self.notificationGranted = granted
            if let error = error {
                    print("granted, but Error in notification permission:\(error.localizedDescription)")
            }
        }
        UNUserNotificationCenter.current().cleanRepeatingNotifications()
    }
    
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        //UNUserNotificationCenter.current().cleanRepeatingNotifications()
    }

    
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }


    
}


Customizing Notifications with the Service and Content Extensions

For all the changes in notifications starting in iOS 10 the coolest is the service extension and the content extension. The service extension intercepts push payloads from apps, and gives you the chance to change content in the notification before it is presented. The Content extension gives you the same tools you have in an app to design your notification in interface builder.
Extensions are background applications, running independently from your app. They are around even when the app is not in the foreground. These two extensions offer some powerful new tools for notification development.

I’ll be continuing lessons from the last two posts. Since the first one requires registration with apple, there is no download. Before you do this, check out my  first post on making push notifications here and the second on push notification actions here. There’s lot of steps I’ll assume you already know in this lesson from those lessons.

User Info

So far, you’ve added everything to the aps dictionary in the payload. APNs does not restrict you to these few content objects.
Go to the payload editor. So far we have this:

{
    "aps":{
        "alert":{
            "title":"Push Pizza Co.",
            "body":"Your pizza is  almost ready!"
        },
        "badge":42,
        "sound":"default",
        "category":"pizza.category",
    }
}

I placed everything in the aps dictionary. The aps dictionary of the payload are system defined key value pairs. If you place things outside this dictionary, but in the JSON dictionary, they are user defined values. For example, place a comma after the aps dictionary ends and then add these two lines:

  "order":["Huli pizza","Lilikoi punch","Duke Pie"],
         "subtitle":"Your order is ready!"

On the first line I added a list for my order in an array. There’s no subtitles in the aps dictionary, yet  subtitle is a property in the notification content, so I put it as userInfo for now.

These store in the userInfo dictionary of the notification content. You have to move them from  userInfo to the right place in the notification.

The Service extension

Storing that data in the userInfo property of UNNotificationContent object might be great, but how to we use them in a push notification? In the UserNotifications framework, Apple introduced the Notification Service Extension. It’s a small program running in the background of your device that intercepts your payload before notification center presents your push notification. In the service extension, you can modify your content with a UNMutablenotificationContent object modifying your content. That can be decrypting a encoded message or grabbing data in the userInfo to modify the content.

To set up the Notification Service extension, you’ll need a new target in your application. In Xcode, go to File>New Target.

2017-01-22_08-22-35

Select iOS, and you’ll find in the list the notification service extension.

2017-01-22_08-23-05

Select it, click Next and it will ask for a product name. I usually name this by suffixing ServiceExtension to my app name, so make it PushPizzaDemoServiceExtention. Click Finish.

2017-01-22_08-24-16

You’ll get a question about activating the extension. Go ahead and activate it.

2017-01-22_08-24-29

You’ll find in the navigator a new group. This is the service extension. Open it up and you’ll find two new files.

2017-01-24_07-16-24

Open it up and you’ll find two new files. Click on the NotificationService.swift file. This is only two functions and two properties. The core method is the didRecieve(Request: withContentHandler:) . Apple even started coding for you. The method assigns the content handler closure to one of the two properties. The other property, bestAttemptContent is a mutable copy of your push notification’s content. In this method, you change the content for the push notification, loading all that rich content you couldn’t fit in the payload, or do any change to the content you wish. For example, Apple’s template changes the title.

if let bestAttemptContent = bestAttemptContent {
   // Modify the notification content here...
   bestAttemptContent.title = "\(bestAttemptContent.title) [modified]"
            
   contentHandler(bestAttemptContent)
}

Always leave the function call to contentHandler there. That changes the content.

Go over to the payload. To use a notification extension, you must have the following key value pair in your payload’s aps dictionary:

"mutable-content":1 

Go over to your payload and add that to the payload, which should now look like this:

{
    "aps":{
        "alert":{
            "title":"Push Pizza Co.",
            "body":"Your pizza is  almost ready!"
        },
        "badge":42,
        "sound":"default",
        "category":"pizza.category",
        "mutable-content":1
    },
    "order":["Huli pizza", "Lilikoi punch","Duke pie"],
    "subtitle":"Your Order is ready",
    }

Coding the Service Extension with UserInfo

In the service extension, delete this line:

 bestAttemptContent.title = "\(bestAttemptContent.title) [modified]"

We’ll replace it with our code. I’m going to place the custom information into the body, giving a list of the order, and add the subtitle to the subtitle property. You’ll need the userInfo dictionary. I’ll make referring to the dictionary easier by making it a constant in the function, casting it to the JSON dictionary type of [String:Any]

//get the dictionary
let userInfo = bestAttemptContent.userInfo as! [String:Any]

Subtitles are user info in a payload but are a property of UNMutableNotificationContent, so it needs transferring to the property. Since it’s a dictionary entry, I’ll have to unwrap it and check for nil. If not nil, I have a subtitle and I can update the value. I downcast the value to String when assigning it the subtitle content, since the dictionary has the value as Any.

//change the subtitle
        if userInfo["subtitle"] != nil{
            bestAttemptContent.subtitle = userInfo["subtitle"] as! String
        }

The payload has an array of items ordered. I’ll make that the notification body text like this:

//change the content to the order
if let orderEntry = userInfo["order"] {
    let orders = orderEntry as! [String]
    var body = ""
    for item in orders{
        body += item + "\n "

    }
    bestAttemptContent.body = body
}

I’ll first check if the entry exists in the userInfo dictionary. If it does, create a string array orders from it. In the loop I’ll concatenate the orders into a string body, separating them with tabs. After the loop, I’ll change the bestAttemptContent.body to the new content.

The Content Extension

You’ve loaded content through the service extension. There’s another extension that gives you a storyboard to format your output, and add other types of objects. The Content extension gives you a view controller and storyboard to create your own notification look. TO add a content extension, go to File> New > Target. Under iOS you’ll find the service extension.

2017-01-24_07-22-37

Select it and click Next. Save a name of PushPizzaContentExtension. Click Finish, then activate.
You’ll find a new target on the navigator. Open it up and it is three files: A navigating controller, a storyboard and a info.plist.

2017-01-24_07-24-21

Open up the storyboard an you’ll find a very short looking scene. It’s here you will design your application.

2017-01-24_07-25-06

There’s one limitation to this storyboard. Interactive controls like buttons don’t work. You are always limited to static controls. With that in mind it’s time to design a custom notification

Change the UI

You’ll see the storyboard is a bit small, meant to cover only a small notification of the single label. Click the view controller icon and then go to the size inspector. The simulated size under freeform is 320 by 37.

2017-01-24_07-27-01

 

Change the Height to 150 to give us some space to work. This notification will look different on different devices, so use auto layout to place three labels. If you are not familiar with using auto layout, don’t fret. I’ll take you through the steps.

Delete the current label. Drag three new labels to the storyboard. Put two towards the top right and one towards the bottom left.

2017-01-23_05-58-53

Change the text of the top label to <Title>. I use angle brackets to document this is where I’m adding the content from the content.The middle one to <Subtitle> and the bottom to <Body>
On the <Title> label make the font Title1, right justified. It won’t fit, so drag to the left handles over so it goes completely across the scene, then right justify it. On the <Subtitle> label, set the text to Title 2, also right justifying. On the <Body> label, set the lines to 0 and the line break to word wrap. This will word wrap the order in the body if necessary.

2017-01-23_06-09-21

You’ll add some auto layout to this next. Click the <Title> label. Using the pin pinMenuButton tool, Pin up 10,left 4 and right 4 with the margins on, updating items of new constraints. Remember to press tab after every field.  The pin tool should look like this. Add the 3 constraints.

2017-01-23_06-11-40

The subtitle is a bit tricky. Pins in this storyboard always go to the superview. Using  the pin tool pinMenuButton  like you did for the title, pin the left and right sides of  subtitle  4 left and 4 right with constrain to margins on.

2017-01-24_07-30-58

Don’t update the view yet, just add the two constraints. Next control-drag from the subtitle to the title. Select vertical spacing in the menu that appears.

2017-01-24_07-34-43

In the size inspector find the constraint:

2017-01-24_07-35-43

Click Edit and change the constant to 10.

2017-01-24_07-37-12

Now click the update frames button update frames to update the frames.

Finally, select the body. Using the pin tool, pin it left 4, right,4 and bottom 10, with constrain to margins on and updating the frames. The completed layout should look like this:

2017-01-23_06-26-18

You have a layout for your notification. Just like an app, I’ll set my outlets.  Since I deleted a control,  Use Shift-Command-K to clean the project before going on. Close the attributes inspector. Open the assistant editor.

Control Drag from <Title>and make an outlet titleLabel.

Control-drag from <Subtitle>and make an outlet subtitleLabel.

Control-drag from <Body> and make an outlet bodylabel.

Close the assistant editor. I’ve now connected the the four outlets for the objects on the storyboard. The next step is to write some code.

Code the controller

Go to the NotificationViewController.swift file. Unlike most view controllers, there’s a special method here: didReceive: notification:. Besides a viewdidLoad, there’s no other methods you’re used to in a view controller. You’ll see in the method we have a template  on how to use it.

In this method’s parameter, we have the full notification. It could be either a push or a local notification. We extract the contents in this method and then populate the fields. Delete this code in the method.

self.label?.text = notification.request.content.body

To make things easier make a constant for the content of the notification:

let content = notification.request.content

I’ll use instead of notification.request.content every time. I can now populate the labels with their content.

titlelabel.text = content.title
subtitleLabel.text = content.subtitle
bodyLabel.text = content.body

With these three, we get all of our text labels into the notification.

The Property List

We’ve got one more step to go. Go back to Xcode. There one more file in the extension we’ve yet to look at. Click on the info.plist. Towards the bottom open the NSExtension. Inside that there’s several properties. Open the NSExtensionAttributes. You’ll find in there the UNNotificationExtensionCategory property.
Content extensions runs off categories, so whatever category this property is the category of the notification found in content.category. Change it to pizza.category. This extension will run for only the pizza.category notifications. If an extension has another category, it will not run. Remember to do this. If you forget this, the extension it will not work for you.

Run the Notification

You are ready to run the notification. When you add extensions, Xcode often defaults to the run scheme for the extension. Check to make sure you are on your device run scheme for the app

2017-01-23_06-41-43.

Run the application. Copy and paste the payload to the test platform. If you haven’t done so for this device, copy and paste the token for the app.

2017-01-24_06-37-53

Send the notification. When it appears like this
img_0036

Swipe down

img_0037

You get a custom notification, with alignment and font changes, but it is too big.

 

Resizing the Notification.

Go back to Xcode. The easiest way to restrict the notification is to add these two lines of code to viewDidLoad in the NotificationViewController.swift file in the content extension:

let size = view.bounds.size
preferredContentSize = CGSize(width: size.width, height: size.height / 4.0)

This code changes the height of the notification to a quarter the size of the notification. Depending on your notification, you can change this size by changing the 4.0. Run with this code, then send a notification from the test platform. When the notification appears on the device, swipe down. You get a better sized notification.
img_0038

Removing the Bottom Notification

You see in white on the bottom of the notification the original notification. You may not want that on your notification. Let me show you how to hide that. In Xcode, Go back to the info.plist  for the content extension again. Click the + for the NSExtentionAttributes.

2017-01-24_06-58-24

In the new attribute, give it a key UNNotificationExtensionDefaultContentHidden. Change it to a Boolean value, and set it to YES.

2017-01-24_06-58-52

Run again so it loads everything. Send the notification in the platform.The bottom disappears.
img_0039

If all you add to the storyboard is  new user content like  map, You may leave the white content to have the original message.

This is some of the basics of the service and content extensions. With a bit of work you can add images, maps and other objects to the extensions to make for a very rich notification for your users. Remember while the service extension is for push notifications only, you can use the content extension for local notifications and push notifications.

The Whole Code

payload.apns

{
    "aps":{
        "alert":{
            "title":"Push Pizza Co.",
            "body":"Your pizza is  almost ready!"
        },
        "badge":42,
        "sound":"default",
        "category":"pizza.category",
        "mutable-content":1
    },
    "order":["Huli pizza", "Lilikoi punch","Duke pie"],
    "subtitle":"Your Order is ready"
}

AppDelegate.Swift

//
//  AppDelegate.swift
//  PushPizzaDemo
//
//  Created by Steven Lipton on 1/2/17.
//  Copyright © 2017 Steven Lipton. All rights reserved.
//

import UIKit
import UserNotifications

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate {

    var window: UIWindow?

    func setCategories(){
        let snoozeAction = UNNotificationAction(identifier: "snooze.action", title: "Snooze", options: [])
        let pizzaCategory = UNNotificationCategory(identifier: "pizza.category", actions: [snoozeAction], intentIdentifiers: [], options: [])
        UNUserNotificationCenter.current().setNotificationCategories([pizzaCategory])
    }
    
    
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
        // Override point for customization after application launch.
        setCategories()
        UNUserNotificationCenter.current().delegate = self
        UNUserNotificationCenter.current().requestAuthorization(options: [.alert,.sound,.badge])
        {(granted,error) in
            if granted{
                application.registerForRemoteNotifications()
            } else {
                print("User Notification permission denied: \(error?.localizedDescription)")
            }
            
        }
        return true
    }
    
    func tokenString(_ deviceToken:Data) -> String{
        //code to make a token string
        let bytes = [UInt8](deviceToken)
        var token = ""
        for byte in bytes{
            token += String(format: "%02x",byte)
        }
        return token
    }
    
    func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
        //TODO: Add code to get token here
        print("Successful registration. Token is:")
        print(tokenString(deviceToken))
    }
    
    func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
        print("Failed to register for remote notifications: \(error.localizedDescription)")
    }



//MARK: - Delegates
    
    func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
        let request = notification.request
        print ("request identifier: \(request.identifier)" )
        completionHandler([.alert,.badge,.sound])
    }
    
    
    func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
        let action = response.actionIdentifier
        let request = response.notification.request
        
        if action == "snooze.action"{
            let content = changePizzaNotification(content: request.content)
            
            let snoozeTrigger = UNTimeIntervalNotificationTrigger(timeInterval: 5.0, repeats: false)
            let snoozeRequest = UNNotificationRequest(identifier: "pizza.snooze", content: changePizzaNotification(content:content), trigger: snoozeTrigger)
            center.add(snoozeRequest){
                (error) in
                if error != nil {
                    print("Snooze Request Error: \(error?.localizedDescription)")
                }
            }
        }
        completionHandler()
    }
    
    func changePizzaNotification(content oldContent:UNNotificationContent) -> UNMutableNotificationContent{
        //get a mutable copy of the content
        let content = oldContent.mutableCopy() as! UNMutableNotificationContent
        //get the dictionary
        let userInfo = content.userInfo as! [String:Any]
        //change the subtitle
        if userInfo["subtitle"] != nil{
            content.subtitle = userInfo["subtitle"] as! String
        }
        //change the body with the order
        if let orderEntry = userInfo["order"] {
            var body = ""
            let orders = orderEntry as! [String]
            for item in orders{
                body += item + "🍕🏄🏽\n"
            }
            content.body = body
        }
        return(content)
    }
}

PushPizzaDemoServiceExtension: NotificationService.swift

//
//  NotificationService.swift
//  PushPizzaDemoServiceExtension
//
//  Created by Steven Lipton on 1/5/17.
//  Copyright © 2017 Steven Lipton. All rights reserved.
//

import UserNotifications

class NotificationService: UNNotificationServiceExtension {

    var contentHandler: ((UNNotificationContent) -> Void)?
    var bestAttemptContent: UNMutableNotificationContent?

    
        
    override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
        self.contentHandler = contentHandler
        bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent)
        
        if let bestAttemptContent = bestAttemptContent {
            let userInfo = bestAttemptContent.userInfo as! [String:Any]
            //change the subtitle
            if userInfo["subtitle"] != nil{
                bestAttemptContent.subtitle = userInfo["subtitle"] as! String
            }
            //change the content to the order
            if let orderEntry = userInfo["order"] {
                let orders = orderEntry as! [String]
                var body = ""
                for item in orders{
                    body += item + "\n "
                    
                }
                bestAttemptContent.body = body
            }
            
            
            contentHandler(bestAttemptContent)
        }
    }
    
    override func serviceExtensionTimeWillExpire() {
        // Called just before the extension will be terminated by the system.
        // Use this as an opportunity to deliver your "best attempt" at modified content, otherwise the original push payload will be used.
        if let contentHandler = contentHandler, let bestAttemptContent =  bestAttemptContent {
            contentHandler(bestAttemptContent)
        }
    }
 
    func changePizzaNotification(content oldContent:UNNotificationContent) -> UNMutableNotificationContent{
        let content = oldContent.mutableCopy() as! UNMutableNotificationContent
        //get the dictionary
        let userInfo = content.userInfo as! [String:Any]
        //change the subtitle
        if let subtitle = userInfo["subtitle"]{
            content.subtitle = subtitle as! String
        }
        
        //change the body with the order
        if let orderEntry = userInfo["order"] {
            var body = ""
            let orders = orderEntry as! [String]
            for item in orders{
                body += item + ", "
            }
            content.body = body
        }
        return content
    }
}

PushPizzaDemoServiceExtension: NotificationViewController.swift


//
//  NotificationViewController.swift
//  PushPizzaDemoContentExtension
//
//  Created by Steven Lipton on 1/5/17.
//  Copyright © 2017 Steven Lipton. All rights reserved.
//

import UIKit
import UserNotifications
import UserNotificationsUI

class NotificationViewController: UIViewController, UNNotificationContentExtension {

    @IBOutlet weak var titlelabel: UILabel!
    @IBOutlet var subtitleLabel: UILabel!
    @IBOutlet var bodyLabel: UILabel!
    override func viewDidLoad() {
        super.viewDidLoad()
        
//set the proportional vertical size of the notification.
        let size = view.bounds.size
        preferredContentSize = CGSize(width: size.width, height: size.height / 4.0)
    }
    
    func didReceive(_ notification: UNNotification) {
        let content = notification.request.content
        titlelabel.text = content.title
        subtitleLabel.text = content.subtitle
        bodyLabel.text = content.body
    }
}

Actions in Push Notifications

In last week’s tutorial, I discussed how to set up a push notification. I also showed you how to test a notification on a test platform with a JSON payload. This week, we’ll go tot the next level: using actions in a push notification. We’ll start here from where we left off last week. If you have not done that lesson, go there first. Because of its nature there is no download file. As I do there, I’m assuming you know how to make a local notification. If you do not, you might want to read this first.

No More Blank Screens

Before we get started, I missed something in the last lesson I should have done. Go to the storyboard, and add one label “Push Pizza Company.”
2017-01-09_03-59-42

I centered this label using the auto layout align function, setting update constraint to items of new constraints.

2017-01-09_03-33-58
This give us one more visual cue the app ran correctly.

Payload Changes

The beauty of the User Notification framework is actions for a push notification work almost the same as a local notification. You set a category in the content, this time in the payload. You add one more key to the aps dictionary for the category:

{
     "aps":{
          "alert":{
              "title":"Push Pizza Co.",
              "body":"Your pizza is almost ready!"
           },
           "badge":42,
           "sound":"default",
           "category":"pizza.category"
    }
}

Setting Categories and Actions

Then you set the category in the AppDelegate. I usually create a new function to do this. Add this function:

func setCategories(){

}

Your first step in this function is to make an action using the UNNotificationAction constructor:

    let snoozeAction = UNNotificationAction(
        identifier: "snooze.action",
        title: "Snooze",
        options: [])

The action wants a unique identifier, which you’ll use to identify it when an action fires. The method also wants a title for the button that will appear on the notification for the action. We have no options, so leave that as a blank array.

Next we stick the action in a category, I’m using the same category identifier as the payload category.

    let pizzaCategory = UNNotificationCategory(
        identifier: "pizza.category",
        actions: [snoozeAction],
        intentIdentifiers: [],
        options: [])

Notice actions is an array. I have only one here, but you can add several if you wanted to. Finally I send the category to the system

    NUserNotificationCenter.current().setNotificationCategories(
        [pizzaCategory])
}

With these three lines You’ve created the snoozeAction, made a category pizza.catergory with them, and then set the notification into the notification center. This had to one of the first thing you do when the application launches, so call the function on didFinishLoading

setCategories()

Build and run. When the push pizza company label appears, on the app, you can close it. Launch the notification on the test platform. (for setup of the test platform see last week’s post) You’ll see the notification, then view it.

img_0023

We have the snooze button, but it does nothing.

Running Code from Actions

In order to make the snooze button do something, You need a delegate method found in the UNNotificationCenterDelegate delegate. Adopt the delegate

class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate {

In didfinishLoading, Set the delegate to self.

UNUserNotificationCenter.current().delegate = self

Add the didRecieveResonse delegate method to the class.

func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {

}

By the time the notification reached this method, the payload from the push notifications is now content of a notification. For a snooze button I don’t touch the content, but I can copy it to a new local notification. Create three constants action, request, and content to have these easily available.

let action = response.actionIdentifier
let request = response.notification.request
let content = request.content

You identify the actionIdentifier then run code if it matches. Add this code for when the action is a snooze.action.

if action == "snooze.action"{
    let snoozeTrigger = UNTimeIntervalNotificationTrigger(
        timeInterval: 5.0,
        repeats: false)
    let snoozeRequest = UNNotificationRequest(
        identifier: "pizza.snooze",
        content: content,
        trigger: snoozeTrigger)
     center.add(snoozeRequest){
        (error) in
        if error != nil {
            print("Snooze Request Error: \(error?.localizedDescription)")
        }
     }
}

Our code will convert the remote notification to a local time interval notification. I’ll make a new trigger with a time interval of 5 seconds and non-repeating. I made new Notification request snoozeRequest using the identifier from the push notification, the content from the push notification, and the new snooze trigger. Finally, I added the notification to the Notification Center associated with the notification.

In the request identifier, I used a single literal identifier so any push notifications using this action will have only one snooze notification, updating the content of the last snooze. You won’t have fifty snoozes for fifty different pushes.

Finally, make sure you add the completion handler at the end of the delegate method.

completionHandler()

Run the application to load all these changes onto your device. Stop the app once it runs. Go to the test platform and send a notification.

2017-01-09_04-40-40

You’ll see the notification appear on the device. Open the notification and hit Snooze.

img_0023

Five seconds later, the notification appears again.

In this example I used a snooze button and made it a local notification from a push notification. If you have notifications that will repeat after the initial push notification, this is a very good practice. You have a limited number of push notification per app per day. Instead of squandering them on repeating the same message, use a local notification for repetitive notifications like snooze buttons. Also remember you only have four actions. Don’t go crazy adding actions everywhere.

The Whole Code

Like last week there is no download. There so little here you can copy it, and much of the meat is the certificates which you have to do yourself. Fr brevity, I removed extra unused methods from the app delegate.

//
//  AppDelegate.swift
//  PushNotification
//
//  Created by Steven Lipton on 12/30/16.
//  Copyright © 2016 Steven Lipton. All rights reserved.
//

import UIKit
import UserNotifications

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate {

    var window: UIWindow?

    func setCategories(){
        let snoozeAction = UNNotificationAction(
            identifier: "snooze.action",
            title: "Snooze",
            options: [])
        let pizzaCategory = UNNotificationCategory(
            identifier: "pizza.category",
            actions: [snoozeAction],
            intentIdentifiers: [],
            options: [])
        UNUserNotificationCenter.current().setNotificationCategories(
            [pizzaCategory])
    }
    
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
        // Override point for customization after application launch.
        setCategories()
        UNUserNotificationCenter.current().delegate = self
        UNUserNotificationCenter.current().requestAuthorization(options: [.alert,.sound,.badge], completionHandler: {(granted,error) in
            if granted{
                application.registerForRemoteNotifications()
            }
        })
        
        return true
    }
    
    
    func tokenString(_ deviceToken:Data) -> String{
        //code to make a token string
        let bytes = [UInt8](deviceToken)
        var token = ""
        for byte in bytes{
            token += String(format: "%02x",byte)
        }
        return token
    }
    
    
    //MARK: - Register Handling
    func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
        print ("token -- \n \(tokenString(deviceToken))")
    }
    
    func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
        print("Error = \(error.localizedDescription)")
    }

    //MARK: - Delegates for Notifications
    func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
        let action = response.actionIdentifier
        let request = response.notification.request
        let content = request.content
        if action == "snooze.action"{
            let snoozeTrigger = UNTimeIntervalNotificationTrigger(
                timeInterval: 5.0,
                repeats: false)
            let snoozeRequest = UNNotificationRequest(
                identifier: "pizza.snooze",
                content: content,
                trigger: snoozeTrigger)
            center.add(snoozeRequest){
                (error) in
                if error != nil {
                    print("Snooze Request Error: \(error?.localizedDescription)")
                }
            }
        }
        completionHandler()
    }
}


Basic Push Notifications in iOS 10 and Swift

Push notifications are messages from a  remote server to your device.  You find them in many types of applications: Messaging apps, notifications from social media platforms, weather, news, and sports reporting. Because this information comes from an outside source, there’s a lot more security and scrutiny needed to the messages delivered. There’s also a large concern to keep the data transmitted small: you can at most transmit 4K of data.

In this lesson, I’ll show you how to set up your application to receive a push notification. I’m going to make some  assumptions here: You are using iOS 10 to do this and the User Notification framework. If you are not, there plenty of other tutorials by others on the older methods.  Secondly, I’m going to keep to the client side of this. I’m not going to talk at all about setting up a server or using a pre made server to send your push notifications. We’ll use an online tool to test our notifications.  I’ll also assume is you have a paid developer account and a phone to test with. Push notifications are one of those few things that need a paid account. Push notifications don’t run on the simulator either, so you’ll need a live phone to test.

With that said, let’s look at how push notifications differ from local notifications.

Local Notifications,  APNs, Tokens and Certificates

If you’ve read my piece on local notifications, local notifications are based on UNNotificationRequest objects added to the  current UNNotificationCenter. Requests have three parts, a unique identifier, a trigger and content. A trigger gives the conditions when the notification appears on a user’s device. The content is the text and attachments that  appear in the notification.  You as a developer set up all of this in code. For review, here’s a simple notification code snippet:

// ---- Make content -------
let content = UNMutableNotificationContent()
content.title = "MakeAppPie Pizza Co."
content.body = "Hello, Pizza!!"
content.categoryIdentifier = "pizza.category"
// ----- Use a time interval trigger of 5 seconds, non repeating       
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 5.0, repeats: false)

// ----- Add the trigger and the content to the request
let request = UNNotificationRequest(identifier: "pizza", content: content, trigger: trigger)

// Add the request to the current notification center, and notify the developer of errors in the closure. 
UNUserNotificationCenter.current().add(request){
     (error) in
     if error != nil{
         print ("Add notification error: \(error?.localizedDescription)")
     }
}

In a remote notification,  all of that is unnecessary.  There’s a server somewhere pushing data towards your device. However between a user’s device and the server is the Apple Push Notifications Service or APNs. To simplify APNs, this service takes the server’s (or as Apple refers to it provider’s) data, checks if it is legitimate data from a legitimate source and sends a  payload  from APNs to the specific device registered to receive the push notification.

APNs coordinates security and device identity through a certificate key and a token. You as a developer register your app with apple and get an app  certificate, an encoded file that identifies your app as a legitimate app to send notifications to. When you run the application, a token gets generated to identify this specific device to APNs.  The developer sends this certificate and token to the provider of the push notification.  Once the provider has this information, when the logic of the provider indicates a need to push a notification, it sends the token in a payload with the message to APNs, who knows how to handle it.

You can read the official documentation or check out the WWDC 2016 video on this for more detail, but for our purposes that’s what you need to know.

Start a New Project

We’ll start this as a new project in Xcode. Open a new single-view project named PushPizzaDemo in Swift. Save the file. The Project opens to  the settings file. Under General, Change the display name to Push Pizza. This will set the icons on the phone to a nicer caption.

2017-01-02_06-49-05

Next to the General Tab now selected, You’ll find the Capabilities tab. Click that and you’ll find a series of extra services you can turn on. The second on the list is Push Notifications. Click the switch to On.

2017-01-02_06-54-40

This sets the app ready to use push notifications and informs Apple you are doing so in your App ID.

Scroll down a bit further, and you’ll find the Background Modes switch.  Open up that Capability, turn it on and select Remote Notifications.

2017-01-02_06-59-11

This tells the system to allow background processing for remote notifications.

We’ve set up the permissions in Xcode to use notifications, we have two more places to get permission: in code and with the certificate.

Register for Notifications

In order to use remote notifications, you have to tell the system before doing anything else you want them. For this reason, you’ll be coding remote notifications in the app delegate. Open the AppDelegate.swift file  and under the import UIKit add the notification framework:

import UserNotifications

Like any other notification, you must ask for permission before using them. For this app, I’ll ask for permission for the alert, sound and badge. Before return true in the application:didFinishLaunchingWithOptions: method add this.

UNUserNotificationCenter.current().requestAuthorization(options: [.alert,.sound,.badge]){
     (granted,error) in
     if granted{
         application.registerForRemoteNotifications()
     } else {
         print("User Notification permission denied: \(error?.localizedDescription)")
     }
            
}

If you’ve made a local notification before, most of this code should look familiar. You request authorization for an alert sound and badge notification. In the closure, you deal with the results of that request. If not granted, you post a message with an error to the console.You can change this code to your wish, possibly adding an alert if the user refuses. What’s different from local notifications   is if you are granted access. The application.registerForRemoteNotifications() method registers the app to recieve notifications from APNs.

There’s two more methods of the app delegate you’ll need to add, both of which you should find in the auto completion of Xcode. Add the first one with a comment.

func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
        //TODO: Add code here later to deal with tokens.
    }

The application:didRegisterForremoteNotificationsWithDeviceToken: method runs if registration is successful. You’ll send the deviceToken in the argument from here to your provider. We’ll get back to that later, so for now add the comment.
The second method
application:didFailToRegisterForRemoteNotificationsWithError runs if the registration is not successful. That could be for lack of internet connection, lack of certificate, or you are running on the simulator which prohibits remote notifications among others. Add the application:didFailToRegisterForRemoteNotificationsWithError to print an error to the console.

func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
        print("Failed to register for remote notifications: \(error.localizedDescription)")
    }

Get a Certificate Key

Your next step is to get the  SSL certificate. For that you’ll have to head over to your apple developer account at https://developer.apple.com/account. As I said earlier, you need a paid account to do the next step. Select the Certificates, Identifiers & Profiles button if it appears.
2017-01-02_07-34-40

Under certificates, Select the APNs Auth Key

2017-01-02_14-55-00

You’ll get a screen like this:

2017-01-02_07-50-25

Click the Certificate Signing Request link. It will ask you for an app ID you want to use. Select the PushPizzaDemo:

2017-01-02_07-51-38

Your next step is creating a certificate request. In you Mac’s applications folder find the Utilities folder. In there, run Keychain Access.

2017-01-03_08-10-00

From the top menu of Keychain Access, select Certificate Assistant> Request a Certificate From a Certificate Authority…

2017-01-02_07-54-47

The certificate assistant appears. Use your email address of your developer id for User Email Address.  Add your name to the common name. Make sure to click the radio button Saved to disk.

2017-01-02_07-55-33

Your file will save to disk, I save mine in downloads.

2017-01-02_07-56-16

Once you have that CSR, go back to the certificate request. choose the file you just downloaded .

2017-01-02_07-57-08

Your certificate will generate and will be ready for download.

2017-01-02_07-58-19

Download it and then double click it to add it to your keychain. Once there go back to keychain access and find the entry for it. open the entry to see the private key.

2017-01-02_14-52-08

You only need the key for what’s ahead. Right click and export the private key.

2017-01-02_14-52-23

For simplicity, I left the password blank, and just pressed OK. For security, add a password.

2017-01-02_14-34-52

Keychain Access will ask you if you really want to do this

2017-01-02_14-35-11

Select Allow, and save the exported file somewhere you can find it.

Back in the developer accounts,  Click App ID’s. Find the XC Comm PushPizza Demo entry and click it. All of the application services appear.

2017-01-03_05-33-51

Toward the bottom of this list You’ll find Push Notifications and under that, an edit button. Click the Edit button, the click the push notification entry.

2017-01-02_14-37-56

You’ll see the results of all the hard work in getting your SSL certificate.  You can generate more here if you need them. I used a production certificate for this example since it can be used for both production and development. You could use a sandboxed development one as well. You need a new certificate for each app, so this isa process you’ll be repeating frequently if working with remote notifications.

Get a Token from APNs

To use a remote notification provider, you need a certificate and a token. The app generates the token with information about the device running the application. It asks APNs some questions, and APNs returns the token. While this is an oversimplification, imagine that the SSL certificate is permission to talk to APNs and the token is where the provider wants to send a message from APNs. Back in Xcode, we stubbed a method earlier that gives us that token.

func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
        //TODO: Add code to get token here
    }

The deviceToken is of type Data. While there might be some systems that accept a type Data token, you may find many, including online test platforms, want a string. You’ll need to convert this into a string of 8-bit hexadecimal numbers. Check with the provider’s documentation on what you need.

I wrote a small function to do this conversion. Add this to your AppDelegate:

func tokenString(_ deviceToken:Data) -> String{
//code to make a token string
    let bytes = [UInt8](deviceToken)
    var token = ""
    for byte in bytes{
        token += String(format: "%02x",byte)
     }
     return token
}

This makes an array of 8-bit unsigned integers, then converts each of those integers into a 2-digit hexadecimal number, which gets concatenated to a token string. I can add this to the application:didRegisterForRemoteNotificationsWithDeviceToken: method to print it out on the console.

print("Successful registration. Token is:")
print(tokenString(deviceToken))

You might send it to your provider though a provider specific add-on library or payload. Check the documentation for how to do that. In this example, I’m cutting and pasting this into a website, so sending to the console is what I want.

Run the Code

For the application, we’ve gotten everything put together in code. The app will run, register a device then return a token to the console. That’s all it needs to do. Connect a device (i used an iPad mini)  with an internet connection to Xcode and set the scheme to run the device. Since we set nothing up on the storyboard, the device will be a pretty boring blank screen. First it will ask for permission to use notifications.

2017-01-03_08-20-47

If you allow, it will print in the console something like

Successful registration. Token is:
3d1864107665c4cc4a55130e0249d3f6aab07a4fde3bf91426a45e7f2f13b4e1

Shut down the app in Xcode. On your device, Go to the home screen.

Test a Payload with an Online Tester

You now have a certificate and token. With both you can send a notification. There’s several ad-supported sites which work the same way. I’ll use pushtry.com due to flexibility in certificates. Load the site and you’ll see several fields for the iOS version

2017-01-03_06-34-14

At the top, you add your certificate by clicking the Choose File button. If you used a password  for the certificate place it in the password field. I didn’t for this very insecure example, so I’ll leave it blank. For the token field, cut and paste the token from the console (don’t use mine – it won’t work).

2017-01-03_06-35-59

The bottom fields show if this is a production system or a developer. Using a production certificate you can do either, so I left it as a development mode.  At the very bottom is two radio buttons switching from text to JSON. APNs prefers JSON, so click JSON, which puts a sample payload in for you.

2017-01-03_06-36-29

Press send. if it all works right, you see a message that the notification sent correctly.

2017-01-03_06-53-01

 

From all time messing in the website, your device probably went sleep.  The push notification will wake it up.

img_0009

If it was still awake, you’ll get a banner, and see a badge on the icon:

img_0010

Tap either, and you’ll launch the application into the foreground, which for us is a bank white screen.

A Note about PEM Certificates

You may have noticed that Pushtry.com takes two types of files PEM and p12.  Many systems and test platforms need you to use PEM. I use Pushtry for this reason: for early testing I can skip a step.  However, you will find a place that needs a PEM, and to get that, you’ll need some command line work. Open up a terminal instance by going to applications>Utilities>terminal.

Go to where you saved the certificate. I try to download these to the desktop to make this step easier.

Go to the desktop, or wherever you saved the certificate.

cd desktop

Run this command, which converts the file.

openssl pkcs12 -in PushPIzzaCertificates.p12 -out pushpizzacert.pem -nodes

Now you can use the PEM. file as well, which you’ll find on your desktop.

Payloads are Your Content

Much of what we just did replaces the trigger in a local notification. Content for a notification is found in the payload. Going back to the testing platform, you’ll find this:

{"aps":{"alert":"Enter your message","badge":1,"sound":"default"}}

Ideally your JSON file should look like this. You only have 4K for your payload so wasting it on spaces is frowned on. When sending payloads avoid whitespace. However, they are hard to read this way. It looks better like this:

{
     "aps":{
            "alert":"Enter your message",
            "badge":1,
            "sound":"default"
     }
}

The aps is a JSON dictionary with entries that describe your content. The alert entry is can be a string like it is here, or a dictionary describing the content of the alert that shows on your device. The badge give the number to show on the badge icon. The sound plays the default sound. You can modify this payload to change the content displayed in the alert. As the alert can be both a dictionary or a string you can add more to it. Change the payload to this:

{
     "aps":{
            "alert":{
                    "title":"Push Pizza Co.",
                    "body":"Your pizza is ready!"
             },
                "badge":42,
                "sound":"default"
     }
}

This will add a title and a message about your pizza being ready. It will also change the badge to 42.
Without spaces that’s this

{"aps":{"alert":{"title":"Push Pizza Co.","body":"Your pizza is ready!"},"badge":42,"sound":"default"}}

Change the payload in the tester to the above code and send the notification. You’ll get this

img_0011

The notification appears with the title and body. The badge appears with the number 42.

There’s a lot more you can do with the payload, including categories and setting up action buttons. We’ll discuss all that in the next lesson.

 

The Whole Code

All the code this time was in the app delegate, so I’m only posting a trimmed version with the methods we use. Since there’s so many external parts to push notification, there’s no downloads this time.

//
//  AppDelegate.swift
//  PushPizzaDemo
//
//  Created by Steven Lipton on 1/2/17.
//  Copyright © 2017 Steven Lipton. All rights reserved.
//

import UIKit
import UserNotifications

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?

// Check if you have permission to use notifications. 
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
        // Override point for customization after application launch.
        UNUserNotificationCenter.current().requestAuthorization(options: [.alert,.sound,.badge])
        {(granted,error) in
            if granted{
                application.registerForRemoteNotifications()
            } else {
                print("User Notification permission denied: \(error?.localizedDescription)")
            }
            
        }
        return true
    }
         
 //code to make a token string  
    func tokenString(_ deviceToken:Data) -> String{
        let bytes = [UInt8](deviceToken)
        var token = ""
        for byte in bytes{
            token += String(format: "%02x",byte)
        }
        return token
    }
// Successful registration and you have a token. Send the token to your provider, in this case the console for cut and paste. 
    func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
      
        print("Successful registration. Token is:")
        print(tokenString(deviceToken))
    }
// Failed registration. Explain why.     
    func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
        print("Failed to register for remote notifications: \(error.localizedDescription)")
    }

}