Notice
Recent Posts
Recent Comments
Link
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
Tags
- 리펙토링
- rxswift
- Human interface guide
- MVVM
- UICollectionView
- SWIFT
- ribs
- Observable
- combine
- Refactoring
- map
- uiscrollview
- uitableview
- 리팩토링
- RxCocoa
- Xcode
- HIG
- swift documentation
- clean architecture
- 클린 코드
- swiftUI
- 리펙터링
- Clean Code
- tableView
- UITextView
- 애니메이션
- collectionview
- Protocol
- ios
- 스위프트
Archives
- Today
- Total
김종권의 iOS 앱 개발 알아가기
[iOS - swift] 2. StoreKit - IAP (In App Purchases, 인앱 결제) 적용 방법 (코딩 및 구현) 본문
iOS 응용 (swift)
[iOS - swift] 2. StoreKit - IAP (In App Purchases, 인앱 결제) 적용 방법 (코딩 및 구현)
jake-kim 2022. 6. 6. 23:271. StoreKit - IAP (In App Purchases, 인앱 결제) 사용 방법 (Sandbox, 인앱 결제 앱 등록)
2. StoreKit - IAP (In App Purchases, 인앱 결제) 적용 방법 (코딩 및 구현)
3. StoreKit - SwiftyStoreKit을 이용하여 IAP (In App Purchase) 쉽게 구현 방법
* 이전 포스팅 글, 인앱 결제 앱 등록 에서 알아보았던 이미 준비되어야 하는 것
- Apple Developer에서 App ID 등록
- App Store Connect > 앱 내 구입 > 상품 등록 완료
- App Store Connect > 계정 및 액세스 > sandbox 계정 생성
- App Store Connect > 계약, 세금 및 금융거래 > 유로 앱 > 약관 보기 및 동의하기 진행
코딩 - StoreKit을 이용한 결제 구현
- StoreKit에서 제공하는 타입과 싱글톤 사용
- SKProduct - 상품정보 (가격, 이름 등)
- SKPayment - SKProduct 인수를 받아서 생성되는 지불 정보 (product의 id, request 데이터 등)
- SKProductsRequest - 결제를 요청할때 사용하는 타입 (delegate로 성공했는지 실패했는지 알려줌)
- SKPaymentQueue.default() - 데이터 가져오기에 사용
- IAPService.swift 파일 생성
- 내부 델리게이트를 통해 IAP에 성공했는지 실패했는지 알 수 있고, 외부에도 completion을 제공하는 식으로 구현하기 위해서, 따로 Completion을 정의
- 이 completion은 외부에서 정의하고, 실행은 내부에서 불리도록 구현 (delegate 패턴)
// IAPService.swift
import StoreKit
typealias ProductsRequestCompletion = (_ success: Bool, _ products: [SKProduct]?) -> Void
- 외부에서 필요한 메소드를 명시하기 위해서 protocol 정의
- products 항목 가져오기 (getProducts)
- product 구입하기 (buyProduct)
- 구입했는지 확인하기 (isProductPurchased)
- 구입한 목록 조회 (restorePurchased)
protocol IAPServiceType {
var canMakePayments: Bool { get }
func getProducts(completion: @escaping ProductsRequestCompletion)
func buyProduct(_ product: SKProduct)
func isProductPurchased(_ productID: String) -> Bool
func restorePurchases()
}
- StoreKit을 사용하려면 NSObject를 상속받고 IAPServiceType을 준수
- 추가로 필요한 프로퍼티 선언
- productIDs: 앱스토어에서 입력한 productID들 "com.jake.sample.ExInAppPurchase.shopping"
- purchasedProductIDs: 구매한 productID
- productsRequest: 앱스토어에 입력한 productID로 부가 정보 조회할때 사용하는 인스턴스
- proeductsCompletion: 사용하는쪽에서 해당 클로저를 통해 실패 or 성공했을때 값을 넘겨줄 수 있는 프로퍼티 (델리게이트)
final class IAPService: NSObject, IAPServiceType {
private let productIDs: Set<String>
private var purchasedProductIDs: Set<String> = []
private var productsRequest: SKProductsRequest?
private var productsCompletion: ProductsRequestCompletion?
}
- canMakePayments - queue를 확인하여 현재 결제가 되는지 확인
var canMakePayments: Bool {
SKPaymentQueue.canMakePayments()
}
- 상품 정보를 받아서 초기화
- 앱스토어에서 입력한 productID들 "com.jake.sample.ExInAppPurchase.shopping"
init(productIDs: Set<String>) {
self.productIDs = productIDs
super.init()
}
- IAPService에 SKPaymentQueue를 연결
init(productIDs: Set<String>) {
self.productIDs = productIDs
super.init()
SKPaymentQueue.default().add(self) // <- 여기 추가
}
- 상품 정보 조회
- completion을 여기서 캡쳐해놓고 밑에 delegate에서 호출하는 형식
func getProducts(completion: @escaping ProductsRequestCompletion) {
self.productsRequest?.cancel()
self.productsCompletion = completion
self.productsRequest = SKProductsRequest(productIdentifiers: self.productIDs)
self.productsRequest?.delegate = self
self.productsRequest?.start()
}
extension IAPService: SKProductsRequestDelegate {
// didReceive
func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
let products = response.products
self.productsCompletion?(true, products)
self.clearRequestAndHandler()
products.forEach { print("Found product: \($0.productIdentifier) \($0.localizedTitle) \($0.price.floatValue)") }
}
// failed
func request(_ request: SKRequest, didFailWithError error: Error) {
print("Erorr: \(error.localizedDescription)")
self.productsCompletion?(false, nil)
self.clearRequestAndHandler()
}
private func clearRequestAndHandler() {
self.productsRequest = nil
self.productsCompletion = nil
}
}
- 나머지 메소드 정의
func buyProduct(_ product: SKProduct) {
SKPaymentQueue.default().add(SKPayment(product: product))
}
func isProductPurchased(_ productID: String) -> Bool {
self.purchasedProductIDs.contains(productID)
}
func restorePurchases() {
SKPaymentQueue.default().restoreCompletedTransactions()
}
IAPService를 wrapping하는 타입 정의
- 앱스터에서 입력한 ProductID를 가지고 있는 Warpping 타입
enum MyProducts {
static let productID = "com.jake.sample.ExInAppPurchase.shopping"
static let iapService: IAPServiceType = IAPService(productIDs: Set<String>([productID]))
static func getResourceProductName(_ id: String) -> String? {
id.components(separatedBy: ".").last
}
}
사용하는쪽
- 기본적으로 필요한 property 선언
import UIKit
import StoreKit
class ViewController: UIViewController {
private let restoreButton: UIButton = { ... }
private let tableView: UITableView = { ... }
private var products = [SKProduct]()
}
- 상품 정보 가져오기
// in viewDidLoad
MyProducts.iapService.getProducts { [weak self] success, products in
print("load products \(products ?? [])")
guard let ss = self else { return }
if success, let products = products {
DispatchQueue.main.async {
ss.products = products
ss.tableView.reloadData()
}
}
}
- 구입했던 목록 조회
@objc private func restore() {
MyProducts.iapService.restorePurchases()
}
- 셀에 'buy' 버튼을 탭한 경우, 구매 요청
cell.buyButtonTap = { [weak product] in
guard let product = product else { return }
MyProducts.iapService.buyProduct(product)
}
(todo - 테이블 뷰에 product 보이는지 확인)
product가 안보이는 경우 체크리스트
- 프로젝트의 BundleID와 Apple Developer에 등록된 BundleID가 일치하는지?
- 앱스토어 입력한것과 코드에서 사용한 productID가 일치하는지 "com.jake.sample.ExInAppPurchase.shopping"
- App Store Connect > 계약, 세금 및 금융거래 탭 > 유로 앱 > 활성 상태가 아니고 사용자 정보 대기 중이면 아직 사용 불가
구매 목록 조회
- 구매 여부를 UserDefaults에 저장해두고 IAPService에서 ProductIDs를 받아올 때 초기화하여 사용
// in IAPService
private var purchasedProductIDs: Set<String>
init(productIDs: Set<String>) {
self.productIDs = productIDs
self.purchasedProductIDs = productIDs
.filter { UserDefaults.standard.bool(forKey: $0) == true }
super.init()
SKPaymentQueue.default().add(self)
}
SKPaymentQueue 에서 처리되는 상태 체크 방법
- SKPaymentQueue에서 처리되는 일
- purchased
- failed
- restored
- deferred
- purchasing
- SKPaymentTransactionObserver 델리게이트를 준수하여 처리
extension IAPService: SKPaymentTransactionObserver {
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
}
}
- 각 상태 처리
- 거래가 성공, 실패, restore된 경우 finishTransaction을 실행
- restore된 경우(구매 완료된 것 다시 조회) 구매했던 목록으로 추가 (UserDefaults)
extension IAPService: SKPaymentTransactionObserver {
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
transactions.forEach {
switch $0.transactionState {
case .purchased:
print("completed transaction")
self.deliverPurchaseNotificationFor(id: $0.original?.payment.productIdentifier)
SKPaymentQueue.default().finishTransaction($0)
case .failed:
if let transactionError = $0.error as NSError?,
let description = $0.error?.localizedDescription,
transactionError.code != SKError.paymentCancelled.rawValue {
print("Transaction erorr: \(description)")
}
SKPaymentQueue.default().finishTransaction($0)
case .restored:
print("failed transaction")
self.deliverPurchaseNotificationFor(id: $0.original?.payment.productIdentifier)
SKPaymentQueue.default().finishTransaction($0)
case .deferred:
print("deferred")
case .purchasing:
print("purchasing")
default:
print("unknown")
}
}
}
private func deliverPurchaseNotificationFor(id: String?) {
guard let id = id else { return }
self.purchasedProductIDs.insert(id)
UserDefaults.standard.set(true, forKey: id)
// TODO: noti
}
}
위 메소드 deliverPuechaseNotificationFor에서 노티 관리
- 노티 관리를 위해 메시지 정의를 위해 파일 추가 "IAPService+Notification.swift"
// IAPService+Notification.swift
import Foundation
extension Notification.Name {
static let iapServicePurchaseNotification = Notification.Name("IAPServicePurchaseNotification")
}
- PaymentQueue의 델리게이트 메소드에서 불리는 deliverPurchaseNotificationFor(id:) 메소드에 노티를 보내는 코드 추가 (post)
private func deliverPurchaseNotificationFor(id: String?) {
guard let id = id else { return }
self.purchasedProductIDs.insert(id)
UserDefaults.standard.set(true, forKey: id)
NotificationCenter.default.post( // <- 추가
name: .iapServicePurchaseNotification,
object: id
)
}
- 사용하는 쪽 ViewController에서 노티 구독
// in viewDidLoad
NotificationCenter.default.addObserver(
self,
selector: #selector(handlePurchaseNoti(_:)),
name: .iapServicePurchaseNotification,
object: nil
)
- handlePurchaseNoti 정의
- 해당 productID를 찾고 변경된 데이터의 index를 탐색하여, 해당 index 셀 업데이트 수행
@objc private func handlePurchaseNoti(_ notification: Notification) {
guard
let productID = notification.object as? String,
let index = self.products.firstIndex(where: { $0.productIdentifier == productID })
else { return }
self.tableView.reloadRows(at: [IndexPath(index: index)], with: .fade)
self.tableView.performBatchUpdates(nil, completion: nil)
}
Sandbox 계정으로 로그인
- iPhone > Setting > iTunes 및 App Store
- 로그인 버튼을 클릭하고 이전 포스팅 글에서 만든 sandbox 계정으로 로그인하면 완료
* 전체 코드: https://github.com/JK0369/ExIAP
* 참고
https://developer.apple.com/support/app-store-connect/
https://www.raywenderlich.com/5456-in-app-purchase-tutorial-getting-started
https://medium.com/better-programming/in-app-purchases-and-storekit-in-ios-14-aed2c3e58966
'iOS 응용 (swift)' 카테고리의 다른 글
Comments