Build a to-do list app for the Apple watch
You will need Xcode and Node installed on your machine.
Introduction
In this post, you will build a realtime iOS application involving a watch and an iPhone. We will be creating a realtime todo watch app with an accompanying iPhone app.
Here is how your application will look afterward:
Prerequisites
To follow along in this article, you need the following:
- A Pusher Channel app. You can create one here (take note of your app keys as you’ll need them later).
- Xcode installed on your machine. You can download here.
- Node JS and NPM (Node Package Manager) installed on your machine. Check here for the latest releases.
Let’s get started.
Building the backend
Our app will be powered by a local Node.js server. Create a folder on your machine, say todo-backend
. Create a package.json
file inside it and set it up like so:
// File: ./package.json
{
"name": "todo",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"body-parser": "^1.18.3",
"express": "^4.16.4",
"pusher": "^2.2.0"
}
}
This file contains a description to our app and the dependencies we will make use of. Next, create an index.js
file and paste this:
// File: ./index.js
const express = require('express');
const bodyParser = require('body-parser');
const Pusher = require('pusher');
const app = express();
app.use(bodyParser.json());
var pusher = new Pusher({
appId: 'PUSHER_APP_ID',
key: 'PUSHER_APP_KEY',
secret: 'PUSHER_APP_SECRET',
cluster: 'PUSHER_APP_CLUSTER'
});
app.post('/addItem', function (req, res) {
pusher.trigger('todo', 'addItem', { text: req.body.value});
res.send(200);
})
app.listen(process.env.PORT || 5000);
Here, we added a single endpoint that will add todo items. Our watch app will make use of this API and send a todo trigger to Pusher.
Replace
PUSHER_APP_*
placeholders with the credentials from your dashboard
Run the npm install
command in the directory to install the Node modules after which you run your server using:
$ node index.js
The localhost runs on port 5000
.
Building the iOS app
Creating a project
Open your Xcode and create a new project. You will be presented with a template wizard. Choose watchOS
and select iOS App with WatchKit App.
Next, you enter a product name - say TodoWatchApp
, team, and organization details. Choose Swift as the default language and choose Next. You will then be asked to choose the directory where you want the project to be created. After this, you’re good to go!
Here is how the directory of your project should look.
This is quite different from when you are developing just the main app. We have three folders, one for the phone app and two for the watch.
The TodoWatchApp WatchKit App
folder is where we design the interface for the watch while the TodoWatchApp WatchKit Extension
is where we will write the logic and view controllers.
Building our iOS phone app
So, we will start with the phone app. The phone app will listen to triggers from Pusher. These triggers will be sent by the app. First, we will need to add the Pusher dependency. Follow these steps to set it up.
Run this command on your main app directory:
$ pod init
This will create a Podfile
where you can insert your dependencies. Paste this into the file:
# File: ./Podfile
target 'TodoWatchApp' do
use_frameworks!
pod 'PusherSwift'
end
target 'TodoWatchApp WatchKit App' do
use_frameworks!
end
target 'TodoWatchApp WatchKit Extension' do
use_frameworks!
end
We only added the dependency for the main app as the library does not support watchOS yet. You can follow this GitHub issue to learn the latest on that.
Next, run this command still in the main app directory:
$ pod install
After the installation is complete, close your project -TodoWatchApp.xcodeproj
. Still in Xcode, open TodoWatchApp.xcworkspace
located in the folder of your project. This was generated after you installed your dependencies.
For now, we will work with the TodoWatchApp
folder. Now, let us design the interface for the app. All the app needs is a table view to display the todo items. Open the Main.storyboard
file, delete the default ViewController
scene. Now, drag the TableViewController
element and drop on the empty light grey area. You should now have something like this:
Set the controller to be the initial controller. You can do that in the identity inspector pane.
Go to your ViewController.swift
class and change the class it is extending from UIViewController
to UITableViewController
.
The UITableViewController
still extends the UIViewController
class but it is specially configured to table views. Go back to your storyboard, select the Table View Controller element, in the identity inspector (third icon at the top right as seen in the image ), change the class to your ViewController
class like so:
Next, select the Table Cell which is a grandchild of the Table View Controller element, in the attributes inspector, enter an identifier of your choice or better still, use cell. This is the unique name used to identify the row name in the Swift file, later on.
Next, let us design how each row of the table view will look like. We will just want a label displaying the todo item. Simply drag a label to the Content View of the Table Cell. Your design should now look like this:
The additions we just made are reflected in the screenshot above.
Next, create a TableCell.swift
file and paste this:
// File: ./TableCell.swift
import Foundation
import UIKit
class TableCell: UITableViewCell {
@IBOutlet weak var textView: UILabel!
func setLabel(labelValue:String) {
textView.text = labelValue
}
}
This class mimics how each row in the table view will look. It has just one UILabel
as we earlier specified on our storyboard. Now that we have created this file, go back to the Main.storyboard
, select the cell, in the attributes inspector, replace the default class there with TableCell
like so:
Be sure to link the
@IBOutlet
variable from your storyboard to the file.
Now, let us hook things up in our ViewController
swift file. Paste this in the file:
// File: ./ViewController.swift
import UIKit
import PusherSwift
class ViewController: UITableViewController {
var pusher: Pusher!
var itemList = [String]() {
didSet {
self.tableView.reloadData()
}
}
override func viewDidLoad() {
super.viewDidLoad()
setupPusher()
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return itemList.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// inflate each row
let currentItem = itemList[indexPath.row]
let todoCell = tableView.dequeueReusableCell(withIdentifier: "cell") as! TableCell
todoCell.setLabel(labelValue: currentItem)
return todoCell
}
}
Let us go through what we have here: first, we declared a Pusher variable and our list. In the viewDidLoad
method, we call the setupPusher
method which we will look at shortly.
The other two methods are for the table view, to get the size of the table and to get data for each row. Next, paste the setupPusher
function inside the class like so:
func setupPusher() {
let options = PusherClientOptions(
host: .cluster("PUSHER_APP_CLUSTER")
)
pusher = Pusher(
key: "PUSHER_APP_KEY",
options: options
)
// subscribe to channel and bind to event
let channel = pusher.subscribe("todo")
let _ = channel.bind(eventName: "addItem", callback: { (data: Any?) -> Void in
if let data = data as? [String : AnyObject] {
let value = data["text"] as! String
self.itemList.append(value)
}
})
pusher.connect()
}
This method initializes Pusher and listens to the todo
channel for updates.
Replace the Pusher holder with keys found on your dashboard
With this, we are done with our phone app. We will now build the watch app.
Building our watchOS app
Go to the TodoWatchApp WatchKit App
folder, open the Interface.storyboard
. Delete the controller scenes except for the Interface Controller Scene. Now drag a group element library to the watch screen. Set the orientation of the group to horizontal. Next, add a label and a button as children to the group.
We will use a custom icon for the button. You can download the icon here. Open the Assets.xcassets
in the TodoWatchApp WatchKit App
folder and import the new image asset. Make sure it is named add_item
. After importing, go back to the Interface.storyboard
, select the button, and select the icon as the background in the identity inspector. Clear the title on the button, then set the width and height to fixed using 20
as the value.
Next, we will hookup an action from the add button in the storyboard to our InterfaceController
file. We use the assistant editor(icon with rings at top right) to split the two files like so:
After that, open the InterfaceController.swift
file in the TodoWatchApp WatchKit Extension
folder and make sure your file looks like this:
// File: ./InterfaceController.swift
import WatchKit
import Foundation
class InterfaceController: WKInterfaceController {
@IBAction func addNewItem() {
let suggestionsArray = ["Visit Neo", "Write Pusher article"]
presentTextInputController(withSuggestions: suggestionsArray,allowedInputMode: WKTextInputMode.allowEmoji, completion: { (result) -> Void in
guard let choice = result else { return }
let newItem = choice[0] as! String
self.postValue(value: newItem)
})
}
func postValue(value:String){
let parameters = ["value": value] as [String : Any]
let url = URL(string: "http://127.0.0.1:5000/addItem")!
let session = URLSession.shared
var request = URLRequest(url: url)
request.httpMethod = "POST"
do {
request.httpBody = try JSONSerialization.data(
withJSONObject: parameters,
options: .prettyPrinted
)
} catch let error {
print(error.localizedDescription)
}
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
request.addValue("application/json", forHTTPHeaderField: "Accept")
let task = session.dataTask(with: request as URLRequest, completionHandler: { data, response, error in
guard error == nil else {
return
}
guard let data = data else {
return
}
do {
//create json object from data
if let json = try JSONSerialization.jsonObject(with: data, options: .mutableContainers) as? [String: Any] {
print(json)
}
} catch let error {
print(error.localizedDescription)
}
})
task.resume()
}
}
Here, when the add button is clicked, the watch opens the input controller where you can either handwrite your todo, or choose from the suggestions, or dictate it. When you confirm your text, the watch sends the item to the server.
You can now run both applications on the simulator. You can select the WatchApp build so it runs both simulators at once.
When you run the app, it should look like this:
Conclusion
Here, we have been able to successfully build an iOS app to show realtime functionalities between the main app, the watch app and, Pusher. You can still play around this in the GitHub repository.
12 February 2019
by Neo Ighodaro