Покупки в приложениях iOS со Swift 3
() translation by (you can also view the original English article)



Вступление
Покупки в приложениях - отличная возможность для разработчиков, которые хотят получать больше дохода и предлагают дополнительный контент и функции через свои приложения. Например, для игр вы можете купить камни или монеты, а для фото-приложений разблокировать новые эффекты или инструменты. Всё это делается с помощью кредитной карты или другим способом, не выходя из приложения.
В этом уроке я опишу все шаги по созданию Consumable и Non-Consumable IAP в iTunes Connect и покажу код, который вам нужен для покупки обоих продуктов. Я сделал sample Xcode project with a label and two buttons, загрузите его и следуйте по указаниям, чтобы понять, как он работает.
Создание Sandbox Tester в iTunes Connect
Я предполагаю, что вы уже создали приложение iOS в разделе My Apps в iTunes Connect. Теперь сделаем Sandbox Tester для проверки IAP на вашем реальном устройстве (не Simulator - он не поддерживает In-App покупки).
Войдите в Users and Roles, далее на вкладку Sandbox Tester, нажмите (+) рядом с Tester.



Заполните форму, чтобы добавить новый sandbox tester. После сохранения вернитесь в раздел My App и щёлкните значок своего приложения, чтобы ввести его данные и создать продукты IAP.
Создание продуктов IAP в iTunes Connect
Consumable Products
Во вкладке Features нажмите (+) рядом с In-App Purchases. Можно создавать только один продукт за раз, поэтому начнём с Consumable.



Consumable IAP, как следует из названия, является продуктом, который можно купить многократно. Мы будем использовать его для сбора дополнительных «монет» в нашем демонстрационном приложении.
Нажмите Create, чтобы инициализировать элемент IAP. На следующем экране вы настроите всю информацию о своем продукте:
- Reference Name: это имя будет использоваться в отчётах iTunes Connect и в Sales and Trends. Оно может быть любым, но не длиннее 64 символов, в App Store оно отражаться не будет.
- Product ID: уникальный алфавитно-цифровой идентификатор для распознавания вашего продукта. Разработчики часто используют для него синтаксис web-reverse. В нашем примере это com.iaptutorial.coins. Позже мы вставим этот ID в строку нашего кода.
- Price: выберите ценовой разряд из выпадающего меню. Помните: чтобы продать ваш продукт через приложение в App Store, вы должны подать заявку на получение Paid Application Agreement в Agreements, Tax & Banking.
- Localizations: для этого урока мы выбрали только английский язык, но вы можете добавить, нажав кнопку (+). Введите Display Name и Description. Оба они будут видны в App Store.
- Screenshot: загрузите скриншот для обзора. Он не отобразится в App Store, и должен быть допустимого для платформы приложений размера, поэтому, если ваше приложение Universal, можете загрузить скриншот iPad.
- Review Notes: любая дополнительная информация о вашем IAP, которая может быть полезна для рецензента.



Закончив, нажмите Save и получите предупреждение:
Ваша первая покупка In-App должна быть представлена в новой версии приложения. Выберите его из раздела In-App Purchases и нажмите Submit.
Non-Consumable Products
Теперь нажмите кнопку In-App Purchases в левом списке, прямо над кнопкой Game Center и добавьте новый продукт IAP. На этот раз выберите опцию Non-Consumable:



Нажмите Create и повторите шаги, описанные выше. Поскольку этот продукт Non-Consumable и пользователи смогут купить его только раз, Apple требует подтверждения возможности оплаты. Это на случай, если вы удалите приложение и установите его повторно или зайдёте с другого устройства с тем же Apple ID и вам нужно будет вернуть свои покупки, не заплатив за них дважды. Поэтому позже мы добавим функцию Restore Purchase в нашем коде.
ID продукта, который мы создали - com.iaptutorial.premium с ценовым уровнем USD $2.99. Мы назвали его Unlock Premium Version.
Когда вы заполните поля, сохраните свой продукт и вернитесь на страницу In-App Purchases. У вас будет список из двух продуктов, с их Name, Type, ID и Status, Ready to Submit.



Вернитесь на страницу своего приложения, нажав на кнопки App Store и Prepare for Submission. Прокрутите вниз до раздела In-App Purchases прямо под General App Information, нажмите кнопку (+), чтобы добавить свои продукты IAP.



Выберите их и нажмите Done.



Наконец, нажмите Save в правом верхнем углу экрана, и вы сможете настроить продукты In-App Purchase на iTunes Connect.
Войдите в Тестер Sandbox на устройстве iOS
Прежде чем перейти к коду, сделайте ещё шаг. Нажмите Settings > iTunes & App Store на устройстве iOS. Если вы уже вошли в систему со своим Apple ID, нажмите на него и выберите Sign Out. Затем войдите в систему с учётными данными для sandbox tester, которые вы создали. После входа в систему может появиться предупреждение:
Просто нажмите Cancel. Ваше устройство снова попросит sandbox login, пытаясь совершить покупку и узнает вашу тестовую учётную запись, чтобы вы не платили ни копейки по кредитной карте за любую покупку, которую совершаете.
Выйдите из Settings, подключите устройство к компьютеру Mac через USB-кабель и, наконец, начните кодирование!
Код
Если вы откроете наш демонстрационный проект, то увидите, что весь необходимый код для In-App Purchase написан:
Если вы хотите протестировать приложение, то должны изменить Bundle Identifier на свой id. В противном случае Xcode не позволит вам запускать приложение на реальном устройстве и приложение не узнает два ваших IAP-продукта.
Войдите в ViewController.swift и проверьте код. Прежде всего, мы добавили инструкцию import для StoreKit
и делегатов, которые нам нужны для отслеживания транзакций и запросов продукта.
1 |
import StoreKit |
2 |
|
3 |
class ViewController: UIViewController, |
4 |
SKProductsRequestDelegate, |
5 |
SKPaymentTransactionObserver
|
6 |
{
|
Затем мы объявили несколько полезных просмотров.
1 |
/* Views */
|
2 |
@IBOutlet weak var coinsLabel: UILabel! |
3 |
@IBOutlet weak var premiumLabel: UILabel! |
4 |
@IBOutlet weak var consumableLabel: UILabel! |
5 |
@IBOutlet weak var nonConsumableLabel: UILabel! |
6 |
CoinsLabel
и premiumLabel
будут использоваться для показа результатов покупок обоих продуктов. ConsumableLabel
и nonConsumableLabel
покажут описание и цену каждого продукта IAP, которые мы ранее создали в iTunes Connect.
Теперь добавим некоторые переменные:
1 |
/* Variables */
|
2 |
let COINS_PRODUCT_ID = "com.iaptutorial.coins" |
3 |
let PREMIUM_PRODUCT_ID = "com.iaptutorial.premium" |
4 |
|
5 |
var productID = "" |
6 |
var productsRequest = SKProductsRequest() |
7 |
var iapProducts = [SKProduct]() |
8 |
var nonConsumablePurchaseMade = UserDefaults.standard.bool(forKey: "nonConsumablePurchaseMade") |
9 |
var coins = UserDefaults.standard.integer(forKey: "coins") |
10 |
Первые две строки - это напоминание об ID продуктов. Важно, чтобы эти строки точно соответствовали тем, которые были зарегистрированы в разделе iTunes Connect In-App Purchase.
-
productID
- это строка, которую мы будем использовать для определения продукта для покупки. -
productsRequest
- это экземплярSKProductsRequest
, необходимый для поиска продуктов IAP из вашего приложения в iTC. -
iapProducts
просто наборSKProducts
. Обратите внимание, что префикс SK означает StoreKit, структуру iOS для обработки покупок.
Последние две строки загружают две переменные типа Boolean
и Integer
, необходимые для отслеживания покупок монет и премиальной версии, consumable и non-consumable продуктов.
Следующий код в viewDidLoad()
выполняет несколько действий сразу после запуска приложения:
1 |
// Check your In-App Purchases
|
2 |
print("NON CONSUMABLE PURCHASE MADE: \(nonConsumablePurchaseMade)") |
3 |
print("COINS: \(coins)") |
4 |
|
5 |
// Set text
|
6 |
coinsLabel.text = "COINS: \(coins)" |
7 |
|
8 |
if nonConsumablePurchaseMade { premiumLabel.text = "Premium version PURCHASED!" |
9 |
} else { premiumLabel.text = "Premium version LOCKED!"} |
10 |
|
11 |
// Fetch IAP Products available
|
12 |
fetchAvailableProducts() |
Сначала мы просто регистрируем каждую покупку в консоли Xcode. Затем мы показываем общее количество монет, которые мы купили с помощью coinsLabel
. Поскольку мы запускаем демонстрационное приложение впервые, оно отображает COINS: 0.
if
ставится в текст premiumLabel
в зависимости от того, был ли приобретен non-consumable продукт. Для начала он будет показывать Premium version LOCKED! пока мы не сделали премиальную покупку.
Последняя строка кода вызывает метод, его мы увидим позже, он извлекает продукты, которые мы хранили в iTC.
Теперь давайте посмотрим, что делают две кнопки покупки, которые мы установили в нашем demo app:
1 |
// MARK: - BUY 10 COINS BUTTON
|
2 |
@IBAction func buy10coinsButt(_ sender: Any) { |
3 |
purchaseMyProduct(product: iapProducts[0]) |
4 |
}
|
5 |
|
6 |
|
7 |
// MARK: - UNLOCK PREMIUM BUTTON
|
8 |
@IBAction func unlockPremiumButt(_ sender: Any) { |
9 |
purchaseMyProduct(product: iapProducts[1]) |
10 |
}
|
Оба метода вызовут функцию, которая проверит, может ли устройство совершать покупки, а если это возможно, приложение вызовет методы StoreKit для их обработки.
Как упоминалось ранее, нам нужна третья кнопка для восстановления нашей non-consumable покупки. Вот её код:
1 |
// MARK: - RESTORE NON-CONSUMABLE PURCHASE BUTTON
|
2 |
@IBAction func restorePurchaseButt(_ sender: Any) { |
3 |
SKPaymentQueue.default().add(self) |
4 |
SKPaymentQueue.default().restoreCompletedTransactions() |
5 |
}
|
6 |
|
7 |
func paymentQueueRestoreCompletedTransactionsFinished(_ queue: SKPaymentQueue) { |
8 |
nonConsumablePurchaseMade = true |
9 |
UserDefaults.standard.set(nonConsumablePurchaseMade, forKey: "nonConsumablePurchaseMade") |
10 |
|
11 |
UIAlertView(title: "IAP Tutorial", |
12 |
message: "You've successfully restored your purchase!", |
13 |
delegate: nil, cancelButtonTitle: "OK").show() |
14 |
}
|
15 |
Функция IBAction
подключается к кнопке Restore Purchase в Storyboard и подсоединяется к системе Apple's In-App Purchase, чтобы восстановить покупку, если она уже была сделана.
paymentQueueRestoreCompletedTransactionsFinished()
- это метод из фреймворка StoreKit, который подтвердит переменную nonConsumablePurchaseMade
после того, как покупка будет успешно восстановлена.
Мы закончили с кнопками, поэтому давайте посмотрим, что делает функция fetchAvailableProducts ()
:
1 |
// MARK: - FETCH AVAILABLE IAP PRODUCTS
|
2 |
func fetchAvailableProducts() { |
3 |
|
4 |
// Put here your IAP Products ID's
|
5 |
let productIdentifiers = NSSet(objects: |
6 |
COINS_PRODUCT_ID, |
7 |
PREMIUM_PRODUCT_ID
|
8 |
)
|
9 |
|
10 |
productsRequest = SKProductsRequest(productIdentifiers: productIdentifiers as! Set<String>) |
11 |
productsRequest.delegate = self |
12 |
productsRequest.start() |
13 |
}
|
14 |
Сначала мы создаем экземпляр NSSet
, который в основном представляет собой массив строк. Мы сохраним два ID продукта, которые мы ранее объявили.
Затем мы запускаем SKProductsRequest
на основе этих ID для отображения информации о продуктах (описании и цене) IAP, которые будут обрабатываться этим методом:
1 |
// MARK: - REQUEST IAP PRODUCTS
|
2 |
func productsRequest (_ request:SKProductsRequest, didReceive response:SKProductsResponse) { |
3 |
if (response.products.count > 0) { |
4 |
iapProducts = response.products |
5 |
|
6 |
// 1st IAP Product (Consumable) ------------------------------------
|
7 |
let firstProduct = response.products[0] as SKProduct |
8 |
|
9 |
// Get its price from iTunes Connect
|
10 |
let numberFormatter = NumberFormatter() |
11 |
numberFormatter.formatterBehavior = .behavior10_4 |
12 |
numberFormatter.numberStyle = .currency |
13 |
numberFormatter.locale = firstProduct.priceLocale |
14 |
let price1Str = numberFormatter.string(from: firstProduct.price) |
15 |
|
16 |
// Show its description
|
17 |
consumableLabel.text = firstProduct.localizedDescription + "\nfor just \(price1Str!)" |
18 |
// ------------------------------------------------
|
19 |
|
20 |
|
21 |
|
22 |
// 2nd IAP Product (Non-Consumable) ------------------------------
|
23 |
let secondProd = response.products[1] as SKProduct |
24 |
|
25 |
// Get its price from iTunes Connect
|
26 |
numberFormatter.locale = secondProd.priceLocale |
27 |
let price2Str = numberFormatter.string(from: secondProd.price) |
28 |
|
29 |
// Show its description
|
30 |
nonConsumableLabel.text = secondProd.localizedDescription + "\nfor just \(price2Str!)" |
31 |
// ------------------------------------
|
32 |
}
|
33 |
}
|
34 |
В приведённой выше функции сначала проверим, есть ли продукты, зарегистрированные в iTunes Connect и соответственно настроим наш массив iapProducts
. Затем мы можем инициализировать два SKProducts и распечатать их описание и цену на этикетках.
Прежде чем перейти к ядру кода In-App Purchase, добавим ещё пару функций:
1 |
// MARK: - MAKE PURCHASE OF A PRODUCT |
2 |
func canMakePurchases() -> Bool { return SKPaymentQueue.canMakePayments() } |
3 |
func purchaseMyProduct(product: SKProduct) { |
4 |
if self.canMakePurchases() { |
5 |
let payment = SKPayment(product: product) |
6 |
SKPaymentQueue.default().add(self) |
7 |
SKPaymentQueue.default().add(payment) |
8 |
|
9 |
print("PRODUCT TO PURCHASE: \(product.productIdentifier)") |
10 |
productID = product.productIdentifier |
11 |
|
12 |
|
13 |
// IAP Purchases dsabled on the Device |
14 |
} else { |
15 |
UIAlertView(title: "IAP Tutorial", |
16 |
message: "Purchases are disabled in your device!", |
17 |
delegate: nil, cancelButtonTitle: "OK").show() |
18 |
} |
19 |
} |
Первая проверяет, может ли наше устройство совершать покупки. Вторую функцию мы вызываем с двух кнопок. Запуская очередь платежей и изменяя нашу переменную productID
в выбранный productIdentifier
.
Теперь мы наконец пришли к последнему методу делегирования, он обрабатывает результаты платежей:
1 |
// MARK:- IAP PAYMENT QUEUE
|
2 |
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) { |
3 |
for transaction:AnyObject in transactions { |
4 |
if let trans = transaction as? SKPaymentTransaction { |
5 |
switch trans.transactionState { |
6 |
|
7 |
case .purchased: |
8 |
SKPaymentQueue.default().finishTransaction(transaction as! SKPaymentTransaction) |
9 |
|
10 |
// The Consumable product (10 coins) has been purchased -> gain 10 extra coins!
|
11 |
if productID == COINS_PRODUCT_ID { |
12 |
|
13 |
// Add 10 coins and save their total amount
|
14 |
coins += 10 |
15 |
UserDefaults.standard.set(coins, forKey: "coins") |
16 |
coinsLabel.text = "COINS: \(coins)" |
17 |
|
18 |
UIAlertView(title: "IAP Tutorial", |
19 |
message: "You've successfully bought 10 extra coins!", |
20 |
delegate: nil, |
21 |
cancelButtonTitle: "OK").show() |
22 |
|
23 |
|
24 |
|
25 |
// The Non-Consumable product (Premium) has been purchased!
|
26 |
} else if productID == PREMIUM_PRODUCT_ID { |
27 |
|
28 |
// Save your purchase locally (needed only for Non-Consumable IAP)
|
29 |
nonConsumablePurchaseMade = true |
30 |
UserDefaults.standard.set(nonConsumablePurchaseMade, forKey: "nonConsumablePurchaseMade") |
31 |
|
32 |
premiumLabel.text = "Premium version PURCHASED!" |
33 |
|
34 |
UIAlertView(title: "IAP Tutorial", |
35 |
message: "You've successfully unlocked the Premium version!", |
36 |
delegate: nil, |
37 |
cancelButtonTitle: "OK").show() |
38 |
}
|
39 |
|
40 |
break
|
41 |
|
42 |
case .failed: |
43 |
SKPaymentQueue.default().finishTransaction(transaction as! SKPaymentTransaction) |
44 |
break
|
45 |
case .restored: |
46 |
SKPaymentQueue.default().finishTransaction(transaction as! SKPaymentTransaction) |
47 |
break
|
48 |
|
49 |
default: break |
50 |
}}} |
51 |
}
|
Эта функция имеет сообщение switch
и проверяет каждое состояние платежа. Первый case
вызывается, если покупка была успешно выполнена и завершает транзакцию.
Внутри этого блока мы проверим выбор ID продукта и выполним необходимые действия для обновления приложения, поэтому, если мы купим 10 дополнительных монет, мы добавим 10 к переменной coins
, сохраним её значение с помощью UserDefaults
, отобразим новое количество монет и заявим об этом.
Обратите внимание, что вы можете совершать эту покупку без ограничений, поскольку это consumable IAP.
Точно так же, если мы купили продукт non-consumable, приложение устанавливает переменную nonConsumablePurchaseMade
в true
, сохраняет её, изменяет текст premiumLabel
и запускает предупреждение о том, что покупка прошла успешно.
Другие два cases
обрабатывают результаты платежей за отказ и восстановление. Приложение запустит оповещение, если ваша транзакция завершится неудачей или если вы восстановили non-consumable покупку.
Вот так! Теперь войдите в систему со своими учётными данными Sandbox Tester и запустите проверку приложения. Сначала появится предупреждение:
Выберите Use Existing Apple ID и снова введите данные Sandbox Tester для входа в систему. Это потому, что приложение распознаёт реального пользователя из настроек iTunes и App Store, а не из Sandbox.
После входа в систему, вы сможете совершать покупки обоих продуктов.






Шаблоны CodeCanyon
Если вы работаете с iOS и хотите глубже узнать язык Swift и разработку приложений, смотрите my iOS app templates on CodeCanyon.
Есть сотни других iOS app templates on the Envato Market, готовых к работе и ускорению вашего приложения. Покопайтесь в них! Возможно, этим вы сэкономите часы работы в своём следующем приложении.
Заключение
В этом уроке мы рассмотрели все шаги по созданию продуктов In-App Purchase в iTunes Connect и написанию кода для вашего приложения. Надеюсь, вы сможете использовать эти знания в своём следующем iOS app!
Спасибо за чтение, и до встречи! Ознакомьтесь с другими нашими курсами и пособиями по разработке iOS app с помощью Swift.