In-App Purchases: Non-Renewing Subscriptions Tutorial
Learn to offer access to time-limited content to users and monetize your app using In-App Purchases in this Non-Renewing Subscriptions Tutorial. By Owen L Brown.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
In-App Purchases: Non-Renewing Subscriptions Tutorial
30 mins
- When to Use Non-Renewing Subscriptions
- Implementing Non-Renewing Subscriptions: Overview
- Getting Started
- To Do List
- Parse Server & Heroku
- iTunes Connect
- Add In-App Purchase Items
- Sandbox Surprises
- Adding Your Subscriptions to the Product List
- Expiration Handling
- Parse Server Sync
- New Product ID Handling
- Progress Check!
- Providing Subscription Content
- Clean Up
- Where to Go from Here?
iOS has four major types of In-App Purchases (IAP):
- Consumables: User purchases the same product multiple times. Often they’re “used up” and bought again. Examples are currency in a free-to-play game, or healing potions, extra lives, etc.
- Non-consumables: User buys once (and only once), then has access to the item forever. Examples are an extra level pack or some downloadable content.
- Auto-renewable Subscriptions: Dynamic content or ongoing service accessible for a set period – making the user purchase a subscription to continue access. Examples are subscribing to an electronic magazine, subscribing to unlock an extra feature in an app for a month, etc. These subscriptions are automatically renewed by Apple indefinitely until the user decides to cancel.
- Non-renewing Subscriptions: Time-limited access to a service or content that may be static. User needs to manually renew subscription after it’s expired.
There are two types of subscriptions: auto-renewable and non-renewing. This tutorial focuses on non-renewing subscriptions.
Note: In recent years, auto-renewing subscriptions have gone through major changes. Initially they were limited mostly to Newsstand apps, later expanded to include a few more categories with very strict rules and now with the introduction of iOS 10 a broader range of apps can take advantage of this type. Its important to point out the advantages of auto-renewing over non-renewing subscriptions to assist in your decision of which to use. The pros and cons are discussed in a bit.
In this tutorial, you’ll add non-renewing subscriptions to an app called “InsomniOwl”, an app that allows you to browse owl comics. You’ll also be using Parse Server with Heroku as a back-end provider for the app to keep track of users and their purchased subscriptions.
Before beginning, you should be sure to complete, or have experience equivalent to:
- In-App Purchase Tutorial: Getting Started. A Rage Comic app that’ll get your feet wet with selling products inside an app.
- In-App Purchase Video Tutorial: Part 4 Receipts. It’s highly recommended to use receipt validation when implementing subscriptions.
- Migrating to Parse Server. You’ll be using Parse SDK with Heroku as the backend service for storing your users’ subscription information.
- GitHub. Your Github account will support the Parse Server.
If you’re ready to level-up your IAP mastery, read on!
When to Use Non-Renewing Subscriptions
It may seem obvious, but here’s a bit more about the type of subscriptions in iOS.
Auto-renewable subscriptions
When a user signs up for an auto-renewable subscription, they’re continually charged until they manually cancel it. This is obviously great from a developer’s point of view, because it takes a lot more effort to cancel something than to just let it continue. This means that Apple allows you to set a subscription duration and manage renewals automatically through the StoreKit framework.
Apple also has revenue incentives for developers using auto-renewing. If a subscriber continues an auto-renewing subscription past the first year, then Apple will increase the developers part of the cash from 70% to 85%.
As of iOS 10, auto-renewing subscriptions are no longer restricted to Newsstand type apps, but is now allowed for all app categories.
Non-renewing subscriptions
When a user signs up for a non-renewing subscription, they subscribe for a set period of time (1 month, 3 months, etc). When the time runs out, their access to the content ends. To continue to access the content, they have to re-subscribe.
This is not ideal from a developer’s point of view as it forces customers to have to continually make the decision to subscribe. It also means your code must keep track of the expiration date, which can get a bit tricky with users having multiple devices.
So far it seems that non-renewing subscriptions cause more work without offering any advantages. In many cases this may be true, but there are situations where it still makes sense. For example, if the subscription gives users access to a static archive which doesn’t provide a benefit for long term access. Other examples may be apps providing seasonal information, like garden planting tips or winter elk hunting expedition info. These are cases where maintaining the subscription is only applicable to the user for a period of a month or so.
Implementing Non-Renewing Subscriptions: Overview
All right, so you’ve decided you want to begin building your non-renewing subscription empire. What does this mean when it comes to the nitty-gritty of development?
Unlike auto-renewable subscriptions, where the StoreKit framework handles subscription durations and renewals, non-renewing subscriptions require you to do all the heavy lifting.
Here are some things to consider when implementing non-renewing subscriptions:
- The subscription duration is not managed for you by StoreKit, so you’ll need a way of calculating the duration at the point of purchase.
- As with consumable products, your users should be able to purchase items multiple times. Thus, you’ll need a way of determining if there’s time remaining on an existing subscription, and including that time in any new duration, should a user choose to renew.
- You’re also required to make the subscription available to any device owned by the user. There are generally two feasible options you can use to accommodate this requirement:
iCloud. Since the user’s iCloud account is exclusive to them, but shared across their devices, this is a simple and effective option. However, if your app is cross-platform, or has a companion web app, this won’t be the best choice since iCloud restricts usage to iOS devices.
Backend as a service, or BaaS. By requiring a user to create an account in order to subscribe, you can store any necessary data, such as the subscription expiry date, against their account on the server. This method will allow you to share a subscription across all platforms simply, by requiring a user to log in.
In this tutorial, you’ll be using Migrating to Parse Server with Heroku as the backend to store this information, as it is very popular and easy to use. Time to get started!
Getting Started
When you’re ready to begin, download the starter project here.
The starter project already has the basics for IAP, such as StoreKit handling, some product ids, and others. It also contains the code required for logging into the Parse Server, but everything directly related to non-renewing subscriptions and setting up the server has yet to be implemented.
Take a peak at the To Do List below to get an idea of the steps you will be taking.
To Do List
- Get a copy of Parse Server
- Connect a new Heroku app to the Parse Server
- Setup the app to use MongoLab for the server database
- Connect the InsomniOwl app to Heroku
- Add new products to iTunes Connect
- Setup a Sandbox Tester
- Add new products to InsomniOwl app
- Expiration date handling
- Display the content
- Final touches
Now that you know where you are going, time to get to it!
Parse Server & Heroku
First, you’ll need to set up an app in Heroku for this tutorial. To do this, do the following:
- Head over to the Parse Server GitHub.
- Click the Fork button at the top right and select one of your existing repositories. This will create a copy of the Parse Server example that you can integrate with Heroku.
- Scroll down the forked repository and click the Deploy to Heroku button. This will open the Heroku website and begin the Parse Server integration.
- Create a new Heroku account or login to an existing one.
- A new Parse Server Example app will appear. Enter an app name.
- The MongoLab Add-On is free, but you may need to enter credit card information to use it. Finish filling out the remaining fields. Leave the PARSE_MOUNT value at the default /parse location. Replace the yourappname section in the SERVER_URL field with, you guessed it, your app name. But be sure to keep the .herokuapp.com/parse part.
- Click Deploy and wait until the deployment process completes. The automatic process is copying over the Parse SDK server to Heroku and setting up the variables needed for access.
- Once deployment is successful, click on Manage App. Then click Settings to view the application variables.
- Now click the Reveal Config Vars button to display the configuration variables created during deployment. There are 3 variables to copy to the Xcode project: APP_ID, MASTER_KEY, and SERVER_URL. Take note of them!
Note: Enter random characters in the APP_ID and MASTER_KEY fields. Keep these secret as it prevents others from accessing your app backend. You’ll use them shortly in the iOS app.
Now that you’ve finished the Heroku and the Parse Server setup, open the Xcode starter project. Open AppDelegate.swift and in application(_:didFinishLaunchingWithOptions:)
replace the applicationId
, clientKey
and server
property values with your Heroku’s configuration values.
let configuration = ParseClientConfiguration {
$0.applicationId = "com.razeware.InsomniOwl.somethingRandomHere"
$0.clientKey = "myMasterKeyKeepItSecret"
$0.server = "https://insomniowl.herokuapp.com/parse"
}
Note: You must prefix the server
property with https, NOT http. Otherwise, the app will not access the Parse Server and the Console will display an error: The resource could not be loaded because the App Transport Security policy requires the use of a secure connection.
Build and run. When the app launches for the first time, you need to create a new account before the displaying the products list.
Enter a username and password to create a new account and select Sign Up. When you’re done, you should see something like this:

Who stole the goods!
Wait a second, where are the cool Owl images? Well…they’re not in your iTunes Connect account yet. Head over there now!
iTunes Connect
Add In-App Purchase Items
You must register In-App purchased products into iTunes Connect before they will appear in an app. Do the following:
- Log onto iTunes Connect
- Click My Apps
- Add a new iOS app for this project.
- Select the new app and click on Features.
- Click In-App Purchases and then the + button.
To provide the app some purchasing variety, add the following Consumable, Non-Consumable and Non-Renewing Subscriptions. For the tutorial, only enter the name, product ID, and a price. The product ID should be your reverse website, app name and product name. For example: com.yoursite.yourappname.3monthsOfRandom. The last part (ex. 3monthsOfRandom) for each item must match perfectly as listed below in order for the tutorial app to grab the correct Owl image.
Also, be sure to check the Cleared for Sale box or the item won’t display in the app.
Enter the following items and use the specified Product ID suffix:
- Non-Renewing Subscription: 3monthsOfRandom
- Non-Renewing Subscription: 6monthsOfRandom
- Non-Consumable: CarefreeOwl
- Non-Consumable: CouchOwl
- Non-Consumable: CryingOwl
- Non-Consumable: GoodJobOwl
- Non-Consumable: GoodNightOwl
- Non-Consumable: InLoveOwl
- Non-Consumable: LonelyOwl
- Non-Consumable: NightOwl
- Non-Consumable: ShyOwl
- Consumable: RandomOwls
Note: It is important that you specify the duration of any subscription-based IAP, and the most common way to do this is in the display name or description. There’s a good chance Apple will reject the app if it fails to clearly state the duration of any subscription.
When it is all said and done, you should see the following:
It’s important to get the Product IDs specified correctly. The Product IDs prefix must match your apps Bundle Identifier. The suffix uniquely describes the product. Ex: com.yourwebsite.yourappname.3monthsOfRandom.
If you are using your own Bundle Identifier in the app, you’ll need to update the Product IDs in the starter project. Open OwlProducts.swift and update productIDsNonConsumables
to match the Product IDs you setup in iTunes Connect.
static let productIDsNonConsumables: Set<ProductIdentifier> = [
"com.back40.InsomniOwl.CarefreeOwl",
"com.back40.InsomniOwl.GoodJobOwl",
"com.back40.InsomniOwl.CouchOwl",
"com.back40.InsomniOwl.NightOwl",
"com.back40.InsomniOwl.LonelyOwl",
"com.back40.InsomniOwl.ShyOwl",
"com.back40.InsomniOwl.CryingOwl",
"com.back40.InsomniOwl.GoodNightOwl",
"com.back40.InsomniOwl.InLoveOwl"
]
Sandbox Surprises
Ok, you have the goods, but something is still missing. A sandbox user!
Testing In-App Purchases requires at least one sandbox user. This lets you make purchases in the app without actually paying the cash. Add one now:
- In iTunes Connect, click Users and Roles
- Click Sandbox Testers
- Add a tester with an email address that is not used as an Apple ID.
You must always test In-App purchases on an actual device. Sorry Mr. Simulator, you’re out of job.
Open Settings on your iOS device and logout of iTunes & App Store. Do NOT login as your sandbox user email. Doing so will make it an Apple ID and the email will no longer be valid for sandbox testing.
Now you have the goods to sell and a sandbox tester, run the app again. You should now see the list of items you entered. But why are only the non-consumables showing? Good question. It is because the starter app doesn’t support the other types yet. You’ll be fixing that shortly.

Non-Consumables
You have to check out at least one of the owl images. So go ahead and make a purchase. Its on me!
When it prompts you to sign into iTunes Store, be sure to use a sandbox user.

Good Job Mate!
Non-consumable items show up in the app and are ready for sale. Now you’re ready to begin implementing non-renewing subscriptions!
Adding Your Subscriptions to the Product List
Previously in the tutorial, you added subscriptions and a consumable to iTunes Connect. They provide the user with a choice of two subscription durations, three months or six months. A subscription needs to benefit the user in some way and that’s where the Random Owl product comes in. A subscribed user has the unlimited awesomeness to view random owl images throughout eternity!
Add the new product identifiers to the starter project now.
Open OwlProducts.swift and add the following code below the PurchaseNotification
property:
static let randomProductID = "com.back40.InsomniOwl.RandomOwls"
static let productIDsConsumables: Set<ProductIdentifier> = [randomProductID]
static let productIDsNonRenewing: Set<ProductIdentifier> = ["com.back40.InsomniOwl.3monthsOfRandom", "com.back40.InsomniOwl.6monthsOfRandom"]
static let randomImages = [
UIImage(named: "CarefreeOwl"),
UIImage(named: "GoodJobOwl"),
UIImage(named: "CouchOwl"),
UIImage(named: "NightOwl"),
UIImage(named: "LonelyOwl"),
UIImage(named: "ShyOwl"),
UIImage(named: "CryingOwl"),
UIImage(named: "GoodNightOwl"),
UIImage(named: "InLoveOwl")
]
The code above lists out each product id and groups them nicely based on the type of purchase. The array of UIImage
s lists out the random images the user can cycle through.
Note: Again, be sure to enter the product ids based on your website and app name to match the iTunes Connect entries but don’t change product name itself. Ex: com.yourwebsite.yourappname.3monthsOfRandom.
The IAPHelper needs to know about the new productIDs. Update the store
initialization:
public static let store = IAPHelper(productIds: OwlProducts.productIDsConsumables
.union(OwlProducts.productIDsNonConsumables)
.union(OwlProducts.productIDsNonRenewing))
This combines all the purchasing types into one Set
and passes it to the IAPHelper
initializer.
Expiration Handling
The user’s local device needs to know the status of its subscription expiration. The starter project already has some variables setup for you to do this. Open UserSettings.swift and take a peek.
- expirationDate: last known subscription date
- randomRemaining: number of remaining random images user can view
- lastRandomIndex: last random image index
- increaseRandomExpirationDate: increments expiration date by months
- increaseRandomRemaining: increases the number of random images user can view
Notice that UserDefaults
is the local persistence for the variables. Something not obvious is later in the project you’ll also save each product id to UserDefaults
with a Bool
value indicating the purchased state.
As mentioned earlier, you need code to generate and handle the expiration date of a subscription purchase. This date needs to be saved locally and on the Parse Server so it is available to all the user’s devices. Open OwlProducts.swift and add the following to the bottom of the OwlProduct
struct:
public static func setRandomProduct(with paidUp: Bool) {
if paidUp {
UserDefaults.standard.set(true, forKey: OwlProducts.randomProductID)
store.purchasedProducts.insert(OwlProducts.randomProductID)
} else {
UserDefaults.standard.set(false, forKey: OwlProducts.randomProductID)
store.purchasedProducts.remove(OwlProducts.randomProductID)
}
}
This is the first of several helper functions you’ll need for subscription handling. The above code updates the local UserDefaults
with a Bool
depending on if the user paid up. It also updates the IAHelper.purchasedProducts
array which keeps track of the paid status of all the product ids.
Next, add the following below setRandomProduct(with:)
:
public static func daysRemainingOnSubscription() -> Int {
if let expiryDate = UserSettings.shared.expirationDate {
return Calendar.current.dateComponents([.day], from: Date(), to: expiryDate).day!
}
return 0
}
This code returns the number of days remaining on the subscription. If the subscription has expired, it will return 0 or negative.
Next, add the following below daysRemainingOnSubscription()
:
public static func getExpiryDateString() -> String {
let remaining = daysRemainingOnSubscription()
if remaining > 0, let expiryDate = UserSettings.shared.expirationDate {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "dd/MM/yyyy"
return "Subscribed! \nExpires: \(dateFormatter.string(from: expiryDate)) (\(remaining) Days)"
}
return "Not Subscribed"
}
This function formats and returns the expiration date or Not Subscribed when expired.
Since there are two ways a user can purchase random owl images (by subscription or in batches of 5), you’ll need a convenient way to determine if a random owl product is viewable. Add the following code below getExpiryDateString()
:
public static func paidUp() -> Bool {
var paidUp = false
if OwlProducts.daysRemainingOnSubscription() > 0 {
paidUp = true
} else if UserSettings.shared.randomRemaining > 0 {
paidUp = true
}
setRandomProduct(with: paidUp)
return paidUp
}
This code checks to see if any days remain on the subscription. If not, it checks to see if the user purchased unused owl images. Once finished, it calls setRandomProduct(with:)
and returns the paid up status.

Hang in there, Ole Bird!
Parse Server Sync
As mentioned before, the local and remote server subscription date need to be synchronized. This requires a trip to the Parse Server. Add the following code below paidUp()
to handle this.
public static func syncExpiration(local: Date?, completion: @escaping (_ object: PFObject?) -> ()) {
// Query Parse for expiration date.
guard let user = PFUser.current(),
let userID = user.objectId,
user.isAuthenticated else {
return
}
let query = PFQuery(className: "_User")
query.getObjectInBackground(withId: userID) {
object, error in
// TODO: Find latest subscription date.
completion(object)
}
}
The code communicates with the Parse Server, retrieving the data for the currently logged in user. The functions goal is to synchronize the locally stored subscription date with the remote server. The first function parameter is the local date, the second parameter is a completion handler passing back the PFObject
returned by Parse.
It’s necessary to compare the local and server date to determine which is more recent. This avoids a potential problem where a user has renewed their subscription on one device and then tries to renew it on a different device, before restoring any existing purchases.
Insert the following code inside the Parse background callback at the TODO:
marker.
let parseExpiration = object?[expirationDateKey] as? Date
// Get to latest date between Parse and local.
var latestDate: Date?
if parseExpiration == nil {
latestDate = local
} else if local == nil {
latestDate = parseExpiration
} else if parseExpiration!.compare(local!) == .orderedDescending {
latestDate = parseExpiration
} else {
latestDate = local
}
if let latestDate = latestDate {
// Update local
UserSettings.shared.expirationDate = latestDate
// See if subscription valid
if latestDate.compare(Date()) == .orderedDescending {
setRandomProduct(with: true)
}
}
The first line grabs the value of the expirationDateKey
from Parse. Next, it decides which date represents the latest subscription, local or remote. Next, the code updates the local UserSetting’s synced date. Finally, call setRandomProduct
if the subscription is valid.
Now the foundations are in place, you’re able to write the subscription purchasing method. Add the following below syncExpiration(local:completion:)
:
private static func handleMonthlySubscription(months: Int) {
// Update local and Parse with new subscription.
syncExpiration(local: UserSettings.shared.expirationDate) {
object in
// Increase local
UserSettings.shared.increaseRandomExpirationDate(by: months)
setRandomProduct(with: true)
// Update Parse with extended purchase
object?[expirationDateKey] = UserSettings.shared.expirationDate
object?.saveInBackground()
NotificationCenter.default.post(name: NSNotification.Name(rawValue: PurchaseNotification),
object: nil)
}
}
For each new subscription purchase, the expiration date increases by the months parameter. Time to talk about what is going on here.
-
syncExpiration
syncs the latest expiration date withUserSettings
-
increaseRandomExpirationDate
increments the local expiration date by X months -
setRandomProduct
updates the variable & product array pertaining to the random product id - Parse remote server receives the new subscription value
- A notification rings throughout the app broadcasting the new purchase
New Product ID Handling
Currently, new purchases coming from IAPHelper
pass through OwlProducts.handlePurchase
. But the starter project code only supported non-consumables. You’ll give it an overhaul. Replace the existing contents with:
if productIDsConsumables.contains(productID) {
UserSettings.shared.increaseRandomRemaining(by: 5)
setRandomProduct(with: true)
NotificationCenter.default.post(name: NSNotification.Name(rawValue: PurchaseNotification),
object: nil)
} else if productIDsNonRenewing.contains(productID), productID.contains("3months") {
handleMonthlySubscription(months: 3)
} else if productIDsNonRenewing.contains(productID), productID.contains("6months") {
handleMonthlySubscription(months: 6)
} else if productIDsNonConsumables.contains(productID) {
UserDefaults.standard.set(true, forKey: productID)
store.purchasedProducts.insert(productID)
NotificationCenter.default.post(name: NSNotification.Name(rawValue: PurchaseNotification),
object: nil)
}
Based on the purchased product type:
- Consumables: Increase random images remaining by 5 and
- Non-Renewing 3 months: increase subscription by 3 months
- Non-Renewing 6 months: increase subscription by 6 months
- Non-Consumables: update the IAPHelper store’s purchasedProducts
In all of the cases, the code posts the PurchaseNotification
either directly from this function or indirectly. setRandomProduct
is only called for cases dealing with the random owl image product, which includes all types except non-consumables.
Progress Check!
Wow, too much coding. Build and run to see your progress.

Cough up the cash!
Good news! The new subscriptions and consumable products are now showing. If you buy a subscription it still doesn’t let you view the random owl images though. You’ll fix that next.
That completes the subscription handling code. Now head over to the GUI side of things and display the new content to the user.
Providing Subscription Content
Up to this point, the new subscription and consumable products appear in the list but don’t display random owls when selected. You’ll fix that.
Open MasterViewController.swift and replace the tableView(_:didSelectRowAt:)
code with the following:
let product = products[indexPath.row]
if OwlProducts.productIDsConsumables.contains(product.productIdentifier)
|| OwlProducts.productIDsNonRenewing.contains(product.productIdentifier) {
performSegue(withIdentifier: randomImageSegueIdentifier, sender: product)
} else {
performSegue(withIdentifier: showDetailSegueIdentifier, sender: product)
}
Now the user can segue to the RandomViewController
when selecting a product related to the random owl.
Next, you’ll update the functionality of the Restore feature. The Restore button allows users to retrieve purchases made on other devices or from a previous install of the app. Currently, the project only restores consumable products. Add the following to the bottom of restoreTapped(_:)
:
// Restore Non-Renewing Subscriptions Date saved in Parse
OwlProducts.syncExpiration(local: UserSettings.shared.expirationDate) {
object in
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
This code calls syncExpiration
to sync the expirationDate
with the Parse Server. Once the app has the latest date, the completion handler reloads the product list with up-to-date status of each product.
Note: You’ll notice that restore doesn’t reload consumable products. This is by design. Consumables are not transferable between devices nor retainable when the app is deleted.
Build and run.

Randomly….. Empty!
Close, but no cigar. The random content is still missing from RandomViewController
. Open RandomViewController.swift and add the following to refresh()
:
guard OwlProducts.paidUp() else {
resetGUI()
return
}
// User has paid for something
btnRandom.isHidden = false
// Get a random image, not same image as last time
var index = 0
let count = UInt32(OwlProducts.randomImages.count)
repeat {
index = Int(arc4random() % count)
} while index == UserSettings.shared.lastRandomIndex
imageView.image = OwlProducts.randomImages[index]
UserSettings.shared.lastRandomIndex = index
Regarding the code above. The first few lines check to see if the user paid for the product. If not, no random images will display. Next, the code assumes all is good and enables btnRandom
allowing the user to cycle through to the next random image. Finally, use arc4random
to randomize the next image index. You’ll also notice the code prevents the same image from displaying twice in a row.
refresh()
needs more functionality. You need something to display the expiration date to the user and to keep track of consumables used up by the user. Add the following to the bottom of refresh()
.
// Subscription or Times
if OwlProducts.daysRemainingOnSubscription() > 0 {
lblExpiration.text = OwlProducts.getExpiryDateString()
} else {
lblExpiration.text = "Owls Remaining: \(UserSettings.shared.randomRemaining)"
// Decrease remaining Times
UserSettings.shared.randomRemaining = UserSettings.shared.randomRemaining - 1
if UserSettings.shared.randomRemaining <= 0 {
OwlProducts.setRandomProduct(with: false)
}
}
The code validates the user's expiration date. If paid up, it updates lblExpiration
. If no valid subscription exists, it reduces the number of consumables remaining by 1. Each time the app calls refresh()
another consumable consumed. Once all purchased images are gone, nothing more will display.
Note: Pressing btnRandom
calls refresh()
, which also uses up the purchased images.
Build and run to see the progress.
Purchase a subscription and check out some Owls.

Random Owls
Hoot! Hoot!
Clean Up
It would be nice if the subscription items in the table indicated if the subscription is already purchased. Plus, the Random Owl item needs to stand out a little to indicate it's our special random product.
Open ProductCell.swift and replace the line of code toggling the visibility of imgRandom
with the following:
imgRandom.isHidden = (product.productIdentifier != OwlProducts.randomProductID)
This will show a special icon next to the Random Owl item in the table.
Next, insert the following code below at the TODO:
marker.
else if OwlProducts.productIDsNonRenewing.contains(product.productIdentifier) {
btnBuy.isHidden = false
imgCheckmark.isHidden = true
if OwlProducts.daysRemainingOnSubscription() > 0 {
btnBuy.setTitle("Renew", for: .normal)
btnBuy.setImage(UIImage(named: "IconRenew"), for: .normal)
} else {
btnBuy.setTitle("Buy", for: .normal)
btnBuy.setImage(UIImage(named: "IconBuy"), for: .normal)
}
ProductCell.priceFormatter.locale = product.priceLocale
lblPrice.text = ProductCell.priceFormatter.string(from: product.price)
}
This code sets the Buy button to Renew for valid subscriptions. You'll still want to let users buy additional months, but it should always be clear to a user what they've purchased. Finally, the last couple lines of code display the pricing for the subscription.
Build and run.

Hoot! Hoot!
The product list should now behave correctly. If you purchase everything, the Subscribe buttons should all change to Renew and the Buy buttons change to checkmarks:
Where to Go from Here?
Here's the completed sample project.
Congratulations! You've implemented in-app non-renewing subscriptions in your app.
For many simple apps this approach is more than sufficient. But if you want to take things even further check out the other IAP tutorials and videos on the site. Also, checkout the WWDC 2016: Introducing Expanded Subscriptions in iTunes Connect video for all the cool new features.
I hope you enjoyed this tutorial. Please join the forum below for any questions or comments!