Back to search

Publish notifications from a news CMS to an iOS app with Pusher Beams

  • Neo Ighodaro
April 30th, 2018
You will need the following to work through this tutorial: A Mac with Xcode, Laravel CLI, SQLite and CocoaPods installed Knowledge of Xcode, Swift and Laravel A Pusher account

A recent piece about the New York Times tech team “How to Push a Story” chronicled the lengths they go to make sure that the push notifications they send are relevant, timely, and interesting.

The publishing platform at the NYT lets editors put notifications through an approval process, measures the tolerance for the frequency of notifications, and tracks whether users unsubscribe from them.

In this article, we will build a simple news publishing CMS and iOS mobile application that has the ability to send the latest news as a push notification. We will also show how you can use interests to segment users who receive push notifications based on specific news categories like “Business” or “Sports”.

When we are done, we will have our application function like this:



Once you have the requirements, let’s start.

Creating our Pusher application

⚠️ You need to be enrolled to the Apple Developer program to be able to use the push notifications feature. Also push notifications do not work on simulators so you will need an actual iOS device to test.

Pusher Beams has first-class support for native iOS applications. Your iOS app instances subscribe to Interests; then your servers send push notifications to those interests. Every app instance subscribed to that interest will receive the notification, even if the app is not open on the device at the time.

This section describes how you can set up an iOS app to receive transactional push notifications about news updates through Pusher.

Configure APNs

Pusher relies on the Apple Push Notification service (APNs) to deliver push notifications to iOS application users on your behalf. When we deliver push notifications, we use your key that has APNs service enabled. This page guides you through the process of getting the key and how to provide it to Pusher.

Head over to the Apple Developer dashboard by clicking here and then create a new key as seen below:


When you have created the key, download it. Keep it safe as we will need it in the next section.

⚠️ You have to keep the generated key safe as you cannot get it back if you lose it.

Creating your Pusher application

The next thing you need to do is create a new Pusher Beams application from the Pusher dashboard.

Push-Notifications-News-App Beams

When you have created the application, you should be presented with a quickstart that will help you set up the application.

In order to configure your Beams instance you will need to get the key with APNs service enabled from Apple. This is the same key as the one we downloaded in the previous section. Once you’ve got the key, upload it.


Enter your Apple Team ID. You can get the Team ID from here. You can then continue with the setup wizard and copy the instance ID and secret key for your Pusher application.

Building our backend using Laravel

The first thing we need to do, is build the backend application. Start a new Laravel project by running the command below in your terminal:

    $ laravel new project-name

When the process is complete, open your project in a code editor of your choice. Next, let’s start building out our project.

Open the .env file and in the file add the following to the bottom:


in the same file, replace the following lines:




Then create a new empty file database.sqlite in the database directory.

Next, let’s install the Pusher Beams SDK for PHP to our application. Run the command below to install using composer:

    $ composer install pusher/pusher-push-notifications

When installation is completed, let’s create our model and migration files. Run the command below to generate our model and migrations for our stories and story_categories table:

    $ php artisan make:model StoryCategory -m
    $ php artisan make:model Story -m

💡 Adding the -m flag will make artisan generate a corresponding migration file for the Model. Also, the order the command is run is important because the stories table will have a foreign key relationship with the story_categories table so the latter needs to exist first.

Open the app/StoryCategory.php model and add the property below to the class:

    protected $fillable = ['title', 'interest']

Next, open the corresponding migration file in database/migrations directory and replace the up method with the following:

    public function up()
        Schema::create('story_categories', function (Blueprint $table) {

Open the app/Story.php model and replace the contents with the following:

    namespace App;

    use Illuminate\Database\Eloquent\Model;
    use Pusher\PushNotifications\PushNotifications;

    class Story extends Model
        protected $with = ['category'];

        protected $fillable = [
            'category_id', 'title', 'notification', 'content', 'featured_image'

        public function push(): array
            if (!$this->exists or $this->notification == null) {
                return [];

            $pushNotifications = new PushNotifications([
                'instanceId' => env('PUSHER_PN_INSTANCE_ID'),
                'secretKey' => env('PUSHER_PN_SECRET_KEY'),

            $publishResponse = (array) $pushNotifications->publish(
                    'apns' => [
                        'aps' => [
                            'alert' => [
                                'title' => "📖 {$this->title}",
                                'body' => (string) $this->notification,
                            'mutable-content' => 0,
                            'category' => 'pusher',
                            'sound' => 'default'
                        'data' => array_only($this->toArray(), [
                            'title', 'content'
            return $publishResponse;

        public function category()
            return $this->belongsTo(StoryCategory::class);

In the model above, we have a push method. This method is a shortcut that helps us send push notifications on the loaded story model. This way we can do something similar to:


Open the corresponding migration file for the Story model in database/migrations and add replace the up method with the following code:

    public function up()
        Schema::create('stories', function (Blueprint $table) {

Now run the command below to process the migrations:

    $ php artisan migrate

If you have setup the database properly, you should see a ‘migration successful’ response from the terminal.

Next, let’s create the routes our application will need. Open the routes/web.php file and replace the contents with the following code:

    use App\Story;
    use Illuminate\Http\Request;

    Route::view('/stories/create', 'story');

    Route::post('/stories/create', function (Request $request) {
        $data = $request->validate([
            'title' => 'required|string',
            'content' => 'required|string',
            'notification' => 'nullable|string',
            'category_id' => 'required|exists:story_categories,id',

        $story = Story::create($data);

        return back()->withMessage('Post Added Successfully.');

    Route::get('/stories/{id}', function (int $id) {
        return Story::findOrFail($id);

    Route::get('/stories', function () {
        return Story::orderBy('id', 'desc')->take(20)->get();

Above, we have four routes:

  • GET /stories/create which just loads a view story.blade.php. This view will be used to display a form where we can enter new content.
  • POST /stories/create which processes the form data from above, adds the content and sends a push notification if appropriate.
  • GET /stories/{id} which loads a single story.
  • GET /stories which loads the 20 of the most recent stories.

Next, let’s create the view file for the first route as that is the only missing piece. Create a story.blade.php file in the resources/views directory and paste the following HTML code:

    <!DOCTYPE html>
    <html lang="en">
        <title>Create new post</title>
        <meta charset="utf-8">
        <link rel="stylesheet" href="">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <style type="text/css">
        .invalid-feedback { width: 100%; margin-top: .25rem; font-size: 80%; color: #dc3545; }
    <body style="margin-top: 120px;">
        <nav class="navbar navbar-inverse bg-inverse fixed-top">
            <a class="navbar-brand" href="#" style="font-weight: bold">TECHTIMES</a>
        <div class="container">
            @if (Session::has('message'))
            <div class="alert alert-success" role="alert">{{ session('message') }}</div>
            <div class="starter-template">
                <form action="/stories/create" method="POST">
                    {{ csrf_field() }}
                    <div class="form-group">
                        <label for="post-title">Post Title</label>
                        <input name="title" type="text" class="form-control" id="post-title" placeholder="Enter Post Title">
                        @if ($errors->has('title'))
                        <div class="invalid-feedback">{{ $errors->first('title') }}</div>
                    <div class="form-group">
                        <label for="post-category">Category</label>
                        <select name="category_id" id="post-category" class="form-control">
                            <option value="">Select A Category</option>
                            @foreach (App\StoryCategory::all() as $category)
                            <option value="{{ $category->id}}">{{ $category->title }}</option>
                        @if ($errors->has('category_id'))
                        <div class="invalid-feedback">{{ $errors->first('category_id') }}</div>
                    <div class="form-group">
                        <label for="post-content">Post Content</label>
                        <textarea name="content" id="post-content" cols="30" rows="10" placeholder="Enter your post content" class="form-control"></textarea>
                        @if ($errors->has('content'))
                        <div class="invalid-feedback">{{ $errors->first('content') }}</div>
                    <div class="form-group">
                        <label for="post-notification">Push Notification</label>
                        <input name="notification" type="text" class="form-control" id="post-notification" placeholder="Enter Push Notification Message">
                        @if ($errors->has('notiifcation'))
                        <div class="invalid-feedback">{{ $errors->first('notiifcation') }}</div>
                        <small class="form-text text-muted">Leave this blank if you do not want to publish a push notification for this post.</small>
                    <button type="submit" class="btn btn-primary">Save Post</button>

The above is just a basic bootstrap powered view that allows the user to create content as seen in the screen recording at the beginning of the article.

That’s it. The backend application is complete. To start serving the application, run the following command:

    $ php artisan serve

This will start a PHP server running on port 8000. You can access it by going to

Building our iOS application using Swift

Now that we have a backend server that can serve us all the information we want and also send push notifications, let us create our iOS application, which will be the client application.

Launch Xcode and create a new ‘Single Page Application’ project. We will be calling ours TechTimes. When the project is created, exit Xcode and cd to the root of the project using a terminal. In the root of the project create a Podfile and paste the following into the file:

    platform :ios, '11.0'
    target 'techtimes' do
      pod 'Alamofire', '~> 4.6.0'
      pod 'PushNotifications', '~> 0.10.7'

Then run the command below to start installing the dependencies we defined above:

    $ pod install

When the installation is complete, we will have a new .xcworkspace file in the root of the project. Double-click the workspace file to relaunch Xcode.

Next let’s create our storyboard. Open your Main.storyboard file. We want to design it to look similar to this:


The initial view controller is a UINavigationController, which is connected to a UITableViewController. The Table View Controller has custom cells that have the class StoryTableViewCell, which we will use to display the data in our cell. The cells have a reuse identifier: Story.

The second scene has a custom class of StoriesTableViewController. We have designed the scene to have a button on the top right which is connected to the Controller via an @IBAction. When the button is clicked we want to display the bottom View Controller.

The third scene has a custom class of StoryViewController and it has a UIScrollView where we have an image, post title and post content. We have created an @IBOutlet for both the title and the content text to the custom class Controller so we can override the contents.

We give each controller a unique storyboard identifier which is the custom class name so we can navigate to them using their storyboard ID.

When you are done creating the storyboard, let’s create the custom classes for each story board scene.

Create a new class StoriesTableViewController.swift and paste the following code into it:

    import UIKit
    import Alamofire

    class StoriesTableViewController: UITableViewController {
        var stories: [Stories.Story] = []

        override func viewDidLoad() {

            self.tableView.rowHeight = UITableViewAutomaticDimension
            self.tableView.estimatedRowHeight = 140

            self.fetchStories { response in
                guard let response = response else { return }
                self.stories = response.stories

        private func fetchStories(completion: @escaping(Stories.Response?) -> Void) {
            let request = Stories.Request()
            Alamofire.request(request.URL).validate().responseJSON { response in
                guard response.result.isSuccess,
                let stories = response.result.value as? [[String:AnyObject]] else {
                    return completion(nil)

                completion(Stories.Response(stories: stories))

        private func noStoriesAlert() {
            let alert = UIAlertController(
                title: "Error Fetching News",
                message: "An error occurred while fetching the latest news.",
                preferredStyle: .alert

            alert.addAction(UIAlertAction(title: "Okay", style: .default, handler: nil))
            self.present(alert, animated: true, completion: nil)   

In viewDidLoad above, we call the fetchStories method, which fetches the stories from the remote application. When the fetch is complete, we then set the stories property and reload the table view.

In the same file let’s create an extension to the controller. In this extension, we will override our Table View Controller delegate methods:

    extension StoriesTableViewController {
        override func numberOfSections(in tableView: UITableView) -> Int {
            return 1

        override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
            return stories.count

        override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
            let cell = tableView.dequeueReusableCell(withIdentifier: "Story", for: indexPath) as! StoryTableViewCell
            let story = stories[indexPath.row]
            let randomNum = arc4random_uniform(6) + 1

            cell.imageView?.image = UIImage(named: "image-\(randomNum)")
            cell.storyTitle?.text = story.title
            cell.storyContent?.text = story.content

            return cell

        override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
            let storyboard = UIStoryboard(name: "Main", bundle: nil)
            let vc = storyboard.instantiateViewController(withIdentifier: "StoryViewController") as! StoryViewController
            vc.story = stories[indexPath.row]
            self.navigationController?.pushViewController(vc, animated: true)

In the tableView(_:cellForRowAt:) method, we define the title and content for our cell. For our image, we are using random images we stored manually in our Asset.xcassets collection. In a real application you would probably want to load the UIImage from the URL returned by the API.

In the tableView(_:didSelectRowAt:) method, we instantiate the *StoryViewController*, set the story property of the controller and then navigate to the controller.

Let’s define the code to the StoryTableViewCell. Create a new file called StoryTableViewCell.swift and paste in the following:

    import UIKit

    class StoryTableViewCell: UITableViewCell {
        @IBOutlet weak var storyTitle: UILabel!
        @IBOutlet weak var storyContent: UILabel!
        @IBOutlet weak var featuredImageView: UIImageView!

        override func awakeFromNib() {

💡 Make sure this is set as the custom class for our StoryTableViewController's cells.

Next, let’s create the Stories struct we references in the stories property of out StoryTableViewController class. Create a Stories.swift file and paste in the following:

    import UIKit

    struct Stories {
        struct Request {
            let URL = ""

        struct Response {
            var stories: [Story]

            init(stories: [[String:AnyObject]]) {
                self.stories = []

                for story in stories {
                    self.stories.append(Story(story: story))

        struct Story {
            let title: String
            let content: String
            let featuredImage: UIImage?

            init(story: [String:AnyObject]) {
                self.title = story["title"] as! String
                self.content = story["content"] as! String
                self.featuredImage = nil

In the Stories struct we have the Request struct which contains the URL of the API. We also have the Response struct and in there, in the init method we pass the data from the API which will then create a Stories.Story instance.

The Stories.Story struct has an init function that takes a dictionary and assigns it to the properties on the struct. With this we can easily map all the results from the API to the Stories.Story struct. This helps keep things structured and clean.

Next, let’s create the StoryViewController class that will be the custom class for our third scene in the storyboard. Create a new StoryViewController.swift file and paste in the following:

    import UIKit

    class StoryViewController: UIViewController {

        var story: Stories.Story?

        @IBOutlet weak var storyTitle: UILabel!
        @IBOutlet weak var storyContent: UITextView!

        override func viewDidLoad() {
            storyTitle.text = story?.title
            storyContent.text = story?.content

In the viewDidLoad method, we are simply setting the values for our @IBOutlets using the story property. This property is set in the tableView(_:didSelectRowAt:) method in the StoriesTableViewController class.

The next class we have to create is the AlertViewController. This will be the custom class to our last scene. This is where the user can subscribe (or unsubscribe) to an interest. When users subscribe to an interest, they start receiving push notifications when new stories are added to that category.

Create a new class AlertViewController and paste in the following code:

    import UIKit

    class AlertsTableViewController: UITableViewController {

        var categories: [[String: Any]] = []

        override func viewDidLoad() {

            navigationItem.title = "Configure Alerts"

        private func getCategories() {
            guard let categories = UserDefaults.standard.array(forKey: "categories") as? [[String: Any]] else {
                self.categories = [
                    ["name": "Breaking News", "interest": "breaking_news", "subscribed": false],
                    ["name": "Sports", "interest": "sports", "subscribed": false],
                    ["name": "Politics", "interest": "politics", "subscribed": false],
                    ["name": "Business", "interest": "business", "subscribed": false],
                    ["name": "Culture", "interest": "culture", "subscribed": false],

                return self.saveCategories()

            self.categories = categories

        private func saveCategories() {
            UserDefaults.standard.set(self.categories, forKey: "categories")

        @objc func switchChanged(_ sender: UISwitch) {
            categories[sender.tag]["subscribed"] = sender.isOn        

In the viewDidLoad we call the *getCategories* method. In the *getCategories* method, we load the categories from UserDefaults. If it does not exist, we create the default categories and save them to UserDefaults. We need to maintain state on the user defaults so we know when the user subscribes and unsubscribes. When the application is restarted, the settings will still be saved.

In the saveCategories method, we just use UserDefaults to save the changes to the categories property. The switchChanged is a listener for when the switch on one of the categories is changed.


Next, in the same file, add the following extension which will conform to the UITableViewController:

    extension AlertTableViewController {
        override func numberOfSections(in tableView: UITableView) -> Int {
            return 1

        override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
            return categories.count

        override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
            let cell = tableView.dequeueReusableCell(withIdentifier: "Alert", for: indexPath)

            let category = categories[indexPath.row]

            let switchView = UISwitch(frame: .zero)
            switchView.tag = indexPath.row
            switchView.setOn(category["subscribed"] as! Bool, animated: true)
            switchView.addTarget(self, action: #selector(switchChanged(_:)), for: .valueChanged)

            cell.accessoryView = switchView
            cell.textLabel?.text = category["name"] as? String

            return cell

In the tableView(_:cellForRowAt:) method, we configure our cell and then create a UISwitch view and set that as the accessory view. We also use the tag property of the switchView to save the indexPath.row. This is so we can then tell which category’s notification switch was toggled. We then set the switchChanged method as the listener for when the switch is toggled.

The next thing we need to do is set up our application to receive and act on push notifications.

Adding push notifications to our iOS new application

Now that we have the application working, let’s integrate push notifications to the application. The first thing we need to do is turn on push notifications from the capabilities list on Xcode.

In the project navigator, select your project, and click on the Capabilities tab. Enable Push Notifications by turning the switch ON.


Next open your AlertsTableViewController class and in there import the PushNotifications library:

    import PushNotifications

In the same file, replace the switchChanged(_:) method with the following code:

    @objc func switchChanged(_ sender: UISwitch) {
        categories[sender.tag]["subscribed"] = sender.isOn

        let pushNotifications = PushNotifications.shared
        let interest = categories[sender.tag]["interest"] as! String

        if sender.isOn {
            try? pushNotifications.subscribe(interest: interest) {
        } else {
            try? pushNotifications.unsubscribe(interest: interest) {

In the code above, when the switch is turned on or off, the user subscription to the interest gets turned on or off also.

Next open the AppDelegate class and import the packages below:

    import PushNotifications
    import UserNotifications

Then in the same file add the following lines of code:

    let pushNotifications = PushNotifications.shared

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
        self.pushNotifications.start(instanceId: "PUSHER_NOTIFICATION_INSTANCE_ID")

        let center = UNUserNotificationCenter.current()
        center.delegate = self

        return true

    func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {

⚠️ Replace PUSHER_NOTIFICATION_INSTANCE_ID with the keys from your Pusher dashboard.

If you have done everything correctly, your applications should now be able to receive push notifications any time the client is subscribed to the news category and there is a new post.

However, right now, when the push notification is clicked, the application will be launched and it’ll just list the articles and not link to the specific articles. Let’s fix that by deeplinking to the specific article when the push notification is opened.

Deeplinking our iOS push notifications

When users click on our push notification, we want to direct the user to the story in the application and not just launch the app. Let’s add this feature. For this we will be implementing tips from the excellent article here.

Create a new file in Xcode called Deeplink.swift and paste the following code into the file:

    import UIKit

    enum DeeplinkType {
        case story(story: Stories.Story)

    let Deeplinker = DeepLinkManager()
    class DeepLinkManager {
        fileprivate init() {}

        private var deeplinkType: DeeplinkType?

        func checkDeepLink() {
            guard let deeplinkType = self.deeplinkType else {


            self.deeplinkType = nil

        func handleRemoteNotification(_ notification: [AnyHashable: Any]) {
            if let data = notification["data"] as? [String: AnyObject] {
                let story = Stories.Story(story: data)
                self.deeplinkType = DeeplinkType.story(story: story)
            } else {
                self.deeplinkType = nil

In our DeeplinkManager we have two methods. The first, checkDeeplink checks the deep link and gets the deeplinkType and then it calls DeeplinkNavigator.shared.proceedToDeeplink which then navigates the user to the deep link. We will create this method later.

The next method is the handleRemoteNotification method. This simply sets the deeplinkType on the DeeplinkManager class based on the notification received.

In the same file, add the following code to the bottom:

    class DeeplinkNavigator {
        static let shared = DeeplinkNavigator()

        private init() {}

        func proceedToDeeplink(_ type: DeeplinkType) {
            switch type {
            case .story(story: let story):
                if let rootVc = UIApplication.shared.keyWindow?.rootViewController as? UINavigationController {
                    let storyboard = UIStoryboard(name: "Main", bundle: nil)
                    if let vc = storyboard.instantiateViewController(withIdentifier: "StoryViewController") as? StoryViewController {
                        vc.story = story
              , sender: rootVc)

This is the code to the DeeplinkNavigator we referenced in the checkDeeplink method earlier. In this class we have one method proceedToDeeplink and this method navigates the user to a controller depending on the deeplinkType. In our case, it will navigate to the story.

Next, open the AppDelegate and add the following methods to the class:

    func applicationDidBecomeActive(_ application: UIApplication) {

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

    func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
        let notification = response.notification.request.content.userInfo

In the applicationDidBecomeActive(:) method we call our Deeplinker.checkDeepLink() method. This checks for a deeplink when the application becomes active.

The other two methods are basically listeners that get fired when there is a new push notification. In these methods, we call the handleRemoteNotification method so the push notification can be handled.

Allowing our application to connect locally

If you are going to be testing the app’s backend using a local server, then there is one last thing we need to do. Open the info.plist file and add an entry to the plist file to allow connection to our local server:


That’s it now. We can run our application. However, remember that to demo the push notifications, you will need an actual iOS device.

Here is the application one more time:



In this article, we have shown how you can use the power of interests to segment the push notifications that gets sent to your users. Hopefully, you have learnt a thing or two and you can come up with interesting ways to segment your users based on their interests.

The source code to the application is on GitHub. If you have any questions, do not hesitate to ask using the comment box below.

  • Beams

© 2018 Pusher Ltd. All rights reserved.

Pusher Limited is a company registered in England and Wales (No. 07489873) whose registered office is at 28 Scrutton Street, London EC2A 4RP.