Create a live stocks application with push notifications for iOS
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:
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.
From the next screen, select Single View App > Next then give the project a name. Let’s name it something really creative, like Stocks.
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:
- Alamofire - an HTTP networking library written in Swift.
- PusherSwift - the iOS library for Pusher.
- PushNotifications - Swift SDK for Pusher Beams.
- NotificationBannerSwift - easy way to display app notification banners in iOS apps.
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.
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.
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.
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 ofStock
items. This is the data that will be displayed on each table cell. TheStock
is a model we have not created but will later on.pusher
- this holds thePusherSwift
library instance. We will use it to connect to Pusher and update the cells in realtime.pushNotifications
- this holds a singleton of thePushNotifications
library. We will use this to subscribe and unsubscribe from interests.notificationSettings
- this holds a singleton of theSTNotificationSettings
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:
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:
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:
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 @IBOutlet
s.
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:
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:
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
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
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.
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.
5 September 2018
by Neo Ighodaro