In the first part of this article, we started developing our cryptocurrency alert application. We developed the backend of the application that will power the iOS application. As it stands, our backend application can return settings for a device based on its UUID, save the settings for a device based on its UUID and also it can figure out what devices to send push notifications to when the currencies update.
In this part, we will focus on creating the iOS application using Swift and Xcode.
To follow along you need the following requirements:
We already started out by building the backend of the application using Laravel. So next, we will build the iOS application using Swift. If you want to test the push notifications then you will need to run the application on a live device.
For the client app, the iOS application, we will create a simple list that will display the available currencies and the current prices to the dollar. Whenever the price of the cryptocurrency changes, we will trigger an event using Pusher Channels so the prices are updated.
From the application, you will be able to set a minimum and maximum price change when you want to be alerted. For instance, you can configure the application to send a push notification to the application when the price of one Etherium (ETH) goes below $500. You can also configure the application to receive a notification when the price of Bitcoin goes above $5000.
When we are done with the application, here's how the application will look:
Let’s get started.
Launch Xcode and click Create a new Xcode project. Select Single View App and click Next. Enter your Product Name, we will call our project cryptoalat, and select Swift from the Language options. You can also change any other detail you wish to on the screen then click Next.
Now you have your Xcode project. Close Xcode and open a terminal window. cd
to the iOS project directory in terminal and run the command below to create a Podfile:
$ pod init
The Podfile is a specification that describes the dependencies of the targets of one or more Xcode projects. The file should simply be named Podfile. All the examples in the guides are based on CocoaPods version 1.0 and onwards. - Cocoapods Guides
This will generate a new file called Podfile
in the root of your project. Open this file in any editor and update the file as seen below:
1// File: Podfile 2 platform :ios, '11.0' 3 4 target 'cryptoalat' do 5 use_frameworks! 6 7 pod 'Alamofire', '~> 4.7.2' 8 pod 'PushNotifications', '~> 0.10.8' 9 pod 'PusherSwift', '~> 6.1.0' 10 pod 'NotificationBannerSwift', '~> 1.6.3' 11 end
If you used a project name other than cryptoalat, then change it in the Podfile to match your project’s target name.
Go to terminal and run the command below to install your dependencies:
$ pod install
When the installation is complete, you will have a *.xcworkspace
file in the root of your project. Open this file in Xcode and let’s start developing our cryptocurrency alert application.
The first thing we need to do is design our storyboard for the application. This is what we want the storyboard to look like when we are done.
Open the Main.storyboard
file and design as seen above.
Above we have three scenes. The first scene, which is the entry point, is the launch scene. We then draw a manual segue with an identifier called Main. Then we set the segue Kind to Present Modally. This will present the next scene which is a navigation view controller. Navigation controllers already have an attached root view controller by default.
We will use this attached view controller, which is a TableViewController
, as the main view for our application. It’ll list the available currencies and show us a text field that allows us to change the setting for that currency when it is tapped.
On the third scene, we set the reuse identifier of the cells to coin and we drag two labels to the prototype cell. The first label will be for the coin name and the second label will be for the price.
Now that we have the scenes, let’s create some controllers and view classes and connect them to our storyboard scenes.
In Xcode, create a new class LaunchViewController
and paste the contents of the file below into it:
1import UIKit 2 3 class LaunchViewController: UIViewController { 4 5 override func viewDidAppear(_ animated: Bool) { 6 super.viewDidAppear(animated) 7 8 SettingsService.shared.loadSettings { 9 self.routeToMainController() 10 } 11 } 12 13 fileprivate func routeToMainController() { 14 performSegue(withIdentifier: "Main", sender: self) 15 } 16 }
Set the controller as the custom class for the first scene in the
Main.storyboard
file.
In the code, we load the settings using a SettingsService
class we will create later. When the settings are loaded for the device, we then call the routeToMainController
method, which routes the application to the main controller using the Main segue we created earlier.
The next controller we will be creating will be the CoinsTableViewController
. This will be the controller that will be tied to the third scene which is the main scene.
Create the CoinsTableViewController
and replace the contents with the following code;
1import UIKit
2 import PusherSwift
3 import NotificationBannerSwift
4
5 struct Coin {
6 let name: String
7 let rate: Float
8 }
9
10 class CoinsTableViewController: UITableViewController {
11
12 var coins: [Coin] = []
13
14 override func viewDidLoad() {
15 super.viewDidLoad()
16 }
17
18 override func numberOfSections(in tableView: UITableView) -> Int {
19 return 1
20 }
21
22 override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
23 return coins.count
24 }
25
26 override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
27 let coin = coins[indexPath.row]
28 let cell = tableView.dequeueReusableCell(withIdentifier: "coin", for: indexPath) as! CoinTableViewCell
29
30 cell.name.text = "1 \(coin.name) ="
31 cell.amount.text = "$\(String(coin.rate))"
32
33 return cell
34 }
35 }
Set the controller as the custom class for the first scene in the
Main.storyboard
file.
Above we have defined the Coin
struct and it has a name
and rate
property. We have the controller which we define the coins
property as an array of Coin
s. We then have some boilerplate code that comes with creating a table view controller.
The numberOfSections
method specifies how many sections the table will have. In the first tableView
method, we return the number of coins
available and in the second tableView
method, we define how we want each row to be handled.
If you noticed in the code above, we referenced a CoinTableViewCell
as the class for each row in the last tableView
method. Let’s create that.
Create a CoinTableViewCell
class and paste the following code into it:
1class CoinTableViewCell: UITableViewCell { 2 @IBOutlet weak var name: UILabel! 3 @IBOutlet weak var amount: UILabel! 4 }
Open the Main.storyboard
file and set the class as the custom class for the prototype cell in the third scene of the Main.storyboard
file. When you have set the class, connect the @IBOutlet
s as specified in the cell class above.
The next class we need to create is the SettingsService
. This class will be responsible for updating and fetching the settings for the device.
Create a new SettingsService
class and replace the contents with the following code:
1import Foundation
2 import Alamofire
3 import NotificationBannerSwift
4
5 class SettingsService {
6 static let key = "CryptoAlat"
7 static let shared = SettingsService()
8
9 var settings: Settings? {
10 get {
11 return self.getCachedSettings()
12 }
13 set(settings) {
14 if let settings = settings {
15 self.updateCachedSettings(settings)
16 }
17 }
18 }
19
20 private init() {}
21
22 func loadSettings(completion: @escaping() -> Void) {
23 fetchRemoteSettings { settings in
24 guard let settings = settings else {
25 return self.saveSettings(self.defaultSettings()) { _ in
26 completion()
27 }
28 }
29
30 self.updateCachedSettings(settings)
31 completion()
32 }
33 }
34
35 fileprivate func defaultSettings() -> Settings {
36 return Settings(
37 btc_min_notify: 0,
38 btc_max_notify: 0,
39 eth_min_notify: 0,
40 eth_max_notify: 0
41 )
42 }
43
44 func saveSettings(_ settings: Settings, completion: @escaping(Bool) -> Void) {
45 updateRemoteSettings(settings, completion: { saved in
46 if saved {
47 self.updateCachedSettings(settings)
48 }
49
50 completion(saved)
51 })
52 }
53
54 fileprivate func fetchRemoteSettings(completion: @escaping (Settings?) -> Void) {
55 guard let deviceID = AppConstants.deviceIDFormatted else {
56 return completion(nil)
57 }
58
59 let url = "\(AppConstants.API_URL)?u=\(deviceID)"
60 Alamofire.request(url).validate().responseJSON { resp in
61 if let data = resp.data, resp.result.isSuccess {
62 let decoder = JSONDecoder()
63 if let settings = try? decoder.decode(Settings.self, from: data) {
64 return completion(settings)
65 }
66 }
67
68 completion(nil)
69 }
70 }
71
72 fileprivate func updateRemoteSettings(_ settings: Settings, completion: @escaping(Bool) -> Void) {
73 guard let deviceID = AppConstants.deviceIDFormatted else {
74 return completion(false)
75 }
76
77 let params = settings.toParams()
78 let url = "\(AppConstants.API_URL)?u=\(deviceID)"
79 Alamofire.request(url, method: .post, parameters: params).validate().responseJSON { resp in
80 guard resp.result.isSuccess, let res = resp.result.value as? [String: String] else {
81 return StatusBarNotificationBanner(title: "Failed to update settings.", style: .danger).show()
82 }
83
84 completion((res["status"] == "success"))
85 }
86 }
87
88 fileprivate func updateCachedSettings(_ settings: Settings) {
89 if let encodedSettings = try? JSONEncoder().encode(settings) {
90 UserDefaults.standard.set(encodedSettings, forKey: SettingsService.key)
91 }
92 }
93
94 fileprivate func getCachedSettings() -> Settings? {
95 let defaults = UserDefaults.standard
96 if let data = defaults.object(forKey: SettingsService.key) as? Data {
97 let decoder = JSONDecoder()
98 if let decodedSettings = try? decoder.decode(Settings.self, from: data) {
99 return decodedSettings
100 }
101 }
102
103 return nil
104 }
105 }
Above we have the SettingsService
. The first method loadSettings
loads the settings from the API and then saves it locally. If there is no setting remotely, it calls the defaultSettings
method and saves the response to the API.
The saveSettings
method saves the Settings
remotely using updateRemoteSettings
and then locally using updateCachedSettings
. The fetchRemoteSettings
gets the settings from the API and decodes the response using the Swift decodable API.
Next, let’s define the Settings
struct and have it extend Codable
. In the same file for the SettingsService
, add this above the SettingsService
class definition:
1struct Settings: Codable {
2 var btc_min_notify: Int?
3 var btc_max_notify: Int?
4 var eth_min_notify: Int?
5 var eth_max_notify: Int?
6
7 func toParams() -> Parameters {
8 var params: Parameters = [:]
9
10 if let btcMin = btc_min_notify { params["btc_min_notify"] = btcMin }
11 if let btcMax = btc_max_notify { params["btc_max_notify"] = btcMax }
12 if let ethMin = eth_min_notify { params["eth_min_notify"] = ethMin }
13 if let ethMax = eth_max_notify { params["eth_max_notify"] = ethMax }
14
15 return params
16 }
17 }
Above we have a simple Settings
struct that conforms to Codable
. We also have a toParams
method that converts the properties to a Parameters
type so we can use it with Alamofire when making requests.
One last class we need to create is AppConstants
. We will use this class to keep all the data that we expect to remain constant and unchanged throughout the lifetime of the application.
Create a AppConstants
file and paste the following code:
1import UIKit
2
3 struct AppConstants {
4 static let API_URL = "http://127.0.0.1:8000/api/settings"
5 static let deviceID = UIDevice.current.identifierForVendor?.uuidString
6 static let deviceIDFormatted = AppConstants.deviceID?.replacingOccurrences(of: "-", with: "_").lowercased()
7 static let PUSHER_INSTANCE_ID = "PUSHER_BEAMS_INSTANCE_ID"
8 static let PUSHER_APP_KEY = "PUSHER_APP_KEY"
9 static let PUSHER_APP_CLUSTER = "PUSHER_APP_CLUSTER"
10 }
Replace the
PUSHER_*
keys with the values gotten from the Pusher Channels and Beams dashboard.
Now that we have defined the settings service, let’s update our controller so the user can set the minimum and maximum prices for each currency.
Open the CoinsTableViewController
class and add the following method:
1override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
2 let coin = coins[indexPath.row]
3
4 var minTextField: UITextField?
5 var maxTextField: UITextField?
6
7 let title = "Manage \(coin.name) alerts"
8 let message = "Notification will be sent to you when price exceeds or goes below minimum and maximum price. Set to zero to turn off notification."
9
10 let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
11
12 alert.addTextField { textfield in
13 minTextField = textfield
14 textfield.placeholder = "Alert when price is below"
15 }
16
17 alert.addTextField { textfield in
18 maxTextField = textfield
19 textfield.placeholder = "Alert when price is above"
20 }
21
22 alert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil))
23
24 alert.addAction(UIAlertAction(title: "Save", style: .default, handler: { action in
25 guard let minPrice = minTextField?.text, let maxPrice = maxTextField?.text else {
26 return StatusBarNotificationBanner(title: "Invalid min or max price", style: .danger).show()
27 }
28
29 var btcMin: Int?, btcMax: Int?, ethMin: Int?, ethMax: Int?
30
31 switch coin.name {
32 case "BTC":
33 btcMin = Int(minPrice)
34 btcMax = Int(maxPrice)
35 case "ETH":
36 ethMin = Int(minPrice)
37 ethMax = Int(maxPrice)
38 default:
39 return
40 }
41
42 let settings = Settings(
43 btc_min_notify: btcMin,
44 btc_max_notify: btcMax,
45 eth_min_notify: ethMin,
46 eth_max_notify: ethMax
47 )
48
49 SettingsService.shared.saveSettings(settings, completion: { saved in
50 if saved {
51 StatusBarNotificationBanner(title: "Saved successfully").show()
52 }
53 })
54 }))
55
56 present(alert, animated: true, completion: nil)
57 }
The method above is automatically called when a row is selected. In this method, we display a UIAlertController
with two text fields for the minimum price and the maximum price. When the prices are submitted, the SettingsService
we created earlier takes care of updating the values both locally and remotely.
Open the CoinsTableViewController
and add the pusher
property to the class as seen below:
var pusher: Pusher!
Then replace the viewDidLoad
method with the following code:
1override func viewDidLoad() {
2 super.viewDidLoad()
3
4 pusher = Pusher(
5 key: AppConstants.PUSHER_APP_KEY,
6 options: PusherClientOptions(host: .cluster(AppConstants.PUSHER_APP_CLUSTER))
7 )
8
9 let channel = pusher.subscribe("currency-update")
10
11 let _ = channel.bind(eventName: "currency.updated") { data in
12 if let data = data as? [String: [String: [String: Float]]] {
13 guard let payload = data["payload"] else { return }
14
15 self.coins = []
16
17 for (coin, deets) in payload {
18 guard let currentPrice = deets["current"] else { return }
19 self.coins.append(Coin(name: coin, rate: currentPrice))
20 }
21
22 Dispatch.main.async {
23 self.tableView.reloadData()
24 }
25 }
26 }
27
28 pusher.connect()
29 }
In the code above, we are using the Pusher Swift SDK to subscribe to our currency-update
Pusher Channel. We then subscribe to the currency.updated
event on that channel. Whenever that event is triggered, we refresh the price of the cryptocurrency in realtime.
To add push notification support, open the AppDelegate
class and replace the contents with the following:
1import UIKit
2 import PushNotifications
3
4 @UIApplicationMain
5 class AppDelegate: UIResponder, UIApplicationDelegate {
6
7 var window: UIWindow?
8
9 func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
10 PushNotifications.shared.start(instanceId: AppConstants.PUSHER_INSTANCE_ID)
11 PushNotifications.shared.registerForRemoteNotifications()
12 return true
13 }
14
15 func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
16 PushNotifications.shared.registerDeviceToken(deviceToken) {
17 if let deviceID = AppConstants.deviceIDFormatted {
18 try? PushNotifications.shared.subscribe(interest: "\(deviceID)_eth_changed")
19 try? PushNotifications.shared.subscribe(interest: "\(deviceID)_btc_changed")
20 }
21 }
22 }
23 }
In the class above, we use the Pusher Beams Swift SDK to register the device for push notifications. We then subscribe to the *_eth_changed
and *_btc_changed
interests, where *
is the device’s unique UUID.
Now that we have completed the logic for the application, let’s enable push notifications on the application in Xcode.
In the project navigator, select your project, and click on the Capabilities tab. Enable Push Notifications by turning the switch ON.
This will create an entitlements file in the root of your project. With that, you have provisioned your application to fully receive push notifications.
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 all. We can run our application. However, remember that to demo the push notifications, you will need an actual iOS device as simulators cannot receive push notifications. If you are using a physical device, you’ll need to expose your local API using Ngrok and then change the API_URL
In AppConstants
.
Anytime you want to update the currency prices, run the command below manually in your Laravel application:
$ php artisan schedule:run
Here is a screen recording of the application in action:
In this article, we have been able to see how easy it is to create a cryptocurrency alert website using Laravel, Swift, Pusher Channels and Pusher Beams. The source code to the application built in this article is available on GitHub.