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
- 리팩토링
- 애니메이션
- 리펙터링
- uitableview
- ios
- UITextView
- combine
- 스위프트
- Clean Code
- swift documentation
- clean architecture
- Protocol
- map
- Xcode
- uiscrollview
- collectionview
- Human interface guide
- tableView
- ribs
- UICollectionView
- 리펙토링
- 클린 코드
- Observable
- RxCocoa
- swiftUI
- SWIFT
- HIG
- MVVM
- Refactoring
- rxswift
Archives
- Today
- Total
김종권의 iOS 앱 개발 알아가기
[iOS - swift] 3. StoreKit - SwiftyStoreKit을 이용하여 IAP (In App Purchase) 쉽게 구현 방법 본문
iOS 응용 (swift)
[iOS - swift] 3. StoreKit - SwiftyStoreKit을 이용하여 IAP (In App Purchase) 쉽게 구현 방법
jake-kim 2022. 6. 7. 22:061. StoreKit - IAP (In App Purchases, 인앱 결제) 사용 방법 (Sandbox, 인앱 결제 앱 등록)
2. StoreKit - IAP(In App Purchases, 인앱 결제) 적용 방법 (코딩 및 구현)
3. StoreKit - SwiftyStoreKit을 이용하여 IAP (In App Purchase) 쉽게 구현 방법
SwiftyStoreKit
- git repo
- 매우 단순하게 IAP 서비스를 구현할 수 있는 프레임워크
- SwiftyStoreKit을 사용하지 않는다면 2번 포스팅 글에서 알아보았듯이 비동기적으로 payment에 관한 상태를 delegate에서 처리하는 형태
- SwiftyStoreKit을 사용하면 싱글톤과 클로저로 직관적으로 처리가 가능
SwiftyStoreKit.completeTransactions(atomically: true) { purchases in
...
}
예제로 사용할 내용
- 버튼 3개 존재하는 화면
- restore 버튼 - 이전에 구입했던 내용이 있을 때, 이 버튼을 클릭하여 복구하는 기능
- RandomColor 버튼 - 클릭하면 배경색상을 랜덤 컬러로 변경할 수 있는 기능 (인앱 결제를 통해 enabled 되는 기능)
- Buy RandomColor 버튼 - RandomColor 기능을 기입하는 버튼
SwiftyStoreKit 구현 구조
- 비동기 처리에서 RxSwift, RxCocoa를 사용할 때를 위해서 Observable로 Wrapping하여 구현
- (Rx를 사용하지 않는다면 delegate 패턴을 사용하여 구현)
- 싱글톤으로 구현하는 것보단 protocol을 준수했을때만 해당 scope 안에서 사용할 수 있도록 하는 mix-in 패턴으로 구현
- cocoapods을 사용할때 아래 3가지 프레임워크 사용
pod 'SwiftyStoreKit'
pod 'RxSwift'
pod 'RxCoacoa'
SwiftyStoreKit 사용
- mix-in, Traits 패턴을 사용하기위해 Interface와 Service 정의
import StoreKit
import SwiftyStoreKit
import RxSwift
import RxCocoa
// Interface
protocol IAPTraits {
static var iapService: IAPServiceType { get }
}
extension IAPTraits {
static var iapService: IAPServiceType { IAPService.shared }
}
// Service
protocol IAPServiceType {
func getPaymentStateObservable() -> Observable<SKPaymentTransactionState>
func getLocalPriceObservable(productID: String) -> Observable<String>
func restorePurchaseObservable() -> Observable<Void>
func purchase(productID: String) -> Observable<Void>
}
private final class IAPService: IAPServiceType {
static let shared = IAPService()
private init() {}
}
- 각 메소드에서 observable.onNext()로 Error를 방출하는 케이스에서 사용할 에러타입 정의
enum IAPError: Error {
case invalidProductID(String)
case unknown(Error?)
case failedRestorePurchases([(SKError, String?)])
case noRetrievedProduct
case noRestorePurchases
case noProducts
case canceledPayment
}
- getPaymentStateObservable() -> Observable<SKPaymentTransactionState> 구현
- transaction의 상태를 알 수 있으며, 거래의 상태에 대한 상태값을 얻을 수 있는 메소드
- Observable로 wrapping하기위해서 Observable.create로 정의
func getPaymentStateObservable() -> Observable<SKPaymentTransactionState> {
.create { observer in
SwiftyStoreKit.completeTransactions(atomically: true) { purchases in
for purchase in purchases {
switch purchase.transaction.transactionState {
case .purchased, .restored:
SwiftyStoreKit.finishTransaction(purchase.transaction)
default:
break
}
observer.onNext(purchase.transaction.transactionState)
}
}
return Disposables.create()
}
}
- getLocalPriceObservable(productID: String) -> Observable<String> 구현
- 인수로 들어온 productID의 가격을 조회하는 메소드
func getLocalPriceObservable(productID: String) -> Observable<String> {
.create { observer in
SwiftyStoreKit.retrieveProductsInfo([productID]) { result in
if let product = result.retrievedProducts.first {
let priceString = product.localizedPrice ?? ""
print("Product: \(product.localizedDescription), price: \(priceString)")
observer.onNext(priceString)
} else if let invalidProductId = result.invalidProductIDs.first {
print("Invalid product identifier: \(invalidProductId)")
observer.onError(IAPError.invalidProductID(invalidProductId))
} else {
print("Error: \(String(describing: result.error))")
observer.onError(IAPError.unknown(result.error))
}
}
return Disposables.create()
}
}
- restorePurchaseObservable() -> Observable<Void> 구현
- 구매했던 정보를 불러오는 메소드
func restorePurchaseObservable() -> Observable<Void> {
.create { observer in
SwiftyStoreKit.restorePurchases(atomically: true) { results in
if results.restoreFailedPurchases.count > 0 {
print("Restore Failed: \(results.restoreFailedPurchases)")
observer.onError(IAPError.failedRestorePurchases(results.restoreFailedPurchases))
} else if results.restoredPurchases.count > 0 {
print("Restore Success: \(results.restoredPurchases)")
observer.onNext(())
} else {
observer.onError(IAPError.noRestorePurchases)
}
}
return Disposables.create()
}
}
- purchase(productID: String) -> Observable<Void> 정의
- 인수로 들어온 productID 상품을 구매하는 메소드
func purchase(productID: String) -> Observable<Void> {
.create { observer in
SwiftyStoreKit.purchaseProduct(productID, quantity: 1, atomically: true) { result in
switch result {
case .success:
observer.onNext(())
case .error(let error):
switch error.code {
case .unknown:
print("Unknown error. Please contact support")
case .clientInvalid:
print("Not allowed to make the payment")
case .paymentCancelled:
observer.onError(IAPError.canceledPayment)
case .paymentInvalid:
print("The purchase identifier was invalid")
case .paymentNotAllowed:
print("The device is not allowed to make the payment")
case .storeProductNotAvailable:
print("The product is not available in the current storefront")
case .cloudServicePermissionDenied:
print("Access to cloud service information is not allowed")
case .cloudServiceNetworkConnectionFailed:
print("Could not connect to the network")
case .cloudServiceRevoked:
print("User has revoked permission to use this cloud service")
default:
print((error as NSError).localizedDescription)
}
}
}
return Disposables.create()
}
}
사용하는 쪽
- ViewController에서 IAPTraits를 준수하여, Self.iapService로 위에서 정의한 서비스를 사용할 수 있도록 정의
- UI와 프로퍼티 선언
import UIKit
import RxSwift
import RxCocoa
class ViewController: UIViewController, IAPTraits {
private let restoreButton: UIButton = { ... }
private let colorButton: UIButton = { ... }
private let buyButton: UIButton = { ... }
private let productID = "some product id"
private var canUseRandomColor = false {
didSet {
self.colorButton.isEnabled = self.canUseRandomColor
self.buyButton.isEnabled = !self.colorButton.isEnabled
}
}
}
- viewDidLoad에서 constraint 정의 및 바인딩
- 특정 상품을 구매했는지 확인하는 것은 UserDefaults에도 저장하여 관리
// autolayout은 생략...
// binding
Self.iapService.getPaymentStateObservable()
.withUnretained(self)
.subscribe(onNext: { ss, state in
switch state {
case .purchased, .restored:
ss.canUseRandomColor = true
default:
ss.canUseRandomColor = false
}
})
.disposed(by: self.disposeBag)
Self.iapService.getLocalPriceObservable(productID: self.productID)
.withUnretained(self)
.subscribe(onNext: { ss, price in
ss.buyButton.setTitle("Buy RandomColor (\(price)$)", for: .normal)
})
.disposed(by: self.disposeBag)
guard UserDefaults.standard.bool(forKey: self.productID) == true else { return }
self.canUseRandomColor = true
- Restore 버튼을 눌렀을 때 restore 처리
@objc private func didTapRestoreButton() {
Self.iapService.restorePurchaseObservable()
.withUnretained(self)
.subscribe(onNext: { ss, _ in
// UserDefaults로 해당 product 구입했는지 저장
UserDefaults.standard.set(true, forKey: ss.productID)
ss.canUseRandomColor = true
})
.disposed(by: self.disposeBag)
}
- buyButton을 탭했을때 처리
@objc private func didTapBuyButton() {
Self.iapService.purchase(productID: self.productID)
.withUnretained(self)
.subscribe(onError: { print("error \($0)") })
.disposed(by: self.disposeBag)
}
- color 버튼을 눌렀을 때 처리
@objc private func didTapColorButton() {
self.view.backgroundColor = UIColor(red: CGFloat(drand48()), green: CGFloat(drand48()), blue: CGFloat(drand48()), alpha: 1.0)
}
* 전체 코드: https://github.com/JK0369/ExSwiftyStoreKit
* 참고
'iOS 응용 (swift)' 카테고리의 다른 글
Comments