Back to search

Create a live stocks application with push notifications for iOS

  • Neo Ighodaro
September 5th, 2018
You will need Xcode, Cocoapods and Node.js installed on your machine. Some knowledge of iOS development will be helpful.

In this article, we will see how you can build a stock market application using iOS and Swift. The prices will update in realtime as the changes to the prices occur. More importantly, though, you will be able to activate push notifications on certain stocks so you get notified when the prices of the stock changes.

When we are done, we will have an application that functions like this:

ios-stocks-demo

Prerequisites

To follow along in this tutorial you need the following things:

  • Xcode installed on your machine. Download here.
  • Know your way around the Xcode IDE.
  • Basic knowledge of the Swift programming language.
  • Basic knowledge of JavaScript.
  • Node.js installed on your machine. Download here.
  • Cocoapods installed on your machine. Install here.
  • A Pusher account. Create one here.

Let’s get started.

Creating your iOS project

The first thing we need to do is create the project in Xcode. Launch Xcode and click Create a new Xcode project.

ios-stocks-welcome-xcode

From the next screen, select Single View App > Next then give the project a name. Let’s name it something really creative, like Stocks.

ios-stocks-new-app

Installing dependencies using Cocoapods

Now that we have our project set up, we need to add some external libraries to the project. These libraries will be used for various functions like push notifications and HTTP requests.

First close Xcode. Next, create a new Podfile in the root of your project and paste the following code:

    # File: ./Podfile
    platform :ios, '11.0'

    target 'Stocks' do
      use_frameworks!
      pod 'Alamofire', '~> 4.7.3'
      pod 'PusherSwift', '~> 6.1.0'
      pod 'PushNotifications', '~> 1.0.1'
      pod 'NotificationBannerSwift', '~> 1.6.3'
    end

Above, we are using the Podfile to define the libraries our project will be depending on to work. Here are the libraries we have:

Now that we have defined the dependencies, let’s install them. Open your terminal and cd to the project root and run this command:

    $ pod update

This will install all the dependencies listed in the Podfile. We are using the update command because we want the latest versions of the libraries, which may have changed since writing this article.

When the installation is complete, we will have a new Stocks.xcworkspace file in the root of the project. Going forward, we will have to open our iOS project using this Xcode workspace file.

Building the iOS application

The first thing we want to do is consider how the entire service will work. We will build two applications. One will be the iOS application and the other will be a backend, which will be built with JavaScript (Node.js).

In this section, we will start with the iOS application. Open the Stocks.xcworkspace file in Xcode and let’s start building the iOS app.

Creating the settings class

The first thing we are going to do is create a notification settings class. This will be responsible for storing the notification settings for a device. When you subscribe for push notifications on a certain stock, we will store the setting using this class so that the application is aware of the stocks you turned on notifications for.

Create a new Swift class named STNotificationSettings and paste the following code:

    // File: ./Stocks/STNotificationSettings.swift
    import Foundation

    class STNotificationSettings: NSObject {
        static let KEY = "ST_NOTIFICATIONS"
        static let shared = STNotificationSettings()

        private override init() {}

        private var settings: [String: Bool] {
            get {
                let key = STNotificationSettings.KEY

                if let settings = UserDefaults.standard.object(forKey: key) as? [String: Bool] {
                    return settings
                }

                return [:]
            }
            set(newValue) {
                var settings: [String: Bool] = [:]

                for (k, v) in newValue {
                    settings[k.uppercased()] = v
                }

                UserDefaults.standard.set(settings, forKey: STNotificationSettings.KEY)
            }
        }

        func enabled(for stock: String) -> Bool {
            if let stock = settings.first(where: { $0.key == stock.uppercased() }) {
                return stock.value
            }

            return false
        }

        func save(stock: String, enabled: Bool) {
            settings[stock.uppercased()] = enabled
        }
    }

In the class above, we have a static property, key, that is just used as the key for the preference that will hold all our settings. This key will be used for lookup and storage of the settings in the iOS file system.

We also have a shared static property, which holds an instance of the class. We want this class to be instantiated once. This is also why we have made our init method private.

Next, we have the settings property. This is a computed property that provides a getter and a setter to retrieve and set other properties and values indirectly. The getter just retrieves the settings data from the filesystem, while the setter saves the settings to the filesystem.

We have two methods in the class, enabled(for:) and save(stock:enabled:). The first one checks if push notifications are enabled for a stock, while the second saves the setting for a stock.

That’s all for the settings class.

Creating our view controller

The next thing we want to do is create the view controller. We will start by creating a view controller class, then we will create a view controller in the storyboard. We will then connect the class to the storyboard.

Create a new table view controller named StocksTableViewController and replace the contents with this:

    // File: ./Stocks/StocksTableViewController.swift
    import UIKit
    import Alamofire
    import PusherSwift
    import PushNotifications
    import NotificationBannerSwift

    class StocksTableViewController: UITableViewController {
    }

We will get back to this class, but for now, leave it and open the Main.storyboard file. In the storyboard, drag a new table view controller to the canvas. Next, drag the arrow from the old view controller that was in the storyboard to the new table view controller and then delete the old view controller.

ios-stocks-tableview

Next, open the Identity Inspector and set the custom class for the table view controller to StocksTableViewController. This will connect the class we created earlier to this table view controller we have on the storyboard.

ios-stocks-identity-inspector

Finally, set the reuse Identifier on the cell to ‘default’. We will not be using the cells that come with this table view controller, but we still need to set the identifier so Swift does not whine about it.

ios-stocks-identifier-default

Next, open the StocksTableViewController class and let's start adding logic to it. Update the class as seen below:

    // [...]

    class StocksTableViewController: UITableViewController {
        var stocks: [Stock] = []

        var pusher: Pusher!
        let pushNotifications = PushNotifications.shared
        let notificationSettings = STNotificationSettings.shared

        override func viewDidLoad() {
            super.viewDidLoad()
        }

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

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

        override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
            let cell = tableView.dequeueReusableCell(withIdentifier: "stock", for: indexPath) as! StockCell
            cell.stock = stocks[indexPath.row]
            return cell
        }
    }

Above we have a few properties we have defined:

  • stocks - this holds an array of Stock items. This is the data that will be displayed on each table cell. The Stock is a model we have not created but will later on.
  • pusher - this holds the PusherSwift library instance. We will use it to connect to Pusher and update the cells in realtime.
  • pushNotifications - this holds a singleton of the PushNotifications library. We will use this to subscribe and unsubscribe from interests.
  • notificationSettings - this holds a singleton of the STNotificationSettings class. We will use this to get the setting for each stock when necessary.

The methods we have defined above are standard with iOS development and should not need explanation.

However, in the tableView(_:cellForRowAt:) method, we do something a little different. We get an instance of StockCell, which we have not created, and then assign a Stock item to the cell. Later on, we will see how we can use the didSet property observer to neatly populate the cell.

In the same class, add the following methods:

    // [...]

    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        let cell = tableView.cellForRow(at: indexPath) as! StockCell
        if let stock = cell.stock {
            showNotificationSettingAlert(for: stock)
        }
    }

    private func showNotificationSettingAlert(for stock: Stock) {
        let enabled = notificationSettings.enabled(for: stock.name)
        let title = "Notification settings"
        let message = "Change the notification settings for this stock. What would you like to do?"

        let alert = UIAlertController(title: title, message: message, preferredStyle: .actionSheet)

        let onTitle = enabled ? "Keep on" : "Turn on notifications"
        alert.addAction(UIAlertAction(title: onTitle, style: .default) { [unowned self] action in
            guard enabled == false else { return }
            self.notificationSettings.save(stock: stock.name, enabled: true)

            let feedback = "Notfications turned on for \(stock.name)"
            StatusBarNotificationBanner(title: feedback, style: .success).show()

            try? self.pushNotifications.subscribe(interest: stock.name.uppercased())
        })

        let offTitle = enabled ? "Turn off notifications" : "Leave off"
        let offStyle: UIAlertActionStyle = enabled ? .destructive : .cancel
        alert.addAction(UIAlertAction(title: offTitle, style: offStyle) { [unowned self] action in
            guard enabled else { return }
            self.notificationSettings.save(stock: stock.name, enabled: false)

            let feedback = "Notfications turned off for \(stock.name)"
            StatusBarNotificationBanner(title: feedback, style: .success).show()

            try? self.pushNotifications.unsubscribe(interest: stock.name.uppercased())
        })

        present(alert, animated: true, completion: nil)
    }

    // [...]

Above, we added two new methods:

  • tableView(_:didSelectRowAt:) - this is a default table view controller method that is fired when a row is selected in the table. In this method, we get the row that was tapped, and then show an alert that we can use to configure the push notification setting for that stock.
  • showNotificationSettingAlert - this is invoked from the method above. It contains all the actual logic required to display the notification settings alert. The alert will look like this when the application is ready:

ios-stocks-alert-demo

Next, let’s update the viewDidLoad() method. Replace the viewDidLoad() method with the following code:

    // [...]

    override func viewDidLoad() {
        super.viewDidLoad()

        fetchStockPrices()

        tableView.separatorInset.left = 0
        tableView.backgroundColor = UIColor.black

        let customCell = UINib(nibName: "StockCell", bundle: nil)
        tableView.register(customCell, forCellReuseIdentifier: "stock")

        pusher = Pusher(
            key: AppConstants.PUSHER_APP_KEY,
            options: PusherClientOptions(host: .cluster(AppConstants.PUSHER_APP_CLUSTER))
        )

        let channel = pusher.subscribe("stocks")
        let _ = channel.bind(eventName: "update") { [unowned self] data in
            if let data = data as? [[String: AnyObject]] {
                if let encoded = try? JSONSerialization.data(withJSONObject: data, options: .prettyPrinted) {
                    if let stocks = try? JSONDecoder().decode([Stock].self, from: encoded) {
                        self.stocks = stocks
                        self.tableView.reloadData()
                    }
                }
            }
        }

        pusher.connect()
    }

    // [...]

Above, we do a couple of things. First, we call the fetchStockPrices() method, which we will define later, to fetch all the stock prices from a backend API. Then we changed the background color of the table view to black.

We registered the non-existent custom cell, StockCell, which we referenced earlier in the article. We finally used the pusher instance to connect to a Pusher channel, stock, and also bind to the update event on that channel. When the event is fired, we decode the data into the stocks property using Codable and reload the table to show the new changes.

Related: Swift 4 decoding JSON using Codable

Below the showNotificationSettingAlert(for:) method in the same class, add the following method:

    // [...]

    private func fetchStockPrices() {
        Alamofire.request(AppConstants.ENDPOINT + "/stocks")
            .validate()
            .responseJSON { [unowned self] resp in
                guard let data = resp.data, resp.result.isSuccess else {
                    let msg = "Error fetching prices"
                    return StatusBarNotificationBanner(title: msg, style: .danger).show()
                }

                if let stocks = try? JSONDecoder().decode([Stock].self, from: data) {
                    self.stocks = stocks
                    self.tableView.reloadData()
                }
            }
    }

    // [...]

The method above was invoked in the viewDidLoad() method above. It fetches all the stocks from the API using the Alamofire library and then decodes the response to the stocks property using Codable. After this, the table view is reloaded to show the updated stocks data.

That’s all for this class.

We referenced a few non-existent classes in the StocksTableViewController though, let’s create them.

Creating supporting classes

Create a new AppConstants Swift file and paste the following code:

    import Foundation

    struct AppConstants {
        static let ENDPOINT = "http://127.0.0.1:5000" // Or use your ngrok HTTPS URL
        static let PUSHER_APP_KEY = "PASTE_PUSHER_APP_KEY_HERE"
        static let PUSHER_APP_CLUSTER = "PASTE_PUSHER_APP_CLUSTER_HERE"
        static let BEAMS_INSTANCE_ID = "PASTE_PUSHER_BEAMS_INSTANCE_ID_HERE"
    }

The struct above serves as our configuration file. It allows us to define one true source of configuration values that we need for the application. At this point, you should create your Pusher Channels and Pusher Beams application if you haven’t already and paste the credentials above.

Next, let’s define the Stock model. Create a new Stock Swift file and paste the following code:

    import Foundation

    struct Stock: Codable {
        let name: String
        let price: Float
        let percentage: String
    }

Above we have our Stock model which extends the Codable protocol. You can read more about it Codable here.

Creating our custom cell

We referenced the StockCell several times above, so let’s create our custom cell now. We are creating this separately so it is easy to manage and everything is modular.

First, create a new Empty view in Xcode as seen below:

ios-stocks-empty-view

Next, drag a new table view cell into the empty canvas. We will be using this as our custom cell. Next, create a new Swift file named StockCell and paste the following code into it:

    import UIKit

    class StockCell: UITableViewCell {

        var stock: Stock? {
            didSet {
                if let stock = stock {
                    stockName.text = stock.name
                    stockPrice.text = "\(stock.price)"
                    stockPercentageChange.text = "\(stock.percentage)"
                    percentageWrapper.backgroundColor = stock.percentage.first == "+"
                        ? UIColor.green.withAlphaComponent(0.7)
                        : UIColor.red
                }
            }
        }

        @IBOutlet private weak var stockName: UILabel!
        @IBOutlet private weak var stockPrice: UILabel!
        @IBOutlet private weak var percentageWrapper: UIView!
        @IBOutlet private weak var stockPercentageChange: UILabel!

        override func awakeFromNib() {
            super.awakeFromNib()
            percentageWrapper.layer.cornerRadius = 5
        }
    }

In the cell class above, we have the stock property which holds a Stock model. The property has the didSet property observer. So anytime the stock property is set, the code in the observer is run. In the observer, we set the private @IBOutlet properties.

This makes our code neat and organized because the StockTableViewController does not have to care about how the stock is handled, it just sets the Stock model to the StockCell and the cell handles the rest.

We have an awakeFromNib() method which is called when the cell is created. We use this to set a corner radius to the view holding the percentage change text.

Next, open the StockCell.xib view, and set the custom class of the view to StockCell. Then design the cell as seen below:

ios-stocks-stockcell

We have used constraints to make sure each item stays in place. You can decide to do the same if you wish.

When you are done designing, connect the labels and views to your StockCell class using the Assistant Editor. This will establish the link between the items in the view and the StockCell's @IBOutlets.

ios-stocks-link-stockcell

Updating the AppDelegate and turning on push notifications

Open the AppDelegate file and replace the contents with the following:

    import UIKit
    import PushNotifications

    @UIApplicationMain
    class AppDelegate: UIResponder, UIApplicationDelegate {
        var window: UIWindow?
        let pushNotifications = PushNotifications.shared

        func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
            pushNotifications.start(instanceId: AppConstants.BEAMS_INSTANCE_ID)
            pushNotifications.registerForRemoteNotifications()
            return true
        }

        func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
            pushNotifications.registerDeviceToken(deviceToken)
        }
    }

In the application(_:didFinishLaunchingWithOptions:) method, we start the PushNotifications library and then we register the device for remote notifications. In the application(_:didRegisterForRemoteNotificationsWithDeviceToken:) method, we register the device token with Pusher Beams.

Next, turn on the Push Notification capability for our application as seen below:

ios-stocks-enable-push-notifications

This will add a Stocks.entitlement file in your project root.

One last thing we need to do before we are done with the iOS application is allowing the application load data from arbitrary URLs. By default, iOS does not allow this, and it should not. However, since we are going to be testing locally, we need this turned on temporarily. Open the info.plist file and update it as seen below:

ios-stocks-enable-app-transport

Now, our app is ready, but we still need to create the backend in order for it to work. Let’s do just that.

Building the backend API

Our API will be built using Node.js. The backend will be responsible for providing the available stocks and also sending push notifications when there are changes. It will also push changes to Pusher Channels when there are changes in the stock price. We will be simulating the stock prices for instant results, but you can choose to use a live API.

Create a new directory for your backend application. Inside this project directory, create a new package.json file and paste the following code:

    {
      "name": "webapi",
      "version": "1.0.0",
      "main": "index.js",
      "dependencies": {
        "body-parser": "^1.18.3",
        "express": "^4.16.3",
        "pusher": "^2.1.3",
        "@pusher/push-notifications-server": "1.0.0"
      }
    }

Next, open a terminal window, cd to the application directory and run the command below:

    $ npm install

This will install the dependencies in the package.json file. Next, create a new config.js file, and paste the following code:

    module.exports = {
      appId: 'PASTE_PUSHER_CHANNELS_APPID',
      key: 'PASTE_PUSHER_CHANNELS_KEY',
      secret: 'PASTE_PUSHER_CHANNELS_SECRET',
      cluster: 'PASTE_PUSHER_CHANNELS_CLUSTER',
      secretKey: 'PASTE_PUSHER_BEAMS_SECRET',
      instanceId: 'PASTE_PUSHER_BEAMS_INSTANCEID'
    };

Above, we have the configuration values for our Pusher instances. Replace the placeholders above with the keys from your Pusher dashboard.

Finally, create a new file, index.js and paste the following code:

    const express = require('express');
    const bodyParser = require('body-parser');
    const path = require('path');
    const Pusher = require('pusher');
    const PushNotifications = require('@pusher/push-notifications-server');

    const app = express();
    const pusher = new Pusher(require('./config.js'));
    const pushNotifications = new PushNotifications(require('./config.js'));

    function generateRandomFloat(min, max) {
      return parseFloat((Math.random() * (max - min) + min).toFixed(2));
    }

    function getPercentageString(percentage) {
      let operator = percentage < 0 ? '' : '+';
      return `${operator}${percentage}%`;
    }

    function loadStockDataFor(stock) {
      return {
        name: stock,
        price: generateRandomFloat(0, 1000),
        percentage: getPercentageString(generateRandomFloat(-10, 10))
      };
    }

    app.get('/stocks', (req, res) => {
      let stocks = [
        loadStockDataFor('AAPL'),
        loadStockDataFor('GOOG'),
        loadStockDataFor('AMZN'),
        loadStockDataFor('MSFT'),
        loadStockDataFor('NFLX'),
        loadStockDataFor('TSLA')
      ];

      stocks.forEach(stock => {
        let name = stock.name;
        let percentage = stock.percentage.substr(1);
        let verb = stock.percentage.charAt(0) === '+' ? 'up' : 'down';

        pushNotifications.publish([stock.name], {
          apns: {
            aps: {
              alert: {
                title: `Stock price change: "${name}"`,
                body: `The stock price of "${name}" has gone ${verb} by ${percentage}.`
              }
            }
          }
        });
      });

      pusher.trigger('stocks', 'update', stocks);

      res.json(stocks);
    });

    app.listen(5000, () => console.log('Server is running'));

Above, we have a simple Express application. We have three helper functions:

  • generateRandomFloat - generates a random float between two numbers.
  • getPercentageString - uses a passed number to generate a string that will be shown on the table cell, for example, +8.0%.
  • loadStockDataFor - loads random stock data for a stock passed to it.

After the helpers, we have the /stocks route. In here we generate a list of stocks, and for each stock, we send a push notification about the change in price. The stocks name serves as the interest for each stock. This means that subscribing to the AAPL interest, for instance, will subscribe to receiving push notifications for the AAPL stock.

Next, we trigger an event, update, on the stocks channel, so all other devices can pick up the recent changes. Lastly, we return the generated list of stocks and we add the code that starts the server on port 5000.

To get the server started, run the following command on your terminal:

    $ node index

ios-stocks-node

Testing the application

Now that we have built the backend and started the Node.js server, you can now run the iOS application. Your stocks will be displayed on the screen. However, if you want to test push notifications, you will need a real iOS device, and you will need to follow the following instructions.

First, you will need to install ngrok. This tool is used to expose local running web servers to the internet. Follow the instructions on their website to download and install ngrok.

Once you have it installed, run the following command in another terminal window:

    $ ngrok http 8000

ios-stocks-ngrok

Make sure your Node.js server is still running before executing the command above.

Now we have a Forwarding URL we can use in our application. Copy the HTTPS forwarding URL and replace the ENDPOINT value in AppConstants.swift with the URL.

Now, run the application on your device. Once it has loaded, tap on a stock and turn on notification for that stock then minimize the application and visit http://localhost:5000/stocks on your web browser. This will simulate a change in the stock prices and you should get a push notification for the stock you subscribed to.

ios-stocks-demo

Conclusion

In this article, we have been able to create a stocks application with push notification using Pusher Channels and Pusher Beams.

The source code to the entire application is available on GitHub.

  • Channels
  • 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.