Swift. In-app Purchase(non-consumable)에 대해서 알아봅니다

In-App Purchase Tutorial: Getting Started, Part 1

Posted by MinJun Ju on Thursday, October 25, 2018 Tags: Swift In-app-Purchase Raywenderlich   25 minute read

In-App Purchase Tutorial: Getting Started에서 필요한 부분을 의역했습니다.


Table of contents


In-App Purchase Tutorial: Getting Started

이 In-App Purchase 튜토리얼에서 사용자에게 잠겨있는 콘텐츠나 기능을 해제 하여 수익을 늘리는 방법에 대해서 알아봅니다.

Update note: 이 튜토리얼은 Xcode 10, Swift 4.2, iOS 11/12로 업데이트 되었습니다.

앱을 만들때 가장 좋은점 중 하나는 앱으로 수익을 창출할수있는 여러가지 선택지가 있다는 것입니다. 기본 바닐라 유료 앱, 광고를 지원하는 앱, 인앱 구매를 지원하는 앱 중 선택할수 있다는 것입니다.

인앱 구매(또는 IAP(in-app purchase))는 앱의 특정 기능 또는 콘텐츠에 사용에 대해 요금을 청구할수 있습니다. IAP의 구현은 몇가지로 이유로 인해서 더 특히 더 중요합니다.

  • 돈을 벌기 위한 추가적인 방법이고, 단순하게 유료앱을 판매하기만 하면 안됩니다. 일부 사용자는 추가 콘텐츠나 기능을 더 많이 사용하려고 합니다.
  • 앱은 무료로 제공될수 있어야하고 대부분의 사람들에게 부담없이 다운로드 하게 할수 있습니다. 무료 앱은 일반적으로 유료앱보다 다운로드 횟수가 더 많습니다. 사용자가 앱을 즐기는 경우 나중에 더 많은 콘텐츠나 기능을 구매할수 있습니다.
  • 광고를 표시하는 무료앱에 IAP 구매를 통해 광고를 지울수 있는 옵션과 함께 배포할수 있습니다.
  • 앱을 처음 출시한후에 돈을 더 벌기위해 새로운 유료 앱을 개발하는 대신 동일한 앱에 새로운 유료 콘텐츠를 추가할수 있습니다.

이 in-app purchase 튜토리얼에서 IAPs를 활용하여 앱에 내장된 추가 콘텐츠의 잠금을 해제 합니다. 기본 Swift, iOS 개념에 익숙해야 합니다. 익숙하지 않다면, 시작하기 전에 Swift 튜토리얼의 범위를 확인하세요. iOS 개발자 센터App Stroe Connect에 접근할수 있는 유료 개발자 계정이 필요합니다.


Getting Started

IAP 튜토리얼에서 RazeFaces라고 불리는 작은 앱을 만들것입니다. 이 앱은 유저에게 RazeFace라는 책과 그림을 위해 이 사이트에서 일반적으로 사용하던 멋진 그림인 RazeFace 구매 할수 있습니다.


여기에서 시작 프로젝트를 다운받고 앱을 시작하여 무엇을 하는지 봅니다. 지금은 아무것도 하지 않습니다. 나중에 구매를 복원하기 위한 네비게이션 바의 단일 Restore버튼이 있는 빈 테이블뷰를 볼수 있습니다.


이 튜토리얼을 마치면, 테이블뷰에서 구매할수 있는 RazeFace의 목록이 표시됩니다. 앱을 삭제 했다가 다시 설치하면 복원 버튼이 이전에 구매한 RazeFaces를 복원합니다.

Xcode로 이동하여 코드를 빠르게 살펴보세요. 메인 뷰 컨트롤러는 MasterViewController.swift 입니다. 이 클레스는 구매 가능한 IAPs 의 목록을 포함하는 테이블뷰를 화면에 표시합니다. 구입한것은 SKProduct 객체의 배열로 저장되어 집니다.

RazeFaceProducts는 앱의 제품에 대한 정보를 가지고 있는 간단한 구조체 입니다. 그리고 IAPHelper은 StoreKit과의 중요한 대화 작업을 수행합니다. 이 메소드들은 지금은 당장 모의 객체 지만, 이 튜토리얼에서 앱에 IAP기능을 추가하는 방법을 설명합니다.

IAP를 포함하기 위한 코드를 작성하기 전에, 먼저 iOS 개발자 센터와 앱스토어 커넥트에서 설정하는것이 필요합니다. Apple Developer Center에서 로그인하고 Certificates, IDs & Profiles를 선택합니다.


그후 Identifiers > App IDs로 가서 오른쪽 상단에 + 버튼을 누르고 새로운 App ID를 생성합니다.

새로운 App ID에 대한 정보를 채웁니다. NameRazeFace IAP Tutorial App을 입력하고 Explicit App ID를 선택하고 유일한 Bundle ID를 입력합니다. 일반적으로 도메인 역순입니다. 다음의 단계에서 번들 아이디를 기억해야합니다.

App Service색션으로 스크롤하고 In-App Purchase, GameCenter가 기본적으로 가능한것으로 체크되어 있는것을 확인합니다. Continue를 클릭하고 Register, Done을 클릭합니다.


Checking Your Agreements

iTunes Connect에서 앱으로 IAPs를 추가하기전에, 다음 두가지를 무조건 해야합니다.

  • developer.apple.com의 최신 애플 개발 프로그램 라이센스 동의를 수락해야합니다.
  • App Store Connect의 계약서, 세금, 청구서 색션에서 최신 유료 애플리케이션 계약을 수락했는지 확인하세요.

아직 이것을 하지 않았다면, iTunes Connect는 다음과같은 경고를 줄것입니다.

위와 같은것을 본다면, 적절한 동의를 수락하는 다음 단계를 따르세요.

iTunes Connect의 Agreements, Tax, Banking 색션을 다시 확인 하는것도 좋습니다.

Paid Applications에 대한 열을 포함하고 있는 Request Contracts라는 제목의 색션을 본다면, Request 버튼을 클릭하고 필수 정보를 작성하고 제출합니다. 요청이 승인되기까지 약간의 시간이 걸리 수 있습니다.

요청이 이미 승인 되어 있다면 아래에 유료앱 목록들을 볼수 있습니다.

Note: Apple는 방금 제출한것들 이후 IAP와 관련된 게약을 승인하는데 몇일이 걸릴수 있습니다. 이 시간동안 앱에서 모든 코드를 올바르게 구현 하더라도 앱에서 IAP 제품을 표시할수 없습니다. 이는 처음으로 IAP를 구현하는 사람들에게 어려운 점입니다.


Creating an App in iTunes Connect

자체 앱을 생성하기 위해 페이지 왼쪽 상단에 App Store Connect를 클릭하고 그후 My apps을 클릭합니다.

페이지의 왼쪽 상단에 + 버튼을 누르고 앱을 추가하기 위해 New App 을 선택하고 양식을 작성합니다.

앱 이름은 앱 스토어에서 고유해야 하므로 여기에서 보이는 앱 이름과 완전히 똑같은 앱 이름은 사용할수 없습니다. 위의 스크린샷에 표시된 예제 제목 뒤에 자신의 이니셜을 추가합니다.

Note: 이 단계를 빠르게 끝냈다면, 번들 ID가 드롭다운 목록에 표시되지 않을수 있습니다. 때로는 Apple 시스템을 통해 전달되는데 시간 걸립니다.

Create를 생성하고 완료합니다


Creating In-App Purchase Products

IAP들을 제공할때 먼저 앱스토어에서 개별 구입 항목을 추가해야 합니다. 스토어에서 판매하기 위한 앱 목록을 가졌다면, 그것과 비슷한 과정이고 구매를 위한 가격 책정 하는 단계들이 포함됩니다. 유저가 구매할때 App Store는 유저에게 복잡한 요금 부과 처리를 하고 이러한 작업에 대한 데이터로 응답합니다.

다음은 추가할수 있는 IAP 의 타입들 입니다.

  • Consumable: 여러번 구매가 가능하고 구매한것을 모두 다 사용할수 있습니다. 추가 생명, 게임 머니, 임시적인 파워업 등에 적합합니다.
  • Non-Consumable: 한번 구매하면 추가 수준 및 잠금해제가 가능한 콘텐츠와 같이 영구적으로 유지될것을 기대합니다. RazeFace 그림이 이 범주에 해당합니다.
  • Non-Renewing Subscription: 일정 기간 동안 사용할수 있는 콘텐츠(고정된 기간)
  • Auto-Renewing Subscription:: 월간 raywenderlich.com 구독과 같은 구독을 반복합니다.

디지털 상품에 대한 IAP만 제공할수 있습니다. 실제 상품이나 서비스는 불가능합니다. 이것에 대한 자세한 정보는 Creating In-App Purchase Products를 참조해주세요.

App Store Connect에서 앱의 전체 상품을 보면서 Features 탭을 클릭하고 그후 In-App Purchase를 선택합니다. 새로운 IAP 제품을 추가하기위해 In-App Purchase의 오른쪽 상단 +을 클릭합니다.

다음과같은 다이얼로그를 볼수 있습니다.

유저가 앱 내에서 RazeFace를 구매할때 항상 앱에서 접근할수 있기를 원하므로 Non-Consumable을 선택하고 생성을 클릭합니다.

다음의 IAP에 대한 자세한 내용을 채웁니다.

  • Refernce Name: iTunes Connect 내의 IAP 식별자 닉네임 입니다. 이 이름은 앱의 어디에서도 표시되지 않습니다. 구매로 잠금 해제하여 사용할 RazeFace의 제목은 Swift Shopping 이므로 여기에서 작성합니다.
  • Product ID: IAP를 식별하는 고유한 문자열 입니다. 일반적으로 번들 ID로 시작한 다음 구매 가능한 항목의 고유한 이름을 추가하는것이 가장 좋습니다. 이 튜토리얼에서 RazeFace를 잠금 해제 하여 나중에 사용하게 될 것이므로 swiftshopping에 다음과 같이 추가해야합니다. com.theNameYouPickedEarlier.razefaces.swiftshopping
  • Cleared for Sale: IAP 의 판매 가능하게 하거나 불가능하게 하는것 입니다.
  • Price Tire: IAP의 가격, Tire 1을 선택합니다.

이제 스크롤을 내려서 Localization section으로 이동하고 거기에 English(U.S)의 기본 항목을 확인합니다. 화면에 표시하는 Name, Description 두 곳에 Swift Shopping를 입력합니다. 저장을 누릅니다. 첫번째 IAP 제품을 생성 했습니다!

Note: App Store Connect는 IAP에 대한 메타 데이터가 누락 되었다고 이야기할수 있습니다. 리뷰로 제출하기 전에, 페이지의 하단에 IAP 스크린샷을 추가해야합니다. 이 스크린샷은 Apple’s review를 위해서만 사용되고 App Store 목록에는 보여지지 않습니다.

코드를 탐구하기 전에 한 단계를 더 거쳐야합니다. 앱 개발 환경에서 IAP를 테스트할때 Apple은 금융 거래 처리를 생성하지 않고 IAP 제품을 테스트할수 있는 purchase 테스트 환경을 제공합니다.

이 특정 구매 테스트는 App Store Connect의 특정 Sandbox Tester 유저 계정을 사용해야만 가능합니다. 이제 거의 코드에 다왔습니다.


Creating a Sandbox User

App Store Connect에서 메인 메뉴로 돌아가서 User and Roles(지금은 Users and Access)를 선택하고 그후 Sandbox Testers 탭을 클릭하고 Tester 타이틀 옆에 +를 클릭 합니다.

정보를 입력하고 Save를 클릭합니다. 먼저 테스트 사용자의 이름과 성은 가상의 이름으로 작성할수 있지만, 이메일은 실제 이메일 주소를 작성해야합니다. Apple이 해당 주소로 확인 이메일을 보내고 이메일을 받으면 링크를 클릭하여 주소를 확인해야 합니다.

입력한 메일 주소는 Apple ID 계정과 연결되어 있지 않아야 합니다. gmail 계정이 있습다면, 새로운 계정을 만드는 대신 주소 별칭을 사용할수 있습니다.

Note: 불행히도 non-consumable IAP를 새로 구매하는것을 테스트 하는것은 새로운 sandbox tester(그리고 이메일 주소)가 필요합니다. 동일한 샌드박스 테스터를 사용하여 반복구매 한 항목은 이미 구입한 항목을 복원하는 것으로 간주되므로 새 구매에 대한 코드는 실행되지 않습니다.

새로운 구매 코드를 통한 여러번의 테스트가 필요하고 이메일 제공 업체가 qualifiers를 지원하지 않는 경우 테스트 용도로만 consumable IAP를 지정하는것이 좋습니다. 각 테스트 후에 기기에서 앱을 삭제하면 소모품 IAP 구매는 새로 구매 해야 되는것으로 간주 됩니다.

사용할수 있는 전략은 가능한 여러번의 실패 사례를 테스트하는것입니다. 그렇게하면 훨씬 적은 샌드박스 테스터만 만들게 됩니다. 일반적으로 사용자는(샌드 박스 유저중 한명) non-consumable IAP를 한번 구매하면 더이상 구매할수 없고 복원만 할수 다는 규칙을 기억하세요.

좋습니다. 이제 테스터 유저를 가지게 되었고 앱에서 IAP를 구현할수 있게 되었습니다.


Project Configuration

모든것이 올바르게 작동하기 위해 앱에서 bundle identifier, product identifier을 앱스토어 커넥트와 개발자 센터에서 생성했던 것과 일치시키는게 중요합니다.

Xcode 스타터 프로젝트로 돌아가서 프로젝트 탐색기에 RazeFaces 프로젝트를 선택합니다. 그후 올바른 팀을 선택하고, 번들 ID를 입력합니다.

Capabilities탭을 선택하고 In-App Purchase를 활성화 합니다.

Note: IAP가 표시되지 않으면, Xcode 환경 설정의 계정 색션에서 앱 ID를 만드는데 사용한 Apple ID로 로그인했는지 확인하세요.

RazeFaceProduct.swift를 엽니다. 내가 생성한 SwiftShopping IAP 제품으로 참조하는 placeholder가 있는것을 인지합니다. 이것을 App Store Connect에서 구성했던 완전한 Product ID로 교체합니다.

public static let SwiftShopping = "com.theNameYouPickedEarlier.razefaces.swiftshopping"

Note: 제품 식별자 목록은 웹서버에서 가져올수 있으므로 새로운 IAP들은 앱을 업데이트 하지 않고 동적으로 추가할수 있습니다. 이 튜토리얼은 단순하게 유지하고 하드코딩된 제품 식별자를 사용합니다.


Listing In-App Purchases

RazeFaceProdctsstore 속성은 IAPHelper의 인스턴스입니다. 이전에 언급했던것처럼 이 객체는 구매 목록을 작성하고 실행하기 위해 StoreKit API와 상호작용하는 객체 입니다. 첫번째 task는 IAPs의 목록을 검색하기 위해 IAPHelper을 갱신 합니다 - 지금 까지 Apple의 서버에는 단 한가지의 제품 밖에 없습니다.

IAPHelper.swift를 열고 클레스의 상단에 다음 private 속성을 추가합니다.

private let productIdentifiers: Set<ProductIdentifier>

그후 super.init() 이전의 init(productIds:)로 다음을 추가합니다.

productIdentifiers = productIds

IAPHelper 인스턴스는 product identifiers의 set이 전달되어 생성되어 집니다. 이것은 RazeFaceProducts가 자체 store 인스턴스를 생성하는 방법입니다.

다음으로 아래의 다른 private 속성들을 방금전에 추가한것 아래에 추가합니다.

private var purchasedProductIdentifiers: Set<ProductIdentifier> = []
private var productsRequest: SKProductsRequest?
private var productsRequestCompletionHandler: ProductsRequestCompletionHandler?

purchasedProductIdentifiers는 구매한 아이템을 추적합니다. 다른 두개의 속성은 Apple 서버로 요청을 수행하기 위해 SKProductsRequest의 델리게이트를 사용하기 위해 사용됩니다.

다음으로 requestProducts(_:)의 구현을 다음과같이 변경합니다.

public func requestProducts(completionHandler: @escaping ProductsRequestCompletionHandler) {
  productsRequest?.cancel()
  productsRequestCompletionHandler = completionHandler

  productsRequest = SKProductsRequest(productIdentifiers: productIdentifiers)
  productsRequest!.delegate = self
  productsRequest!.start()
}

이 코드는 차후에 유저를 위해 실행되는 completion handler를 저장합니다. 그 후 SKProductReqeust 객체를 통해 Apple로 요청을 생성하고 초기화합니다. 여기는 하나의 문제가 있습니다: 이 코드는 요청의 delegate로서 IAPHelper로 선언 되었지만, 아직 SKProductsReqeustDelegate 프로토콜을 체택하지 않았습니다.

이것을 고치기 위해 다음 확장을 IAPHelper.swift의 끝에 추가합니다.

// MARK: - SKProductsRequestDelegate

extension IAPHelper: SKProductsRequestDelegate {

  public func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
    print("Loaded list of products...")
    let products = response.products
    productsRequestCompletionHandler?(true, products)
    clearRequestAndHandler()

    for p in products {
      print("Found product: \(p.productIdentifier) \(p.localizedTitle) \(p.price.floatValue)")
    }
  }
  
  public func request(_ request: SKRequest, didFailWithError error: Error) {
    print("Failed to load list of products.")
    print("Error: \(error.localizedDescription)")
    productsRequestCompletionHandler?(false, nil)
    clearRequestAndHandler()
  }

  private func clearRequestAndHandler() {
    productsRequest = nil
    productsRequestCompletionHandler = nil
  }
}

추가 확장은 SKProductReqeustDelegate 프로토콜의 두개의 매소드 요구 사항을 구현하여 Apple의 서버에서 제품의 목록, 타이틀, 설명과 가격을 얻는데 사용됩니다.

productsReqeust(_:didReceive:)는 목록을 성공적으로 찾았을때 호출됩니다. 이것은 SKProduct배열 객체를 받고 이전에 저장된 completion handler로 보냅니다. 이 handler는 새로운 데이터와 함께 테이블뷰를 reload 합니다. 문제가 발생하면, reqeust(_:didFailWithError:)가 호출됩니다. 이 경우에 요청이 끝나면, 요청과 completion handler는 clearReeustAndHandler()과 제거되어 집니다.

앱을 빌드하고 실행합니다.

Note: IAP 제품들을 iOS 시뮬레이터, 실제 iOS 디바이스에서 화면에 표시할수 있습니다. 하지만 제품을 구매 또는 복원 테스트하길 원한다면, 이것은 실제 디바이스에서만 할수 있습니다. 구매색션 아래에서 더 자세하게 설명합니다.

Note: 성공적으로 실행되지 않거나 제품 목록을 볼수 없다면, 몇가지 확인 해야 할것들이 있습니다.

  1. 이 프로젝트의 Bundle ID는 iOS 개발자 센터의 App ID와 일치 합니까?

  2. SKProductReqeust를 생성할때 full product ID가 사용되나요?(RazeFaceProducts의 productIdentifiers 속성을 확인하세요.)

  3. iTunes Connect에서 유료 애플리케이션 계약이 유효한가요? pending 상태에서 수락 상태까지 시간이 걸릴수 있습니다.

  4. Apple 개발자 시스템 상태를 확인하세요. 또는 여기를 참조하세요. 상태 값으로 응답하지 않는다면, iTunes sandbox는 다운됫을 것입니다. Apple Store 문서의 Apple’s Validating Receipts에 설명되어 있습니다.

  5. 앱 ID에 IAP가 사용가능으로 설정되어 있나요?

  6. 기기에서 앱을 삭제하고 다시 설치해보셨나요?


Purchased Items

구입한 아이템을 확인할수 있어야 합니다. 이것을 하려면 이전에 추가한 purchasedProductIdentifiers 속성을 사용해야 합니다. product identifier가 이 set에 포함되어있다면, 유저가 해당 아이템을 구매한것 입니다. 이를 확인하는 방법은 간단합니다.

IAPHelper.swift에서 isProductPurchased(_:)에서 retruen 조건문을 다음과같이 변경합니다.

return purchasedProductIdentifiers.contains(productIdentifier)

로컬에서 구매 상태를 저장하면 앱이 시작할때마다 Apple의 서버로 이러한 데이터를 요청할 필요성을 줄입니다(alleviate). purchasedProductIdentifiersUserDefaults를 사용하여 저장되어 집니다.

init(productIds:)를 다음과같이 변경합니다.

public init(productIds: Set<ProductIdentifier>) {
  productIdentifiers = productIds
  for productIdentifier in productIds {
    let purchased = UserDefaults.standard.bool(forKey: productIdentifier)
    if purchased {
      purchasedProductIdentifiers.insert(productIdentifier)
      print("Previously purchased: \(productIdentifier)")
    } else {
      print("Not purchased: \(productIdentifier)")
    }
  }
  super.init()
}

각 제품 식별자에 해서 UserDefaults에 값이 저장되어있는지 아닌지 확인합니다. 저장되어 있다면, 식별자를 purchasedProductIdentifiers set에 추가합니다. 그후 식별자를 구매 이후에 set에 추가합니다.

Note: User defaults는 실제 애플리케이션에서 구매한 제품에 대한 정보를 저장하는 최고의 장소는 아닐것입니다. 크렉된 장치의 소유자는 앱의 UserDefaults plists에 쉽게 접근할수 있으며 이를 수정하여 구매를 잠금해제 할수 있습니다. 이런 종류의 문제가 걱정 된다면 Apple의 Validating App Store Receipts를 확인해볼 필요가 있습니다. 이렇게하면 사용자가 특정 구매를 했는지 확인할수 있습니다.


Making Purchases(Show Me The Money!)

유저가 구매한것을 아는것은 좋지만 처음 장소에서 구매할수 있게 만들어야 합니다. 구매 기능은 다음 단계에서 구현합니다.

buyProduct(_:)를 다음과같이 변경합니다.

public func buyProduct(_ product: SKProduct) {
  print("Buying \(product.productIdentifier)...")
  let payment = SKPayment(product: product)
  SKPaymentQueue.default().add(payment)
}

이것은 Payment queue에 추가하기위해 SKProduct(에플 서버에서 검색했던)을 사용하여 지불 객체를 생성합니다. 이 코드는 SKPaymentQueue싱글톤 객체의 default()를 호출하여 사용합니다. 은행에 돈이 지불되었거나 그렇지 않은지 어떻게 알수 있나요?

지불 검증(Payment verification)은 SKPaymentQueue에서 일어나는 처리(transaction)를 IAPHelper가 관찰하여 이루어집니다. SKPaymentQueue transaction 옵저버로서 IAPHelper를 설정하기 전에 클레스는 반드시 SKPaymentTransactionObserver 프로토콜을 체택해야 합니다.

IAPHelper.swift의 맨 아래 끝까지 가고 다음 코드를 추가합니다.

// MARK: - SKPaymentTransactionObserver
 
extension IAPHelper: SKPaymentTransactionObserver {
 
  public func paymentQueue(_ queue: SKPaymentQueue, 
                           updatedTransactions transactions: [SKPaymentTransaction]) {
    for transaction in transactions {
      switch transaction.transactionState {
      case .purchased:
        complete(transaction: transaction)
        break
      case .failed:
        fail(transaction: transaction)
        break
      case .restored:
        restore(transaction: transaction)
        break
      case .deferred:
        break
      case .purchasing:
        break
      }
    }
  }
 
  private func complete(transaction: SKPaymentTransaction) {
    print("complete...")
    deliverPurchaseNotificationFor(identifier: transaction.payment.productIdentifier)
    SKPaymentQueue.default().finishTransaction(transaction)
  }
 
  private func restore(transaction: SKPaymentTransaction) {
    guard let productIdentifier = transaction.original?.payment.productIdentifier else { return }
 
    print("restore... \(productIdentifier)")
    deliverPurchaseNotificationFor(identifier: productIdentifier)
    SKPaymentQueue.default().finishTransaction(transaction)
  }
 
  private func fail(transaction: SKPaymentTransaction) {
    print("fail...")
    if let transactionError = transaction.error as NSError?,
      let localizedDescription = transaction.error?.localizedDescription,
        transactionError.code != SKError.paymentCancelled.rawValue {
        print("Transaction Error: \(localizedDescription)")
      }

    SKPaymentQueue.default().finishTransaction(transaction)
  }
 
  private func deliverPurchaseNotificationFor(identifier: String?) {
    guard let identifier = identifier else { return }
 
    purchasedProductIdentifiers.insert(identifier)
    UserDefaults.standard.set(true, forKey: identifier)
    NotificationCenter.default.post(name: .IAPHelperPurchaseNotification, object: identifier)
  }
}

위의 자세한 코드에 대한 자세한 설명은 아래에 있습니다. 다행히 각 메소드는 매우 짧습니다.

paymentQueue(_:updatedTransaction:)는 프로토콜에 요구되는 실제 메소드 입니다. 하나 이상의 transaction 상태가 변경되면 호출되어집니다. 이 매소드는 갱신된 trnasaction의 배열에 각 transaction의 상태를 평가하고 관계된 도우미 메소드들(complete(transaction:), restore(transaction:), fail(transaction:))들을 호출합니다.

transaction이 성공하거나 복원되면 구입의 세트로 추가되고 UserDefaults에 식별자가 저장됩니다. 또한 해당 transaction과 함께 알림을 발행하고 이 알람을 듣고 있는 객체가 사용자 인터페이스 업데이트와 같은 작업을 수행할수 있습니다. 마지막으로 성공, 또는 실패 모두의 경우에 transaction을 완료로 표시합니다.

남은 할일은 IAPHelper를 지불 payment transaction 옵저버로 연결하는 것입니다. IAPHelper.swift에서 init(productIds:)로 돌아가서 super.init()이후에 다음 코드를 추가합니다.

SKPaymentQueue.default().add(self)

Making a Sandbox Purchase

앱을 빌드하고 실행합니다 - 하지만 구매 테스트를하기 위해서는 iOS 기기에서 실행해야합니다. sandbox tester은 요금 청구없이 구매를 이행하기 위해 이전에 생성했던 계정을 테스터 계정을 사용합니다.

쇼핑할수 있는 샌드박스 테스터를 가졌다면, 테스터 계정을 어떻게 사용하는지에 대한 설명은 아래에 있습니다.

iPhone로 가서 App Store 계정을 로그아웃 합니다. 이것을 하려면 Setting 에서 iTunes & App Store를 탭합니다.


iClould account name을 탭하고 그후 Sign out 합니다. 이 시점에서 실제 sandbox user로 로그인 하면 안됩니다. 샘플 앱에서 IAP를 구매하려고 하면 이 작업을 수행 하라는 메시지를 받을 것입니다.

Login 하라는 알림창이 보이게 되면, Use Exisiting Apple ID를 탭하고, 이전에 생성한 sandbox 계정에 대한 로그인 정보를 입력합니다.

Buy을 탭하여 구입을 확인하고 경고 뷰는 이것에 대해 요금을 지불하지 않는 Sandbox 환경이라는 내용을 표시합니다.

마지막으로 경고창은 성공적으로 구매를 완료했다는걸 보여줍니다. 구매 처리가 완료되면, 체크 마크가 구매 아이템 옆에 보입니다. 마침내 이 Swift Shopping RazeFace를 보내될것입니다.


Restoring Purchases

유저가 앱을 지우거나 재설치하거나 다른 기기에서 설치하면 이전 구매 목록에 접근하는것이 필요합니다. 사실, Apple은 복원할수 없는 non-consumable purchases이면 리젝 합니다.

구매 transaction 옵저버로서 IAPHelper는 구매가 복원되었을때 이미 알람을 받습니다. 다음 단계는 구매를 복원하여 이 알림에 반응하는 것입니다.

IAPHelper.swift를 열고 파일의 아래로 스크롤을 내립니다. StoreKit API extension 에서 restorePurchase()를 다음과같이 변경합니다.

public func restorePurchases() {
  SKPaymentQueue.default().restoreCompletedTransactions()
}

너무 쉽습니다. 이미 trnasaction 옵저버가 설정되어 있고 이전 단계에서 복원 transaction을 처리하는 매소드를 구현 했습니다. 이전에 구입한 제품 옆에 체크 표시를 봐야합니다.


Payment Permissions

어떤 기기와 계정은 IAP가 허가되지 않을수 있습니다. 예를들어 이것이 발생하면, 부모의 컨트롤이 이것을 허가하지 않는 경우 Apple은 이 상황을 정상적으로 처리하길 요구합니다. 그렇게 하지 않으면 앱은 리젝 당할수 있습니다.

IAPHelper.swift를 다시 열고 StoreKit API extension의, canMakePayments()에서 return라인에 다음과같은 코드를 추가합니다.

return SKPaymentQueue.canMakePayments()

Product cell은 canMakePayments()에서 반환한 값에 따라서 다르게 동작해야 합니다. 예를들어 canMakePayments()가 false를 반환하면 구매버튼이 보이지 않아야 하고 가격은 Not Available로 대체 되어야 합니다.

이를 수행하려면 ProductCell.swift를 열고 제품 속성의 didSet 처리기의 전체 구현을 다음과같이 변경합니다.

didSet {
  guard let product = product else { return }
 
  textLabel?.text = product.localizedTitle
 
  if RazeFaceProducts.store.isProductPurchased(product.productIdentifier) {
    accessoryType = .checkmark
    accessoryView = nil
    detailTextLabel?.text = ""
  } else if IAPHelper.canMakePayments() {
    ProductCell.priceFormatter.locale = product.priceLocale
    detailTextLabel?.text = ProductCell.priceFormatter.string(from: product.price)
 
    accessoryType = .none
    accessoryView = self.newBuyButton()
  } else {
    detailTextLabel?.text = "Not available"
  }
}

이 구현은 기기에서 구매가 불가능할때 더 적절한 정보를 화면에 표시합니다. 그리고. IAP를 가진 앱을 가지게 되었습니다!


Where To Go From Here?

이 프로젝트의 완성된 버전은 여기에서 다운 받을수 있습니다.

Sna Davies의 In-App Purchase Video Tutorial Series은 오늘 다룬 모든 주제들을 다룹니다. 그리고 3부에서 validating receipts에 관해 이야기 합니다.

셈플앱의 한가지 단점은 Apple와 통신할때 사용자에게 알려주지 않는다는 것입니다. 적절한 시간에, loading 인디케터, HUD control을 화면에 보여주어서 개선이 가능합니다. HUD control에 대한 더 자세한 정보는 The iOS Apprentice책의 색션 3를 참조해주세요.

Apple은 IAP를 위한 훌륭한 페이지를 가지고 있습니다. 이곳에 IAP와 관계된 문서, WWDC 비디오 링크가 모여있습니다.