The user interface for smart watches is vastly different than what we’re used to. We are entering a new era of UX that involves creative thinking on how we can use the ridiculously small screen real estate, while still giving the user enough power to interact with it. One thing that comes to mind is how do we enter numbers into smart watches. In this post, I would like to show you how to create a number keypad for Apple Watch.
The Storyboard
First thing is we will create a dedicated interface controller that will manage the number keypad. We can use the group container to split the screen up for the buttons. Here’s how the storyboard will look:
I have five groups stacked on top of each other. In each group, I have three buttons side-by-side. I did this by dragging the group control onto the interface, then adding one button at a time, and finally assigning each one to take up 33% of the screen width. These are the properties of the group, notice the relative to container width and fixed height in the size section:
For each button, I am setting the properties as so:
A couple of exceptions though. First is that five groups on top of each other will fit fine on the 42mm Apple Watch, but will scroll on the 38mm version. So only for the first and last group, we will assign an explicit height for the 38mm watch:
The other exception is the size for the label in the first group of course, which will take up 75% of the width.
The Interface Controller
Now that we got our interface set up, let’s wire up a class to it so we can listen to events. Right-click in the target and add a new Cocoa Class file. On the next screen, be sure to make it a subclass of WKInterfaceController:
Once you have the class in your file list, we can now add it as a custom class to the interface controller we created in the storyboard. To do this, click on the interface controller in the storyboard, then on the left pane, go to the “Identity inspector”. From there, type or find your class in the list to assign it:
While we’re here, give the interface an identifier so it can be called with the presentControllerWithName function later, and also give it a title of “Done”, which will will stick on the top left corner of the number keypad to close the keypad.
Now that the interface is glued to the Swift class, you can now create outlets and actions for the labels and buttons. To do this, hold down the Control key and drag each of the components onto your Swift class:
Remember, you will need these to be actions so you can respond to the tap. Do these for all the buttons. The label will just be an outlet so we can display what the user is entering.
Cool, now you have a bunch of empty actions in your class and an outlet for the label.
Show Me the Code!
The actions for these buttons will simply append the number to the label, like how a calculator works. There are some special cases we need to handle, such as decimals and zeros, but let’s wrap it up in a function so it can be shared across the number buttons like this:
var isDecimalAppended = false var isPointZeroAppended = false @IBOutlet var amountLabel: WKInterfaceLabel! func appendValue(value: Int) { let newValue = "\(value)" var currentValue = getDisplayAmount(sourceController.amount, round: false) // Handle appending new number if currentValue == "0" && !isDecimalAppended { // New start of entry currentValue = newValue } else { // Handle point zero because of rounding if isPointZeroAppended { currentValue += currentValue.rangeOfString(".") != nil ? "0" : ".0" isPointZeroAppended = false } // Handle intended decimal if applicable if isDecimalAppended { currentValue += "." isDecimalAppended = false } // Handle point zero because of rounding if value == 0 && currentValue.rangeOfString(".") != nil { isPointZeroAppended = true } // Concatenate intended value currentValue += newValue } amountLabel.setText(currentValue) } @IBAction func oneTapped() { appendValue(1) } @IBAction func twoTapped() { appendValue(2) } @IBAction func threeTapped() { appendValue(3) } @IBAction func fourTapped() { appendValue(4) } @IBAction func fiveTapped() { appendValue(5) } @IBAction func sixTapped() { appendValue(6) } @IBAction func sevenTapped() { appendValue(7) } @IBAction func eightTapped() { appendValue(8) } @IBAction func nineTapped() { appendValue(9) } @IBAction func zeroTapped() { appendValue(0) } func getDisplayAmount(value: Double, round: Bool = true) -> String { // Truncate decimal if whole number return value % 1 == 0 ? "\(Int(value))" : (round ? String(format: "%.5f", value) : "\(value)") }
The appendValue function handles the appending of numbers as the user taps on the buttons. If the value is zero, it will overwrite it, signifying it’s a fresh start. The isDecimalAppend is an indicator if the user type a decimal place previously so it can append it before the new number. The isPointZeroAppended is a tricky scenario if the user types a zero after a decimal place. It will keep rounding off the zero because of getDisplayAmount, so this flag adds the zero back in before adding the new number. We haven’t talked about what the sourceController variable is yet, but we will cover that in just a bit. First, let’s handle the clear, decimal, and positive/negative buttons:
@IBAction func clearTapped() { amountLabel.setText("0") isDecimalAppended = false } @IBAction func positiveNegativeTapped() { sourceController.amount = sourceController.amount * -1 amountLabel.setText("\(getDisplayAmount(sourceController.amount))") } @IBAction func decimalTapped() { var currentValue = getDisplayAmount(sourceController.amount) if currentValue.rangeOfString(".") == nil { currentValue += "." amountLabel.setText(currentValue) isDecimalAppended = true } }
Using the Number Keypad
Now that we have our number keypad working, it’s time for another controller to make use of it. In your main controller, add a variable called “amount”, which is the value the number keypad will update. This is what the sourceController variable was referring to in the number keypad controller. Then add a button and create an action for it, which will popup the number keypad. It makes sense to make the keypad open as a modal so it doesn’t inject itself into the page history for the back button. Here’s what the code looks like:
var amount = Double(0) ... @IBAction func buttonTapped() { // Redirect to number pad self.presentControllerWithName("numberPadController", context: self) }
Notice in the presentControllerWithName, it is passing the whole main controller object into the context. That way, the number keypad controller will be able to update the previous screen (to get to the amount variable).
Putting It All Together
The main interface controller is relatively simple. It’s just a label and a button. The button will open up the number keypad to update the label. Here is what the interface looks like:
The interface class will have an amount variable that will keep track of the current value of the interface. This will be updated later by the number keypad’s controller. The loadData function is placed in the willActivate event so that the amountLabel is updated with the latest amount when this screen is shown again. The getDisplayAmount will do just display the amount in a friendly way. And finally, the buttonTapped will call the number keypad screen and pass the whole controller as the context so it can be updated in the remote number keypad controller. Here’s the full interface code!
class InterfaceController: WKInterfaceController { var amount = Double(0) @IBOutlet var amountLabel: WKInterfaceLabel! override func awakeWithContext(context: AnyObject?) { super.awakeWithContext(context) // Configure interface objects here. } override func willActivate() { // This method is called when watch view controller is about to be visible to user super.willActivate() loadData() } override func didDeactivate() { // This method is called when watch view controller is no longer visible super.didDeactivate() } func loadData() { amountLabel.setText(getDisplayAmount(amount)) } func getDisplayAmount(value: Double, round: Bool = true) -> String { // Truncate decimal if whole number return value % 1 == 0 ? "\(Int(value))" : (round ? String(format: "%.5f", value) : "\(value)") } @IBAction func buttonTapped() { // Redirect to number pad self.presentControllerWithName("numberPadController", context: self) } }
For the number keypad controller, the trick is to store the previous controller context in a variable in the awakeWithContext event. That way, the number keypad controller always has a reference to the previous screen to update the amount variable. Here’s the full number keypad controller code!
class NumberPadController: WKInterfaceController { var sourceController: InterfaceController! var isDecimalAppended = false var isPointZeroAppended = false @IBOutlet var amountLabel: WKInterfaceLabel! override func awakeWithContext(context: AnyObject?) { super.awakeWithContext(context) // Configure interface objects here. sourceController = context as! InterfaceController amountLabel.setText("\(sourceController.getDisplayAmount(sourceController.amount))") } override func willActivate() { // This method is called when watch view controller is about to be visible to user super.willActivate() } override func didDeactivate() { // This method is called when watch view controller is no longer visible super.didDeactivate() } func appendValue(value: Int) { let newValue = "\(value)" var currentValue = sourceController.getDisplayAmount(sourceController.amount, round: false) // Handle appending new number if currentValue == "0" && !isDecimalAppended { // New start of entry currentValue = newValue } else { // Handle point zero because of rounding if isPointZeroAppended { currentValue += currentValue.rangeOfString(".") != nil ? "0" : ".0" isPointZeroAppended = false } // Handle intended decimal if applicable if isDecimalAppended { currentValue += "." isDecimalAppended = false } // Handle point zero because of rounding if value == 0 && currentValue.rangeOfString(".") != nil { isPointZeroAppended = true } // Concatenate intended value currentValue += newValue } sourceController.amount = (currentValue as NSString).doubleValue amountLabel.setText(currentValue) } @IBAction func clearTapped() { sourceController.amount = 0.0 amountLabel.setText("0") isDecimalAppended = false } @IBAction func positiveNegativeTapped() { sourceController.amount = sourceController.amount * -1 amountLabel.setText("\(sourceController.getDisplayAmount(sourceController.amount))") } @IBAction func decimalTapped() { var currentValue = sourceController.getDisplayAmount(sourceController.amount) if currentValue.rangeOfString(".") == nil { currentValue += "." amountLabel.setText(currentValue) isDecimalAppended = true } } @IBAction func oneTapped() { appendValue(1) } @IBAction func twoTapped() { appendValue(2) } @IBAction func threeTapped() { appendValue(3) } @IBAction func fourTapped() { appendValue(4) } @IBAction func fiveTapped() { appendValue(5) } @IBAction func sixTapped() { appendValue(6) } @IBAction func sevenTapped() { appendValue(7) } @IBAction func eightTapped() { appendValue(8) } @IBAction func nineTapped() { appendValue(9) } @IBAction func zeroTapped() { appendValue(0) } }
Below is what the screens should look and behave like in the emulator. First tap on the “Enter Value” button:
Type in the numbers in the number keypad, then tap “Done”:
That will return you to the original controller with the new value:
Conclusion
There you have it, a number keypad for the Apple Watch! Keep in mind that the end game is voice recognition, but until then this will have many uses đ
Enjoy the source code here.
HAPPY CODING!!
Can you update this tutorial? It isn’t working for me, the number on the main interface won’t update after using the number pad and some lines of code like setting … as sourceController cause problems. Thanks! Otherwise, it’s a great tutorial, really easy to follow.
Thanks for the feedback! Have you tried downloading the sample from GitHub? Iâve just tried it with the latest Xcode 6.3.2 and Swift 1.2 and still works as expected. Be sure that youâre loading your data in the right event of your main controller (willActivate not awakeWithContext).
In the numberedcontroller class it gives me the error: use of undeclared type, at InterfaceController
var sourceController: InterfaceController!<- It gives an error message here
var isDecimalAppended = false
var isPointZeroAppended = false
@IBOutlet var amountLabel: WKInterfaceLabel!
override func awakeWithContext(context: AnyObject?) {
super.awakeWithContext(context)
// Configure interface objects here.
sourceController = context as! InterfaceController<- and here
amountLabel.setText("\(sourceController.getDisplayAmount(sourceController.amount))")
}