tvOS Tutorial: Make a Video App in Swift
Finally Apple is allowing developers the ability to make our own Apple TV apps using the new tvOS SDK. tvOS is just about the same as developing for iOS but with a couple restrictions. No web views are allowed, tvOS does not contain all of the UI objects that are in iOS but it does have most of them, like buttons, alert views, table views and collection views which are probably the most common objects found in Apple TV apps currently. The other restriction is you can only use third-party frameworks that are built specifically for tvOS, you cannot reuse the same frameworks or libraries you use in your iOS or Watch OS apps.
In this tutorial we are going to build the most common type of tvOS app that is currently on the Apple TV platform, a video app. We’ll try to recreate a Netflix style layout and look at the classes and objects needed to make browsing and viewing TV shows and movies.
Project Setup
- Create a new project, File > New > Project
- Choose Application under tvOS and select “Single View Application”
- Name the Product “Video App”, and we’re going to develop it in Swift
Setting up the Menu
The main menu for most Apple TV apps is a tab bar controller, except for tvOS apps it’s at the top of the screen instead of the bottom like iOS apps. This menu will let the user select between the sections of the app, i.e. Movies or TV Shows.
- Click “Main.storyboard” from the project navigator.
- From the UI objects library in the bottom right of Xcode, find “Tab Bar Controller” and drag it onto the Storyboard. Use the pinch gesture on your trackpad to zoom out so you can see everything in the Storyboard.
- Select the Item 2 View Controller and name it “TV” and change the “Item 2” bar item to “TV”
- Do the same for the Item 1 Scene but change that to “Movies”
- Run the program and you’ll see that we have a standard tab bar with both Movies and TV. You can use your arrow keys to switch back and forth between view controllers.
Designing the Featured Movies Layout
Our layout is going to include one top row with the main featured movies or shows then a few rows after it which are individual movies and shows that the user can browse through, just like Netflix.
The secret to creating a tvOS app similar to the ones on the Apple TV now is to put all of your objects into a scroll view. As the user moves the focus down the page the scroll view will automatically move up or down so the user can see objects that are outside of the current visible area.
- Starting with the Movies Scene selected, uncheck “Under Top Bars” and “Under Bottom Bars” from the Attributes Inspector on the right. This will allow the Menu to hide smoothly when the user is at the top of the screen.
- Click on the view so we can customize the layout for our scrolling movie lists.
- From the UI objects library drag out a scroll view and make it the entire size of the view controller 1920 x 940.
- Next drag a UICollectionView from the UI objects library and place it at the top of the screen. This will be our main featured area. It’s size should be 1920×400.
- Set the Cell Size to 640 x 480 so we can fit 3 on the screen.
- Add your featured assets to the project. Right click on the Project Navigator and select “Add Files to …”, select the images you want to store locally.
- Back to the Storyboard, from the UI objects library drag a UIImageView into the Collection View Cell, it should be the entire width and height of the cell.
Custom Featured Collection View Cell
We need to create a custom collection view cell so we can tailor the layout of the cell to exactly how the design looks. We’ll call it FeaturedCollectionViewCell. Start with the large featured collection view cells.
- Right click in the Project Navigator, select “New File”, choose “tvOS > Source > Swift File”.
- Enter in the name of the custom class FeaturedCollectionViewCell and click “Create”.
Below is the code for the featured collection view class, it only contains 1 object, an UIImageView.
import UIKit class FeaturedCollectionViewCell: UICollectionViewCell { @IBOutlet weak var featuredImage: UIImageView! override init(frame: CGRect) { super.init(frame: frame) self.commonInit() } required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) } private func commonInit() { // Initialization code self.layoutIfNeeded() self.layoutSubviews() self.setNeedsDisplay() } override func didUpdateFocusInContext(context: UIFocusUpdateContext, withAnimationCoordinator coordinator: UIFocusAnimationCoordinator) { if (self.focused) { self.featuredImage.adjustsImageWhenAncestorFocused = true } else { self.featuredImage.adjustsImageWhenAncestorFocused = false } } override func layoutSubviews() { super.layoutSubviews() } override func awakeFromNib() { super.awakeFromNib() } override func prepareForReuse() { super.prepareForReuse() } }
You’ll notice a new method you’ve never seen before:
override func didUpdateFocusInContext(context: UIFocusUpdateContext, withAnimationCoordinator coordinator: UIFocusAnimationCoordinator) { if (self.focused) { self.featuredImage.adjustsImageWhenAncestorFocused = true } else { self.featuredImage.adjustsImageWhenAncestorFocused = false } }
Since tvOS doesn’t have touch capability like iOS or Watch OS this method tells the cell what to do when it is in focus. The adjustsImageWhenAncestorFocused property for the UIImageView will make the image expand and popout when it’s in focus for the user. This is the most common UI indicator for the user when browsing through the content.
Hook up the Featured Cell
Now that we have the custom class, we need to implement the UI objects for it within the Storyboard.
- Click “Main.storyboard” from the project navigator.
- Click the Cell under Collection View 1 and inside the Attributes Inspector change the Identifier to “FeaturedCell”. This will be our re-useable identifier when creating the cells.
- Enter the custom class “FeaturedCollectionViewCell” with the object so you can hookup the IBOutlet for the UIImageView we defined earlier.
- From the UI objects library in the bottom right of Xcode, find “Image View” and drag it onto the collection view cell. Then hookup the IBOutlet by dragging the featuredImage outlet to the FeaturedCell.
Movies View Controller
- Make a new Swift file for our Movies View Controller. Right Click on the Project Navigator and select “New File”, choose “tvOS > Source > Swift File”. Name it “MoviesViewController” and click the “Create” button.
- Enter the following code, this is everything required to get the scroll view and collection view to work with the images we have within the project.
import Foundation import UIKit class MoviesViewController: UIViewController, UICollectionViewDelegateFlowLayout, UICollectionViewDataSource, UIScrollViewDelegate { @IBOutlet var scrollView : UIScrollView! @IBOutlet var collectionView1 : UICollectionView! let reuseIdentifierFeatured = "FeaturedCell" override func viewDidLoad() { super.viewDidLoad() } override func viewDidLayoutSubviews() { self.scrollView!.contentSize = CGSizeMake(1920, 2200) } // Collection View Methods func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAtIndex section: Int) -> CGFloat { return 50 } func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAtIndex section: Int) -> CGFloat { return 50 } func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAtIndex section: Int) -> UIEdgeInsets { return UIEdgeInsets(top: 0.0, left: 50.0, bottom: 0.0, right: 50.0) } func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int { return 1 } func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { if (collectionView == self.collectionView1) { return 8 } return 0 } func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell { if (collectionView == self.collectionView1) { let cell : FeaturedCollectionViewCell = collectionView.dequeueReusableCellWithReuseIdentifier(self.reuseIdentifierFeatured, forIndexPath: indexPath) as! FeaturedCollectionViewCell let imageFilename = "feature-\(indexPath.row).jpg" cell.featuredImage.image = UIImage(named: imageFilename) return cell } return UICollectionViewCell() } }
Note: As of Beta3 the scrollView contentSize should be entered within the viewDidLayoutSubviews() method since the view size is not yet set when viewDidLoad() is called
Hook up the Outlets
All of the code has been written in the Movie View Controller class, so we just need to hookup the outlet and delegates for the scroll view and collection view.
- Click “Main.storyboard” from the project navigator.
- Define the custom class for the Movies view controller.
- Click the scroll view and drag the outlet to the Movies
- Click the Collection View and drag the delegates and outlet to the Movies view controller so we can control it within the MoviesViewController.swift file.
Run It
If everything is linked up correctly and the code above all entered you should see the first collection view on the screen with the images displayed.
Scrolling Movies
If you would like to have categories that the user can browse through, you can make them by adding more collection views below the featured one that we just created. Each collection view will be unique with it’s own set of data.
Download the entire project at the end of this tutorial to see how I did it.
Playing Video
The last feature we need to make is allowing the user to watch video once one of the movies or TV shows has been selected.
- Make a new Swift file for our Player View Controller. Right Click on the Project Navigator and select “New File”, choose “tvOS > Source > Swift File”. Name it “PlayerViewController” and click the “Create” button.
- Add the following code to it:
import Foundation import UIKit import AVKit class PlayerViewController: AVPlayerViewController, AVPlayerViewControllerDelegate { override func viewDidLoad() { super.viewDidLoad() } func playVideo() { player = AVPlayer(URL: NSURL(string: "http://clips.vorwaerts-gmbh.de/big_buck_bunny.mp4")!) player?.play() } }
- Add the following collection view method to the the MoviesViewController.swift so when a cell is selected it’ll open up the PlayerViewController:
func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath) { let playerVC = PlayerViewController() playerVC.playVideo() [self.presentViewController(playerVC, animated: true, completion: nil)] }
- Click “Main.storyboard” from the project navigator.
- From the UI objects library in the bottom right of Xcode, find “AVKit Player View Controller” and drag it onto the Storyboard.
- Change the custom class name to “PlayerViewController”.
- Before we can run this, as of iOS 9, we need to allow our app to access external URLs, like the one in our PlayerViewController that links to the video we’re playing.
- Click the Info.plist from the Project Navigator and add the keys below:
Watch Video
Lastly lets test to see if our video player works. Run the app, scroll to a video cell and click the Select button on the new Apple TV remote (or Enter key in the simulator) to play the video. You should see the screen below.
There you have it, your own video app where users can browse through titles by category and select one to watch a video. Be sure to download the sample app below so you can see the full working app.
You can grab the full source code for this tutorial.
Note: Created using Xcode 7.1 GM (tvOS & Swift 2).
Tutorial: Deep Linking in Swift
Deep Linking is becoming very important in apps these days. You can deep link from another app or even from a website to your app. If you want to direct a user to relevant content and they already have your app, linking to a specific article or an app section from an ad, promotion, or your website makes things a lot easier for them.
Register your URL Scheme
A URL scheme lets you communicate with other apps through a protocol that you define. To communicate with an app that implements such a scheme, you must create an appropriately formatted URL and ask the system to open it. To implement support for a custom scheme, you must declare support for the scheme and handle incoming URLs that use the scheme.
Similar to a URL “http://”, ours for the app is going to be set to “DeepLink://”. You can set this to whatever you like but make sure it is unique as possible, no other app on the users phone should have the same URL scheme.
To register your URL Scheme add the following fields to your Info.plist. Add URL Types, then within Item 0, set Document Role to Editor. URL Identifier to your app bundle ID. Create a URL Schemes field and set Item 0 to the URL Scheme you want to use, i.e. “DeepLink” (don’t put in the protocol symbols ://)
Handling URL Requests
An app that has its own custom URL scheme must be able to handle URLs passed to it. All URLs are passed to your app delegate, either at launch time or while your app is running or in the background. To handle incoming URLs, your delegate should implement the following methods below.
The sample URL being loaded is DeepLink://article/A. Below we are looking to see if the deep link type is an article. You may have videos, show, app section in your app.
var window: UIWindow? var loadedEnoughToDeepLink : Bool = false var deepLink : RemoteNotificationDeepLink? func application(application: UIApplication, openURL url: NSURL, sourceApplication: String?, annotation: AnyObject?) -> Bool { if url.host == nil { return true; } let urlString = url.absoluteString let queryArray = urlString!.componentsSeparatedByString("/") let query = queryArray[2] // Check if article if query.rangeOfString("article") != nil { let data = urlString!.componentsSeparatedByString("/") if data.count >= 3 { let parameter = data[3] let userInfo = [RemoteNotificationDeepLinkAppSectionKey : parameter ] self.applicationHandleRemoteNotification(application, didReceiveRemoteNotification: userInfo) } } return true } func applicationHandleRemoteNotification(application: UIApplication, didReceiveRemoteNotification userInfo: [NSObject : AnyObject]) { if application.applicationState == UIApplicationState.Background || application.applicationState == UIApplicationState.Inactive { var canDoNow = loadedEnoughToDeepLink self.deepLink = RemoteNotificationDeepLink.create(userInfo) if canDoNow { self.triggerDeepLinkIfPresent() } } } func triggerDeepLinkIfPresent() -> Bool { self.loadedEnoughToDeepLink = true var ret = (self.deepLink?.trigger() != nil) self.deepLink = nil return ret }
If your app is not running when a URL request arrives, it is launched and moved to the foreground so that it can open the URL. We call applicationHandleRemoteNotification and initialize our custom Deep Link Handler class RemoteNotificationDeepLink to store the userInfo and we’ll call triggerDeepLinkIfPresent() in our main ViewController once it’s loaded to grab the data.
override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view, typically from a nib. let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate appDelegate.triggerDeepLinkIfPresent() }
Next we trigger our custom Deep Link Handler class to take the parameters in the url “DeepLink://article/A” to handle what article A is and open that ViewController in your app to complete the deep link.
// // RemoteNotificationDeepLink.swift // DeepLink // // Created by Brian Coleman on 2015-07-12. // Copyright (c) 2015 Brian Coleman. All rights reserved. // import UIKit let RemoteNotificationDeepLinkAppSectionKey : String = "article" class RemoteNotificationDeepLink: NSObject { var article : String = "" class func create(userInfo : [NSObject : AnyObject]) -> RemoteNotificationDeepLink? { let info = userInfo as NSDictionary var articleID = info.objectForKey(RemoteNotificationDeepLinkAppSectionKey) as! String var ret : RemoteNotificationDeepLink? = nil if !articleID.isEmpty { ret = RemoteNotificationDeepLinkArticle(articleStr: articleID) } return ret } private override init() { self.article = "" super.init() } private init(articleStr: String) { self.article = articleStr super.init() } final func trigger() { dispatch_async(dispatch_get_main_queue()) { //NSLog("Triggering Deep Link - %@", self) self.triggerImp() { (passedData) in // do nothing } } } private func triggerImp(completion: ((AnyObject?)->(Void))) { completion(nil) } } class RemoteNotificationDeepLinkArticle : RemoteNotificationDeepLink { var articleID : String! override init(articleStr: String) { self.articleID = articleStr super.init(articleStr: articleStr) } private override func triggerImp(completion: ((AnyObject?)->(Void))) { super.triggerImp() { (passedData) in var vc = UIViewController() // Handle Deep Link Data to present the Article passed through if self.articleID == "A" { vc = ViewControllerA() } else if self.articleID == "B" { vc = ViewControllerB() } else if self.articleID == "C" { vc = ViewControllerC() } let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate appDelegate.window?.addSubview(vc.view) completion(nil) } } }
Testing the Deep Link
To make sure that the deep links are working put the links onto a website and view it in Safari.
First run the app in the Simulator, in our app you’ll just see a blank page, but the app has been initialized and is ready to handle the URL.
Next press the phone Home button or in the Simulator Hardware > Home. It will take you out of the app. Launch Safari and put in this URL: http://www.brianjcoleman.com/code/deepLinkTest.html.
deepLinkTest.html
DeepLink://article/A
You’ll see there links, each that link to a different article A, B, or C.
Select one of the links and watch as the app opens and links right to the article chosen.
Detect If Your App is Installed
Below is a little bit of JavaScript that you can add to your website to detect if the user has your app installed. If they don’t you can redirect the user to the App Store to download your app. If they do have it you can deep link directly into your app.
You can grab the full source code for this tutorial. Note: Created using XCode 6.4 (Swift 1.2).
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).