Tutorial 59
Tutorial: Receipt Validation in Swift
After you have processed your In-App Purchases you may want to verify the receipt with the App Store directly. You’ll get more details about the purchase so you can store that information in your database. There are two ways: first connecting to the Apple App Store directly within your app, and second send the receipt data to your own server and having your server perform the validation with the App Store server. The second is the easiest and most secure method when verifying receipts since it protects against Man-in-the-middle (MITM) attacks where its possible to get free in-app purchases in any app.
Apple recommends you use a trusted server to communicate with the App Store. Using your own server lets you design your app to recognize and trust only your server, and lets you ensure that your server connects with the App Store server. It is not possible to build a trusted connection between a user’s device and the App Store directly because you don’t control either end of that connection.
Validate In-App Purchases
To validate an in-app purchase, your application performs the following tests, in order:
- Parse and validate the application’s receipt, as described in the previous sections. If the receipt is not valid, none of the in-app purchases are valid.
- Parse the in-app purchase receipts. Each in-app purchase receipt consists of a set of attributes, like the application’s receipt does.
- Examine the product identifier for each in-app purchase receipt and enable the corresponding functionality or content in your app.
If validation of an in-app purchase receipt fails, your application simply does not enable the functionality or content.
To begin, be sure you already have In-App Purchases implement in your app. You can follow the steps in this article to integrate In-App Purchases: Tutorial: In-App Purchases (IAP) in Swift.
Send Receipt Data to your Server
Use the validateReceipt() method below to retrieve the receipt data, then send this data to your server. Submit the receipt object as the payload of an HTTP POST request, and encode the data using base64 encoding. Make sure you change the request URL to point to the PHP script on your server. Next, if the PHP script communicates correctly with Apple’s server you will need to parse the response with all of the receipt and transaction information.
func validateReceipt() { var response: NSURLResponse? var error: NSError? var receiptUrl = NSBundle.mainBundle().appStoreReceiptURL var receipt: NSData = NSData(contentsOfURL:receiptUrl!, options: nil, error: nil)! var receiptdata: NSString = receipt.base64EncodedStringWithOptions(NSDataBase64EncodingOptions(rawValue: 0)) //println(receiptdata) var request = NSMutableURLRequest(URL: NSURL(string: "http://www.brianjcoleman.com/code/verifyReceipt.php")!) var session = NSURLSession.sharedSession() request.HTTPMethod = "POST" var err: NSError? request.HTTPBody = receiptdata.dataUsingEncoding(NSASCIIStringEncoding) var task = session.dataTaskWithRequest(request, completionHandler: {data, response, error -> Void in var err: NSError? var json = NSJSONSerialization.JSONObjectWithData(data, options: .MutableLeaves, error: &err) as? NSDictionary if(err != nil) { println(err!.localizedDescription) let jsonStr = NSString(data: data, encoding: NSUTF8StringEncoding) println("Error could not parse JSON: '\(jsonStr)'") } else { if let parseJSON = json { println("Receipt \(parseJSON)") } else { let jsonStr = NSString(data: data, encoding: NSUTF8StringEncoding) println("Receipt Error: \(jsonStr)") } } }) task.resume() }
In-App Purchase Shared Secret
If your application provides Auto-Renewable Subscriptions you will need to send a password along with the receipt data to Apple during validation. If you’re validating a receipt for an auto-renewing receipt, go to iTunes Connect and get the hexadecimal shared secret for your app.
The secret is typically a 32 digit, alpha-numeric string that looks something like this e4b2dd30b7rt4a7382b5173hg790elk7 (this one is fake).
In iTunes Connect go to My Apps > (select your app) > In-App Purchases > View or generate a shared secret
Send Receipt Data to Apple using PHP
Use the following PHP script to take the receipt data and send it to Apple’s server to validate. The script takes two values, the receipt-data and a password (if needed for auto-renewable subscriptions). You must replace this text
Save this file as “verifyReceipt.php”, upload it to your web server and change the permissions to 755.
$value){ $newcontent .= $key.' '.$value; } $new = trim($newcontent); $new = trim($newcontent); $new = str_replace('_','+',$new); $new = str_replace(' =','==',$new); if (substr_count($new,'=') == 0){ if (strpos('=',$new) === false){ $new .= '='; } } $new = '{"receipt-data":"'.$new.'","password":""}'; $info = getReceiptData($new); ?>
Testing
To test the receipt validation, run your application and buy one of the in-app purchases, call the validateReceipt() method to initiate the receipt-data to send to your server where the PHP will pass the receipt (and possible the shared secret password), then get the response back from Apple and send it back to the iOS app where you can parse the JSON and get the receipt information.
During testing you may see some status codes returned from Apple, here is a table that describes each of them:
Status Code | Description |
21000 | The App Store could not read the JSON object you provided. |
21002 | The data in the receipt-data property was malformed or missing. |
21003 | The receipt could not be authenticated. |
21004 | The shared secret you provided does not match the shared secret on file for your account. Only returned for iOS 6 style transaction receipts for auto-renewable subscriptions. |
21005 | The receipt server is not currently available. |
21006 | This receipt is valid but the subscription has expired. When this status code is returned to your server, the receipt data is also decoded and returned as part of the response. Only returned for iOS 6 style transaction receipts for auto-renewable subscriptions. |
21007 | This receipt is from the test environment, but it was sent to the production environment for verification. Send it to the test environment instead. |
21008 | This receipt is from the production environment, but it was sent to the test environment for verification. Send it to the production environment instead. |
Receipt Data
Below is a sample of the data you will receive for the receipt. You may want to parse this JSON and store the values into a database so you can track your users status.
"latest_receipt_info" = ( { "is_trial_period" = false; "original_purchase_date" = "2015-05-20 17:41:12 Etc/GMT"; "original_purchase_date_ms" = 1432143672000; "original_purchase_date_pst" = "2015-05-20 10:41:12 America/Los_Angeles"; "original_transaction_id" = 1000000156014803; "product_id" = "com.brianjcoleman.testiap1"; "purchase_date" = "2015-05-20 17:41:12 Etc/GMT"; "purchase_date_ms" = 1432143672000; "purchase_date_pst" = "2015-05-20 10:41:12 America/Los_Angeles"; quantity = 1; "transaction_id" = 1000000156014803; }, { "expires_date" = "2015-05-23 17:05:59 Etc/GMT"; "expires_date_ms" = 1432400759000; "expires_date_pst" = "2015-05-23 10:05:59 America/Los_Angeles"; "is_trial_period" = false; "original_purchase_date" = "2015-05-23 17:03:00 Etc/GMT"; "original_purchase_date_ms" = 1432400580000; "original_purchase_date_pst" = "2015-05-23 10:03:00 America/Los_Angeles"; "original_transaction_id" = 1000000156451343; "product_id" = "com.brianjcoleman.testiap3"; "purchase_date" = "2015-05-23 17:02:59 Etc/GMT"; "purchase_date_ms" = 1432400579000; "purchase_date_pst" = "2015-05-23 10:02:59 America/Los_Angeles"; quantity = 1; "transaction_id" = 1000000156451343; "web_order_line_item_id" = 1000000029801713; } ); receipt = { "adam_id" = 0; "app_item_id" = 0; "application_version" = 1; "bundle_id" = "com.brianjcoleman.iqtest"; "download_id" = 0; "in_app" = ( { "is_trial_period" = false; "original_purchase_date" = "2015-05-24 01:06:58 Etc/GMT"; "original_purchase_date_ms" = 1432429618000; "original_purchase_date_pst" = "2015-05-23 18:06:58 America/Los_Angeles"; "original_transaction_id" = 1000000156455961; "product_id" = "com.brianjcoleman.testiap2"; "purchase_date" = "2015-05-24 01:06:58 Etc/GMT"; "purchase_date_ms" = 1432429618000; "purchase_date_pst" = "2015-05-23 18:06:58 America/Los_Angeles"; quantity = 1; "transaction_id" = 1000000156455961; }, { "is_trial_period" = false; "original_purchase_date" = "2015-05-20 17:41:12 Etc/GMT"; "original_purchase_date_ms" = 1432143672000; "original_purchase_date_pst" = "2015-05-20 10:41:12 America/Los_Angeles"; "original_transaction_id" = 1000000156014803; "product_id" = "com.brianjcoleman.testiap1"; "purchase_date" = "2015-05-20 17:41:12 Etc/GMT"; "purchase_date_ms" = 1432143672000; "purchase_date_pst" = "2015-05-20 10:41:12 America/Los_Angeles"; quantity = 1; "transaction_id" = 1000000156014803; } ); "original_application_version" = "1.0"; "original_purchase_date" = "2013-08-01 07:00:00 Etc/GMT"; "original_purchase_date_ms" = 1375340400000; "original_purchase_date_pst" = "2013-08-01 00:00:00 America/Los_Angeles"; "receipt_type" = ProductionSandbox; "request_date" = "2015-05-24 16:31:18 Etc/GMT"; "request_date_ms" = 1432485078143; "request_date_pst" = "2015-05-24 09:31:18 America/Los_Angeles"; "version_external_identifier" = 0; }; status = 0; }
You can learn more about the Receipt Fields in the Apple Validation Programming Guide.
Note that while testing auto-renewable subscriptions the “expires_date” in the sandbox will not act like it does in production. When testing auto-renewable subscriptions in the test environment, the duration times are compressed. Additionally, test subscriptions only auto-renew a maximum of six times.
You can grab the full source code for this tutorial. Note: Created using XCode 6.3 (Swift 1.2).
Tutorial: In-App Purchases in Swift
In this tutorial we are going to review how to setup an In-App Purchase (IAP) for your app using Swift. This will show you step by step how to setup an IAP in iTunes Connect for your app, then how to implement it in your app and how you can unlock features or consumables for the user.
Enable IAP in the Developer Portal
The first step is to enable In-App Purchases when you create the App ID for your app. Make sure this is correct by logging into the developer center, then navigating to Certificates, Identifiers & Profiles, App Ids and checking that In-App Purchases are Enabled. They are enabled by default when you create a new app.
Define the IAP in iTunes Connect
The next step is to list the In-App Purchases you would like to include in your app. To do this you already need to have your bundle ID and your app setup in iTunes Connect. To begin listing your IAP’s, go into your app within iTunes Connect and select “In-App Purchases”.
Next if you don’t already have any listed, click “Create New”.
Then you’ll need to select the type of In-App Purchase you need from the 5 types. There are two major types, Consumables and Subscriptions. Both are divided into two categories: for Consumables, we have Non-Consumable and Consumable. For Subscriptions, we have Renewable and Non-Renewable subscriptions. The choice of which one to implement will depend on your application and what you need to give users access to with their purchase.
For each IAP that you define, you need to enter a Reference Name (only displayed to you to remember which IAP is which), Product ID (the string thats used in the code to reference this product) and Price Tier (How much you are going to charge). Every item must also have a product identifier that is associated with your application and uniquely identifies an item sold. Your application uses this product id to fetch localized item descriptions and pricing information from the App Store and to request payments.
Enter in the localized item name and description per language that your app supports. This will be displayed to the user before they agree to pay for your product.
Make sure you upload a screenshot for each IAP to show how it works, and where it is purchased from. You can also leave a review note so that you can explain the overall idea and flow. This will help the review team understand what the IAP is for and may save you a lot of back and forth and get it approved faster. The screenshots and the review note won’t be seen by the end users, they only support the app review team when they are reviewing your app.
I’ve created an In-App Purchase for each one of the 5 types for testing.
Implementing StoreKit
The interactions between the user, your app, and the App Store during the In-App Purchase process take place in three stages, as shown below. First, the user navigates to your app’s store and your app displays its products. Second, the user selects a product to buy and the app requests payment from the App Store. Third, the App Store processes the payment and your app delivers the purchased product.
First let’s start off by declaring the delegates and properties needed for the class. The SKProductsRequestDelegate is used for the callback method needed when retrieving the product information from iTunes Connect.
class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate, SKProductsRequestDelegate { var tableView = UITableView() let productIdentifiers = Set(["com.brianjcoleman.testiap1", "com.brianjcoleman.testiap2", "com.brianjcoleman.testiap3", "com.brianjcoleman.testiap4", "com.brianjcoleman.testiap5"]) var product: SKProduct? var productsArray = Array()
We are going to use a UITableView to display our products. There is a set defined with the unique product identifiers that we setup in iTunes Connect for each one of our products. Finally the product is of type SKProduct which is an object that contains the title, description and price for a product. Then the productArray will be used to store all of the products we receive before we present it to the user.
Stage 1: Retrieving Product Information
To make sure your users see only products that are actually available for purchase, query the App Store before displaying your app’s store UI. This is valuable so you can change the title, description and prices for your products anytime without having to update your app.
In the viewDidLoad(), call the requestProductData() method to retrieve the product information for each one of the product identifiers defined above. It’s best to do a check to determine if the users device has In-App Purchases enabled. If they can make payments then the SKProductsRequest is made to iTunes Connect.
func requestProductData() { if SKPaymentQueue.canMakePayments() { let request = SKProductsRequest(productIdentifiers: self.productIdentifiers as Set) request.delegate = self request.start() } else { var alert = UIAlertController(title: "In-App Purchases Not Enabled", message: "Please enable In App Purchase in Settings", preferredStyle: UIAlertControllerStyle.Alert) alert.addAction(UIAlertAction(title: "Settings", style: UIAlertActionStyle.Default, handler: { alertAction in alert.dismissViewControllerAnimated(true, completion: nil) let url: NSURL? = NSURL(string: UIApplicationOpenSettingsURLString) if url != nil { UIApplication.sharedApplication().openURL(url!) } })) alert.addAction(UIAlertAction(title: "Ok", style: UIAlertActionStyle.Default, handler: { alertAction in alert.dismissViewControllerAnimated(true, completion: nil) })) self.presentViewController(alert, animated: true, completion: nil) } } func productsRequest(request: SKProductsRequest!, didReceiveResponse response: SKProductsResponse!) { var products = response.products if (products.count != 0) { for var i = 0; i < products.count; i++ { self.product = products[i] as? SKProduct self.productsArray.append(product!) } self.tableView.reloadData() } else { println("No products found") } products = response.invalidProductIdentifiers for product in products { println("Product not found: \(product)") } }
Since the delegate SKProductsRequestDelegate is used, after the SKProductsRequest is made the callback method productsRequest is performed. In this method we store all of the product objects into the productsArray so we can populate our UITableView with the products so the user can select one to purchase.
Run your app to ensure the In-App Purchases are coming back from iTunes Connect. Here's what mine look like after I've added them to a UITableView.
While in development it's good to check to see if all of your products are coming through correctly by adding the error checking for invalid product identifiers. If you are not getting any products back from Apple when you are trying to retrieve them look to this checklist:
http://www.gamedonia.com/game-development/solve-invalid-product-ids.
Stage 2: Requesting Payment
So now you've presented the list of products to the user, what's next? Well if you're providing good value or an amazing feature the user wants then they will want to buy one of your products.
func buyProduct(sender: UIButton) { let payment = SKPayment(product: productsArray[sender.tag]) SKPaymentQueue.defaultQueue().addPayment(payment) }
This method uses the button.tag property to get the correct product from the productsArray so the same method can be used for all products. You could also setup a separate function per product.
The user will then be prompted with the standard Apple In-App Purchase dialog asking them to confirm their purchase.
Note: While you can test up to here using the Simulator you will need to test on the device to actually make a successful purchase. To test all In-App Purchases before they are submitted to Apple for review you'll be working in the Sandbox mode. Start by setting up a Sandbox (test account) in from iTunes Connect account. Users & Roles > Sandbox Testers.
When testing on a device be sure to log out of your production iTunes account in the App Store before trying to purchase from a Sandbox account so you'll be prompted to login using your Sandbox Testing account. On the Simulator it will ask you to login (as shown below).
Step 3 - Delivering Products
Once the user has confirmed they would like to purchase your product, the paymentQueue delegate method is invoked, but in order to be able to use the updatedTransactions method we need to declare it's delegate "SKPaymentTransactionObserver" in our class first.
class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate, SKProductsRequestDelegate, SKPaymentTransactionObserver
Also add "SKPaymentQueue.defaultQueue().addTransactionObserver(self)" to your viewDidLoad to register your class with the delegate and add the transaction observer. Now we can add the following function to capture the approved or failed transaction from Apple.
func paymentQueue(queue: SKPaymentQueue!, updatedTransactions transactions: [AnyObject]!) { for transaction in transactions as! [SKPaymentTransaction] { switch transaction.transactionState { case SKPaymentTransactionState.Purchased: println("Transaction Approved") println("Product Identifier: \(transaction.payment.productIdentifier)") self.deliverProduct(transaction) SKPaymentQueue.defaultQueue().finishTransaction(transaction) case SKPaymentTransactionState.Failed: println("Transaction Failed") SKPaymentQueue.defaultQueue().finishTransaction(transaction) default: break } } } func deliverProduct(transaction:SKPaymentTransaction) { if transaction.payment.productIdentifier == "com.brianjcoleman.testiap1" { println("Consumable Product Purchased") // Unlock Feature } else if transaction.payment.productIdentifier == "com.brianjcoleman.testiap2" { println("Non-Consumable Product Purchased") // Unlock Feature } else if transaction.payment.productIdentifier == "com.brianjcoleman.testiap3" { println("Auto-Renewable Subscription Product Purchased") // Unlock Feature } else if transaction.payment.productIdentifier == "com.brianjcoleman.testiap4" { println("Free Subscription Product Purchased") // Unlock Feature } else if transaction.payment.productIdentifier == "com.brianjcoleman.testiap5" { println("Non-Renewing Subscription Product Purchased") // Unlock Feature } }
If the purchase is approved we need to make sure to deliver the functionality or product to the user and remove the transaction from the queue. In this example I am capturing the transaction and passing it to a deliverProduct method where I'll unlock the feature for the user.
Restore Purchases
If your app offers Non-Consumable or any Subscription based In-App Purchases Apple requires that you provide a Restore Purchases button so users who have already purchased your product can get access to it again if they use your app on another Apple device (i.e. another iPhone, iPad or Apple TV).
func restorePurchases(sender: UIButton) { SKPaymentQueue.defaultQueue().addTransactionObserver(self) SKPaymentQueue.defaultQueue().restoreCompletedTransactions() } func paymentQueueRestoreCompletedTransactionsFinished(queue: SKPaymentQueue!) { println("Transactions Restored") var purchasedItemIDS = [] for transaction:SKPaymentTransaction in queue.transactions as! [SKPaymentTransaction] { if transaction.payment.productIdentifier == "com.brianjcoleman.testiap1" { println("Consumable Product Purchased") // Unlock Feature } else if transaction.payment.productIdentifier == "com.brianjcoleman.testiap2" { println("Non-Consumable Product Purchased") // Unlock Feature } else if transaction.payment.productIdentifier == "com.brianjcoleman.testiap3" { println("Auto-Renewable Subscription Product Purchased") // Unlock Feature } else if transaction.payment.productIdentifier == "com.brianjcoleman.testiap4" { println("Free Subscription Product Purchased") // Unlock Feature } else if transaction.payment.productIdentifier == "com.brianjcoleman.testiap5" { println("Non-Renewing Subscription Product Purchased") // Unlock Feature } } var alert = UIAlertView(title: "Thank You", message: "Your purchase(s) were restored.", delegate: nil, cancelButtonTitle: "OK") alert.show() }
The paymentQueueRestoreCompletedTransactionsFinished delegate method will loop through each In-App Purchase and check the product identifier of it. For each product identifier it find’s it will unlock the feature, just like the original deliverProduct(transaction:SKPaymentTransaction) method.
You can grab the full source code for this tutorial. Note: Created using XCode 6.3 (Swift 1.2).
If you would like to add receipt validation code to your app to prevent unauthorized purchases and keep a record of successful in-app purchases read Tutorial: Receipt Validation in Swift.
Tutorial: Swipe Actions for UITableViewCell in Swift
In a new project we wanted to give the user options when they swipe across a table view cell. Most people know that in the Mail app for iOS 8 you can swipe and expose 3 buttons “More” “Flag” and “Archive”. Lets look to see how we can display our own buttons with different colors and titles, then do something when they are tapped.
UITableViewRowAction
A UITableViewRowAction object defines a single action to present when the user swipes horizontally in a table row. In an editable table, performing a horizontal swipe in a row reveals a button to delete the row by default. This class lets you define one or more custom actions to display for a given row in your table. Each instance of this class represents a single action to perform and includes the text, formatting information, and behavior for the corresponding button.
To add custom actions to your table view’s rows, implement the tableView:editActionsForRowAtIndexPath: method in your table view’s delegate object. In that method, create and return the actions for the indicated row. The table handles the remaining work of displaying the action buttons and executing the appropriate handler block when the user taps the button.
func tableView(tableView: UITableView, editActionsForRowAtIndexPath indexPath: NSIndexPath) -> [AnyObject]? { let more = UITableViewRowAction(style: .Normal, title: "More") { action, index in println("more button tapped") } more.backgroundColor = UIColor.lightGrayColor() let favorite = UITableViewRowAction(style: .Normal, title: "Favorite") { action, index in println("favorite button tapped") } favorite.backgroundColor = UIColor.orangeColor() let share = UITableViewRowAction(style: .Normal, title: "Share") { action, index in println("share button tapped") } share.backgroundColor = UIColor.blueColor() return [share, favorite, more] }
Use this method when you want to provide custom actions for one of your table rows. When the user swipes horizontally in a row, the table view moves the row content aside to reveal your actions. Tapping one of the action buttons executes the handler block stored with the action object.
This method returns an array of UITableViewRowAction objects representing the actions for the row. Each action you provide is used to create a button that the user can tap.
Here’s what it looks like when a user swipes across the cell.
To get this working working, you will need to implement the following two methods on your UITableView’s delegate.
func tableView(tableView: UITableView, canEditRowAtIndexPath indexPath: NSIndexPath) -> Bool { // the cells you would like the actions to appear needs to be editable return true } func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) { // you need to implement this method too or you can't swipe to display the actions }
The first method canEditRowAtIndexPath enabled what rows you would like to make editable within the table. The second commitEditingStyle needs to be included in your class or you will not be able to swipe open the action buttons.
It’s interesting to note that in iOS 8 Apple opened up editActionsForRowAtIndexPath for developers. It was a private API in iOS 7 that Apple used in their Mail app. Looks like they are doing this again with some newer features. In iOS8 the ability to swipe completely across a cell to implement the action in the array is private, as well as the ability to swipe from the left side to display other action buttons. Features to look for in iOS 9.
You can grab the full source code for this tutorial. Note: Created using XCode 6.3 (Swift 1.2).
Tutorial: Building an Apple Watch App
The new Apple Watch is going to be released soon, we are going to take a little time to build a quick app that displays the North American time zones. This app will allow the user to swipe between the timezones so they can easily know what time it is anywhere in Canada and the US.
This app is built on top of the Tutorial: Today Widget in Swift tutorial, since in that app we also made a timezone view. Since the Today Widget and the Apple Watch apps are very similar because they are both built using extensions to an existing app it should make it a little easier to build.
What is a Watch App?
If you don’t know much about the framework of what makes up a watch app, you should first read Get Started with Apple Watch and WatchKit. This will give you a great introduction to what a Watch App is, and how glances and notifications work with the watch.
Setup the Application Extension
- Select File > New > Project. Select the Single view application template, and call it WatchApp.
- Add the Application Extension Target. Select File > New > Target…. Then in the left pane, select Apple Watch and under that choose WatchKit App.
Set the language to Swift if is isn’t already and check both Include Notification Scene and Include Glance Scene are not checked. Click Finish and Xcode will set up the target and the files needed for the Watch interface.
- A message will popup asking if you want to activate the “WatchApp WatchKit App” scheme. Click Activate.
- Adding a WatchKit App target to your Xcode project creates two new executables and updates your project’s build dependencies. Building your iOS app builds all three executables (the iOS app, WatchKit extension, and WatchKit app) and packages them together.
The WatchKit app is packaged inside your WatchKit extension, which is in turn packaged inside your iOS app. When the user installs your iOS app on an iPhone, the system prompts the user to install your WatchKit app if there is a paired Apple Watch available.
Create the Watch App View
The terminology for Watch Apps are a little different than iOS apps, instead of calling them “views”, Apple uses the term “Interface”. WatchKit apps do not use the same layout model as iOS apps. When assembling the scenes of your WatchKit app interface, you do not create view hierarchies by placing elements arbitrarily in the available space. Instead, as you add elements to your scene, Xcode arranges the items for you, stacking them vertically on different lines. At runtime, Apple Watch takes those elements and lays them out for you based on the available space.
When creating your interfaces in Xcode, let objects resize themselves to fit the available space whenever possible. App interfaces should be able to run both display sizes of Apple Watch. Letting the system resize objects to fit the available space minimizes the amount of custom code you have to write for each device
- We are going to start by adding a label into the Watch App Interface, in Interfaces the label type is WKInterfaceLabel. This one is the name of the time zone for the time we are going to display. Drag the label out from the Objects inspector just like you would in an iOS app. Also be sure to align the label text to center so it looks nice, also set the Position – Horizontal to “Center” as well.
- Next we need to add another label for the time. We’ll use a standard label so we can easily change the date per time zone> Watch Kit comes with its own Date label called WKInterfaceDate (but that only displays the current date and time). This can be customized, for our purposes we only want to see the time, not the date, so change the Date format to “None” and customize the font to “System” with a size of 30, and make it “Bold” so the time stands out, also set the Position – Horizontal to “Center” and Vertical to “Center”.
Xcode supports customizing your interface for the different sizes of Apple Watch. The changes you make in the storyboard editor by default apply to all sizes of Apple Watch, but you can customize your storyboard scenes as needed for different devices. For example, you might make minor adjustments to the spacing and layout of items or specify different images for different device sizes.
- Now that we have our UI defined, let’s connect the IBOutlets with the code in the “InterfaceController.swift” file. You can do this many ways, just like an iOS app. Either Control-drag the UI object from the storyboard into the InterfaceController.swift or define the code and link up using the Connections Inspector.
Here is the code for the two objects we are linking:// // InterfaceController.swift // WatchApp WatchKit Extension // // Created by Brian Coleman on 2015-04-17. // Copyright (c) 2015 Brian Coleman. All rights reserved. // import WatchKit import Foundation class InterfaceController: WKInterfaceController { @IBOutlet var timeInterfaceDate : WKInterfaceDate? = WKInterfaceDate() 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() } override func didDeactivate() { // This method is called when watch view controller is no longer visible super.didDeactivate() } }
Watch App Navigation
For WatchKit apps with more than one screen of content, you must choose a technique for navigating between those screens. WatchKit apps support two navigation styles, which are mutually exclusive:
- Page based. This style is suited for apps with simple data models where the data on each page is not closely related to the data on any other page. A page-based interface contains two or more independent interface controllers, only one of which is displayed at any given time. At runtime, the user navigates between interface controllers by swiping left or right on the screen. A dot indicator control at the bottom of the screen indicates the user’s current position among the pages.
- Hierarchical. This style is suited for apps with more complex data models or apps whose data is more hierarchical. A hierarchical interface always starts with a single root interface controller. In that interface controller, you provide controls that, when tapped, push new interface controllers onto the screen.
Although you cannot mix page-based and hierarchical navigation styles in your app, you can supplement these base navigation styles with modal presentations. Modal presentations are a way to interrupt the current user workflow to request input or display information. You can present interface controllers modally from both page-based and hierarchical apps. The modal presentation itself can consist of a single screen or multiple screens arranged in a page-based layout.
For our project we are going to use a page based navigation so the user can swipe between the four time zones.
You configure a page-based interface in your app’s storyboard by creating a next-page segue from one interface controller to the next.
To create a next-page segue between interface controllers:
- In your storyboard, add interface controllers for each of the pages in your interface. Let’s add one for each of our time zones.
- Control-click your app’s main interface controller, and drag the segue line to another interface controller scene.
- The second interface controller should highlight, indicating that a segue is possible.
- Release the mouse button.
- Select “next page” from the relationship segue panel.
- Using the same technique, create segues from each interface controller to the next.
- The order in which you create your segues defines the order of the pages in your interface.
Add in Live Dynamic Data
We are almost done making our watch app, let’s add in the clocks to finish it up. The updateClocks() method is called every second to keep the time up to date.
// // InterfaceController.swift // WatchApp WatchKit Extension // // Created by Brian Coleman on 2015-04-17. // Copyright (c) 2015 Brian Coleman. All rights reserved. // import WatchKit import Foundation class InterfaceController: WKInterfaceController { @IBOutlet var easternLabel : WKInterfaceLabel? = WKInterfaceLabel() @IBOutlet var centralLabel : WKInterfaceLabel? = WKInterfaceLabel() @IBOutlet var mountainLabel : WKInterfaceLabel? = WKInterfaceLabel() @IBOutlet var pacificLabel : WKInterfaceLabel? = WKInterfaceLabel() override func awakeWithContext(context: AnyObject?) { super.awakeWithContext(context) // Create a timer to refresh the time every second var timer = NSTimer.scheduledTimerWithTimeInterval(1.0, target: self, selector: Selector("updateClocks"), userInfo: nil, repeats: true) timer.fire() } 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 updateClocks() { var time: NSDate = NSDate() let formatter:NSDateFormatter = NSDateFormatter(); var timeZone = NSTimeZone(name: "UTC") formatter.timeZone = timeZone formatter.dateFormat = "h:mm a" var formattedString = formatter.stringFromDate(time) var formatDateString = formattedString.stringByReplacingOccurrencesOfString(" p", withString: "PM", options: nil, range: nil) formattedString = formattedString.stringByReplacingOccurrencesOfString(" a", withString: "AM", options: nil, range: nil) timeZone = NSTimeZone(name: "US/Eastern") formatter.timeZone = timeZone formatter.dateFormat = "h:mm a" formattedString = formatter.stringFromDate(time) formatDateString = formattedString.stringByReplacingOccurrencesOfString(" p", withString: "PM", options: nil, range: nil) formattedString = formattedString.stringByReplacingOccurrencesOfString(" a", withString: "AM", options: nil, range: nil) self.easternLabel?.setText(formatDateString) timeZone = NSTimeZone(name: "US/Pacific") formatter.timeZone = timeZone formatter.dateFormat = "h:mm a" formattedString = formatter.stringFromDate(time) formatDateString = formattedString.stringByReplacingOccurrencesOfString(" p", withString: "PM", options: nil, range: nil) formattedString = formattedString.stringByReplacingOccurrencesOfString(" a", withString: "AM", options: nil, range: nil) self.pacificLabel?.setText(formatDateString) timeZone = NSTimeZone(name: "US/Mountain") formatter.timeZone = timeZone formatter.dateFormat = "h:mm a" formattedString = formatter.stringFromDate(time) formatDateString = formattedString.stringByReplacingOccurrencesOfString(" p", withString: "PM", options: nil, range: nil) formattedString = formattedString.stringByReplacingOccurrencesOfString(" a", withString: "AM", options: nil, range: nil) self.mountainLabel?.setText(formatDateString) timeZone = NSTimeZone(name: "US/Central") formatter.timeZone = timeZone formatter.dateFormat = "h:mm a" formattedString = formatter.stringFromDate(time) formatDateString = formattedString.stringByReplacingOccurrencesOfString(" p", withString: "PM", options: nil, range: nil) formattedString = formattedString.stringByReplacingOccurrencesOfString(" a", withString: "AM", options: nil, range: nil) self.centralLabel?.setText(formatDateString) } }
Test Your Watch App
- When you run the app you’ll see a blank view since our app doesn’t do anything except create the Watch App and the watch is shown in an External Display.
- Build and Run your app against the Watch App target.
- Then, switch over to your simulator and check that you have the External Display set for the watch. iOS Simulator > Hardware > External Displays (try both sizes to see how it looks).
- You should see your app in the Apple Watch simulator. Swipe left to see the other time zones.
You can grab the full source code for this tutorial. Note: Built for Xcode 6.3 (Swift 1.2).
If you would like to continue on and add a Glance to your watch app, read Tutorial: Building a Apple Watch Glance or add a Dynamic Notification Interface, read Tutorial: Building a Apple Watch Notification
Tutorial: Building an Apple Watch Glance
A glance is a supplemental way for the user to view important information from your app. Not all apps need a glance. A glance provides immediately relevant information in a timely manner. For example, the glance for a calendar app might show information about the user’s next meeting, whereas the glance for an airline app might display gate information for an upcoming flight. WatchKit apps may have only one glance interface. Do not add more than one glance interface controller to your app’s storyboard.
To build a Glance you need to already have a Watch App, if you’d like to learn how to do that, read
Tutorial: Building an Apple Watch App In that tutorial we walk through building an app that shows the time across all four Canadian and U.S. timezones.
Glance Guidelines
Xcode provides fixed layouts for arranging the contents of your glance. After choosing a layout that works for your content, use the following guidelines to fill in that content:
- Design your glance to convey information quickly. Do not display a wall of text. Make appropriate use of graphics, colors, and animation to convey information.
- Focus on the most important data. A glance is not a replacement for your WatchKit app. Just as your WatchKit app is a trimmed down version of its containing iOS app, a glance is a trimmed down version of your WatchKit app.
- Do not include interactive controls in your glance interface. Interactive controls include buttons, switches, sliders, and menus.
- Avoid tables and maps in your glance interface. Although they are not prohibited, the limited space makes tables and maps less useful.
- Be timely with the information you display. Use all available resources, including time and location to provide information that matters to the user. And remember to update your glance to account for changes that occur between the time your interface controller is initialized and the time it is displayed to the user.
- Use the system font for all text. To use custom fonts in your glance, you must render that text into an image and display the image.
- Because an app has only one glance interface controller, that one controller must be able to display the data you want.
For our app we’re going to add a Glance that contains all timezones for the user to view at once.
Add a Glance
- Begin by adding all of the labels needed for the Interface.
- Now that we have our UI defined, let’s connect the IBOutlets with the code in the “GlanceController.swift” file. You can do this many ways, just like an iOS app. Either Control-drag the UI object from the storyboard into the GlanceController.swift or define the code and link up using the Connections Inspector.
- After the UI is all hooked up, let’s add some code as we did for the Watch app so our times will update every second.
// // GlanceController.swift // WatchApp WatchKit Extension // // Created by Brian Coleman on 2015-04-17. // Copyright (c) 2015 Brian Coleman. All rights reserved. // import WatchKit import Foundation class GlanceController: WKInterfaceController { @IBOutlet var easternLabel : WKInterfaceLabel? = WKInterfaceLabel() @IBOutlet var centralLabel : WKInterfaceLabel? = WKInterfaceLabel() @IBOutlet var mountainLabel : WKInterfaceLabel? = WKInterfaceLabel() @IBOutlet var pacificLabel : WKInterfaceLabel? = WKInterfaceLabel() override func awakeWithContext(context: AnyObject?) { super.awakeWithContext(context) // Create a timer to refresh the time every second var timer = NSTimer.scheduledTimerWithTimeInterval(1.0, target: self, selector: Selector("updateClocks"), userInfo: nil, repeats: true) timer.fire() } 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 updateClocks() { var time: NSDate = NSDate() let formatter:NSDateFormatter = NSDateFormatter(); var timeZone = NSTimeZone(name: "UTC") formatter.timeZone = timeZone formatter.dateFormat = "h:mm a" var formattedString = formatter.stringFromDate(time) var formatDateString = formattedString.stringByReplacingOccurrencesOfString(" p", withString: "PM", options: nil, range: nil) formattedString = formattedString.stringByReplacingOccurrencesOfString(" a", withString: "AM", options: nil, range: nil) timeZone = NSTimeZone(name: "US/Eastern") formatter.timeZone = timeZone formatter.dateFormat = "h:mm a" formattedString = formatter.stringFromDate(time) formatDateString = formattedString.stringByReplacingOccurrencesOfString(" p", withString: "PM", options: nil, range: nil) formattedString = formattedString.stringByReplacingOccurrencesOfString(" a", withString: "AM", options: nil, range: nil) self.easternLabel?.setText(formatDateString) timeZone = NSTimeZone(name: "US/Pacific") formatter.timeZone = timeZone formatter.dateFormat = "h:mm a" formattedString = formatter.stringFromDate(time) formatDateString = formattedString.stringByReplacingOccurrencesOfString(" p", withString: "PM", options: nil, range: nil) formattedString = formattedString.stringByReplacingOccurrencesOfString(" a", withString: "AM", options: nil, range: nil) self.pacificLabel?.setText(formatDateString) timeZone = NSTimeZone(name: "US/Mountain") formatter.timeZone = timeZone formatter.dateFormat = "h:mm a" formattedString = formatter.stringFromDate(time) formatDateString = formattedString.stringByReplacingOccurrencesOfString(" p", withString: "PM", options: nil, range: nil) formattedString = formattedString.stringByReplacingOccurrencesOfString(" a", withString: "AM", options: nil, range: nil) self.mountainLabel?.setText(formatDateString) timeZone = NSTimeZone(name: "US/Central") formatter.timeZone = timeZone formatter.dateFormat = "h:mm a" formattedString = formatter.stringFromDate(time) formatDateString = formattedString.stringByReplacingOccurrencesOfString(" p", withString: "PM", options: nil, range: nil) formattedString = formattedString.stringByReplacingOccurrencesOfString(" a", withString: "AM", options: nil, range: nil) self.centralLabel?.setText(formatDateString) } }
Test Your Glance
- When you run the app you’ll see a blank view since our app doesn’t do anything except create the Watch App and the watch is shown in an External Display.
- Build and Run your app against the Glance target.
- Then, switch over to your simulator and check that you have the External Display set for the watch. iOS Simulator > Hardware > External Displays (try both sizes to see how it looks).
- You should see your app in the Glance in the Watch simulator.
You can grab the full source code for this tutorial. Note: Built for Xcode 6.3 (Swift 1.2).
If you would like to continue on and add a Dynamic Notification Interface to your Watch App, read Tutorial: Building a Apple Watch Notification