Swift. In-app Purchase(Non-Renewing Subscriptions)에 대해서 알아봅니다

In-App Purchases: Non-Renewing Subscriptions Tutorial, Part 3

Posted by MinJun Ju on Saturday, October 27, 2018 Tags: Swift In-app-Purchase Raywenderlich   30 minute read

In-App Purchases: Non-Renewing Subscriptions Tutorial에서 필요한 부분을 의역했습니다.


Table of contents


In-App Purchases: Non-Renewing Subscriptions Tutorial

Non-Renewing Subscriptions 튜토리얼에서 In-App Purchases를 사용하여 유저에게 무제한 콘텐츠를 제공 하고 수익을 창출하는 방법에 대해서 배워봅니다.

iOS는 4개의 큰 IAP(In-App Purchases) 타입을 가집니다.

  1. Consumables: 유저는 같은 제품을 여러번 구매할수 있습니다. 이것들은 다 사용하거나, 다시 구매 할수 있습니다. 무료 게임 앱에서 포션, 추가 생명 등에 해당합니다.
  2. Non-consumables: User는 한번만 구입하면 아이템에 무제한으로 접근합니다. 예로는 추가 레벨 패키지 또는 다운로드 가능한 콘텐츠 입니다.
  3. Auto-renewable Subscriptions: 설정한 기간동안 서버로 접근가능한 콘텐츠 또는 동적 콘텐츠 입니다 - 지속적으로 접근하기 위해 유저에게 구독을 구입하게 합니다. 예를들어 전자 매거진을 구독하고 한달동안 앱에서 추가 기능을 잠금해제하는 등의 작업을 할수 있습니다. 이러한 구독은 사용자가 취소하기로 결정할때가지 무기한 자동 갱신 됩니다.
  4. Non-renewing Subscriptions: 정적인 콘텐츠 또는 서버로 접근하는데 시간제한이 있는 접근입니다. 유저는 이것이 만료되면 수동으로 다시 갱신해야 합니다.

이들의 구독은 두가지 타입입니다 : 자동갱신(auto-renewable)비-자동갱신(non-renewing) 입니다. 이 튜토리얼은 non-renewing subscriptions에 초점을 맞춥니다.

Note: 최근 몇년동안 auto-renewing subscriptions이 크게 변경되었습니다. 처음에 이들은 Newsstand apps으로 제한되어 있었고 몇가지 엄격한 규칙이 적용된 카테고리가 추가 되었고 이제 iOS 10이 출시됨에 따라서 광범위한 유형의 앱이 이러한 타입의 이점을 얻게 되었습니다. 이들을 사용하기 위한 결정에 도움을 주기위해 비-자동갱신 구독(non-renewing subscriptions)보다 자동갱신 구독(auto-renewing subscriptions)의 이점을 주목하는게 중요합니다.

이 튜토리얼에서 InsomniOwl이라고 불리는 앱으로 비-자동갱신 구독을 추가 합니다. 이 앱은 부엉이 이미지를 볼수 있는 앱입니다. 유저와 이들이 구매한 구독을 추적을 유지하기 위해 제공하는 back-end 서비스인 Heroku와 Parse Server를 사용하여 유저와 이들의 구매 구독을 추적할수 있스빈다.

시작 하기전에 다음을 완료하거나 경험이 있어야합니다.


When to use Non-Renewing Subscriptions

이것은 분명해보일지도 모르지만 여기에서 iOS의 구독 유형에 대해서 좀더 자세하게 설명 합니다.

Auto-renewable subscriptions

유저가 자동 갱신 구독을 구입했을때 이들이 수동으로 구독을 취소할때까지 지속적으로 요금을 청구합니다. 이것은 개발자 관점에서 확실히 좋습니다. 왜냐하면 무언가를 취소하려면 많은 노력이 필요하기 때문입니다. 즉 Apple은 StoreKit 프레임 워크를 통해서 구독 기간을 설정, 또는 갱신 하고 이것들을 자동으로 관리하는것을 의미합니다.

Apple은 또한 자동갱신을 사용하는 개발자에게 수입 인센티브를 제공합니다. 구독자가 자동갱신 구독을 1년이상 지속하고 다음해에도 자동 갱신 구독을 계속한다면, Apple은 판매금액의 개발자 할당 부분을 70% 에서 85%로 증가시킵니다.

iOS 10 부터 자동 갱신 구독은 Newsstand 타입의 앱으로 제한되지 않고 모든 앱 카테고리에 허용됩니다.

Non-renewing subscriptions

유저가 비-자동 갱신 구독에 가입했을때 이들은 특정 기간동안에만 구독합니다(1달, 3달, 등) 구독 기간이 끝나면 콘텐츠에 접근도 종료 됩니다. 콘텐츠에 계속 접근하기 위해서 이들은 재-구독 해야합니다.

이것은 개발자의 관점에서 사용자에게 지속적으로 구독 결정을 요청해야 하기때문에 바람직하지 않습니다. 또한 코드가 만료 날짜를 추적해야 한다는걸 의미하기도 합니다. 만료일은 여러 장치를 가지고있는 사용자에 대해서는 추적이 조금 까다로울수도 있습니다.

지금까지는 이점이 없이 비-자동갱신 구독이 추가적인 작업을 발생시키는것처럼 보입니다. 많은 경우에 이것은 사실일수 있지만 여전히 의미있는 상황이 있을수 있습니다. 예를 들면 장기 구독에 대한 혜택을 제공하지 않는 정적 아카이브에 대한 구독을 사용자에게 제공하는 경우, 정원 가꾸기 팁 또는 겨울 엘크 사냥 여행 정보와 같은 계절 정보를 제공하는 앱일수 있습니다. 한달 또는 특정 기간 동안에만 해당하는 구독을 유지하는 경우일수 있습니다.


Implementing Non-Renewing Subscriptions: Overview

좋습니다. 비-자동갱신 구독 제국 건설을 시작하기로 결정했습니다. 이것을 개발할때 핵심은 무엇인가요?

StoreKit 프레임워크는 구독 기간과 갱신을 처리하는 자동 갱신 구독과 달리 비-자동 구독 갱신은 개발자에게 모든 무거운 작업들을 처리하길 요구합니다.

다음은 non-renewing subscriptions을 구현할때 고려 해야 할것들 입니다.

  • 구독 기간은 StoreKit에 의해서 관리되지 않습니다. 그래서 구매 시점에서 기간을 계산하는것이 필요합니다.
  • consumable products와 마찬가지로 여러번 구입할수 있어야합니다 그렇기 때문에 기존 구독에 남은 시간을 확인하고 사용자가 갱신을 선택하는 경우 기존 시간과 새로운 시간을 계산하는 방법을 포함해야합니다.
  • 또한 사용자가 여러 장치에서 구독할수 있도록 해야합니다. 일반적으로 이 요구사항을 수용하기 위해 두가지 가능한 옵션이 있습니다.

iCloud. iCloud 계정은 이들에게 베타적이지만 사용자의 기기들을 공유하기에 효과적인 방법입니다. 하지만 앱이 공유 플렛폼 이거나, 다른 웹앱이 있다면, iCloud가 iOS 기기만 사용하도록 제한 하기 때문에 최상의 선택이 아닙니다.

백엔드 서비스 또는 BaaS. 구독을 위해 계정 생성을 요구하여 구독 만료 날짜, 계정등 필요한 모든 데이터를 서버의 계정에 저장할수 있습니다. 이 방법은 유저의 로그인을 요구하는것으로 모든 플렛폼에 간단하게 구독을 공유할수 있습니다.

이 튜토리얼에서 사용하기 쉽고 대중적인 백엔드로 Heroku가 있는 Parse Server를 사용하여 정보를 저장할것입니다.


Getting Started

여기에서 시작 프로젝트를 다운받습니다. 시작 프로젝트에는 이미 StoreKit 처리, 제품 ids등 기본 사항이 설정되어 있습니다. 또한 Parse Server로 로그인하기 위한 코드가 포함되어 있지만 비-자동갱신 구독과 서버 설정과 직접적으로 관련있는 모든 것이 아직 구현되지 않았습니다.

할일목록에서 각 단계에서 해야할것들이 무엇인지 확인합니다.


To Do List

  1. Parse Server 복사본 가져오기
  2. Parse Server로 새로운 Heroku app 연결하기
  3. 서버 데이터 베이스를 위한 MongoLab 사용하여 앱에 설정
  4. InsomniOwl 앱을 Heroku에 연결합니다
  5. iTunes Connect로 새로운 제품을 연결합니다
  6. Sandbox Tester을 설정합니다.
  7. InsomniOwl 앱으로 새로운 제품을 추가합니다.
  8. 만료일 처리
  9. 화면에 콘텐츠 보여주기
  10. 최종 마무리

이제 무엇을 해야할지 알았습니다. 시작해봅시다.


Parse Server & Heroku

먼저, 이 튜토리얼 위해 Heroku에 앱을 설정해야합니다. 이것을 하기위해 다음을 해야합니다.

  • 1. Parse Server Github으로 이동합니다.
  • 2. 오른쪽 상단에 Fork 버튼을 클릭하고 기존 레포를 선택합니다. Heroku와 통합할수 있는 Parse Server example의 복사본을 생성합니다.

  • 3.Fork한 레포에서 스크롤을 내려서 Deploy to Heroku 버튼을 클릭합니다.

  • 4.새로운 Heroku 계정을 생성하거나 아이디가 있다면, 로그인 합니다.

  • 5. Parse Serer Example 앱이 보입니다. 앱의 이름을 입력합니다.

  • 6. Mongo Lab 추가는 무료 이지만 신용카드 정보를 입력해야 할수 있습니다. 나머지 필드 채우기를 완료하세요. PARSE_MOUNT값은 기본(\parse) 으로 그대로 두고 SERVER_URL필드에 yourappname을 교체하고(이때 yourappname을 app name으로 할수 있습니다. 하지만 .herokuapp.com/parse부분은 남겨 놓아야 합니다.

Note: APP_IDMASTER_KEY 필드에 임의 characters 를 입력합니다. 이들은 비밀스런 값으로 다른사람들이 이 앱의 백엔드로 접근하는걸 방지 합니다. 앱에서 이들을 짧게 사용합니다.

Error unauthorized를 받는다면, 이 튜토리얼의 댓글을 확인 해주세요.

  • 7. Deploy를 클릭하고 배포 처리가 완료될때까지 기다립니다. 자동 처리는 Parse SDK 서버를 통해 Heroku로 복사하고 접근에 필요한 변수들을 설정합니다.

  • 8. 배포가 완료되면, Manage App을 클릭하고 애플리케이션 변수들을 보기위해 Setting를 클릭합니다.

  • 9. 배포되고있는 동안 생성된 구성 변수를 보기위해 Reveal Config Vars 버튼을 클릭합니다. Xcode 프로젝트로 복사하기 위한 3개의 변수가 있습니다: APP_ID, MASTER_KEY, SERVER_URL 입니다.

이제 Heroku 및 Parse Server 설정을 마쳤으니 시작프로젝트를 엽니다. AppDelegate.swift를 열고 application(_:didFinishLaunchingWithOptions:) 에서 applicationID, clientKey, server 속성값을 Heroku’s의 구성값으로 변경합니다.

let configuration = ParseClientConfiguration {
  $0.applicationId = "com.razeware.InsomniOwl.somethingRandomHere"
  $0.clientKey = "myMasterKeyKeepItSecret"
  $0.server = "https://insomniowl.herokuapp.com/parse"
}

Note: server속성의 접두사는 https이여야 합니다. http가 아닙니다. 그렇지 않으면, Parse Server에 접근하지 못하고 Console에 에러를 표시합니다: The resource could not be loaded because the App Transport Security policy requires the use of a secure connection.

앱을 빌드하고 실행합니다. 앱이 처음 실행되면 제품목록을 화면에 보여주기 전에 계정을 생성해야합니다.

새로운 계정을 생성하기 위해 username 과 password를 입력하고 Sign Up을 선택합니다. 완료되면 다음과 같이 보아야 합니다.


몇초 기다리세요. 멋진 올빼미 이미지들이…안보인다면 iTunes Connect 에서 제품을 등록하지 않은것입니다.


Itunes Connect: Add In-App Purchase Items

앱에서 이들을 보기 위해 iTunes Connect 로 IAP 제품을 등록해야합니다.

  1. iTunes Connect로 로그인 합니다.
  2. My Apps을 클릭합니다.
  3. 새로운 iOS 프로젝트를 클릭하고
  4. 새로운 앱을 클릭하고 Features 를 클릭합니다
  5. In-App Purchases를 클릭하고 +버튼을 누릅니다

앱에 다양한 제품들을 제공하려면 Consumable, Non-Consumable, Non-Renewing Subscriptions을 추가하세요. 튜토리얼에는 name, product ID, price만 입력합니다. 제품 ID는 역 도메인, 앱 이름은 제품 이름이어야 합니다. 예를들어: com.yoursite.yourappname.3monthsOfRandom 같이 앱이 올바른 올빼미 이미지를 가지려면 각항목의 마지막 부분(3monthsOfRandom)이 아래에 나열된 것과 완벽하게 일치 해야합니다.

또한 Cleared for Sale 박스를 선택해야 앱에 아이템이 표시되지 않습니다.

특정 Product ID 접미사를 사용하여 다음 아이템을 입력하세요.

  • 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: 구독 기반 IAP의 기간을 지정하는것은 중요합니다. 이를 수행하는 가장 일반적인 방법은 이름 또는 설명에 표시하는 것입니다. Apple은 구독 기간을 명확하게 기재하지 않으면 앱을 거부할수 있습니다.

모든것이 끝나면 다음과같이 볼수 있습니다.

지정한 Product ID들을 올바르게 가져와야합니다. Product Ids의 접두사는 반드시 app bundle Identifier와 일치해야 합니다. 접미사는 고유하게 제품을 설명해야합니다. 예를들면: com.yourwebsite.yourappname.3monthsOfRandom.

앱에서 고유한 Bundle Identifier를 사용한다면, 시작 프로젝트의 Product ID를 업데이트 해야합니다. iTunes Connect에서 설정한 제품 ID와 일치하도록 OwlProducts.swift를 열고 productIdsNonConsumables를 다음과같이 업데이트 합니다.

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

  1. IAP 테그의 이전 글들에서 해당 정보를 얻을수 있습니다.
  2. sendbox tester을 통해서 제품을 구입합니다.

Non-consumable 아이템은 앱에서 볼수 있고 판매할 준비가 되었습니다. 비-자동갱신 구독 구현을 시작할 준비가 됬습니다.


Adding Your Subscriptions to the Product List

이전에 iTunes Connect에서 구독과 consumable을 추가 했습니다. 이들은 2개의 구독 기간(3달, 6달)을 선택 할수 있는 옵션을 제공합니다. 구독은 사용자에게 어떤식으로돈 유익함을 제공해야 하고 여기에는 랜덤 올빼미 제품을 제공합니다. 구독한 사용자는 유저는 임의의 올빼미 이미지를 무제한으로 볼수 있습니다.

새로운 제품 식별자를 시작 프로젝트에 추가합니다. OwlProducts.swift를 열고 PurchaseNotification 속성 아래에 다음 코드를 추가합니다.

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")
  ]

위의 이 코드는 각 제품 id와 구매 유형에 따라서 적절하게 그룹화합니다. UIImages의 배열은 사용자가 계속 사용할수 있는 임의의 이미지를 나열합니다.

Note: 다시 한번 iTunes Connect 항목과 일치하도록 웹 사이트, 애플리케이션 이름을 기반으로 제품 ID를 입력해야하지만 제품 이름은 변경 하지 마세요. 예 om.yourwebsite.yourappname.3monthsOfRandom.

IAPHelper은 새로운 제품IDs에 대해 알아야합니다. store의 초기화를 다음과같이 변경합니다.

public static let store = IAPHelper(productIds: OwlProducts.productIDsConsumables
  .union(OwlProducts.productIDsNonConsumables)
  .union(OwlProducts.productIDsNonRenewing))

모든 구매 타입을 하나의 Set으로 결합하고 IAPHelper초기화로 전달합니다.


Expiration Handling

유저의 로컬 기기는 구독 만료 날짜를 알아야 합니다. 시작 프로젝트는 이미 이것을 하기위한 몇가지 변수가 설정되어 있습니다. UserSettings.swift를 열고 둘러보세요.

  • expirationDate: 최신 구독 날짜
  • randomRemaining: 유저가 볼수있는 임의 이미지 수.
  • lastRandomIndex: 마지막 임의 이미지 색인
  • increaseRandomExpirationDate: 만료날짜를 개월단위로 증가 시킵니다.
  • increaseRandomRemaining: 유저가 볼수 있는 임의이미지 수를 증가시킵니다.

UserDefaults는 변수에 대한 로컬 데이터 베이스 입니다. 명백하지 않은 무언가가 프로젝트의 이후에 있습니다. 구매 한 상태를 나타내는 Bool값을 사용하여 각 제품 ID를 UserDefaults에 저장합니다.

이전에 말했듯이, 구독 구매의 만료 날짜를 생성하고 다루어야 하는 코드가 필요합니다. 이 날짜는 로컬과 Parse 서버에 저장되어 집니다. 그렇기 때문에 유저의 모든 기기에서 사용이 가능합니다. OwlProducts.swift를 열고 OwlProduct 구조체의 하단에 다음을 추가합니다.

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)
  }
}

구독을 처리하기 위해 필요한 몇가의 도우미 함수중 첫번째 입니다. 위의 코드는 유저가 지불했는지에 따라서 의존하여 Bool값을 로컬 UserDefaults에 업데이트 합니다. 또한 모든 제품 식별자의 지불 상태를 추적하고 유지하는 IAHelper.purchasedProdcuts 배열을 업데이트 합니다.

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
}

이 코드는 구독만료까지 남아있는 일수를 반환합니다. 구독이 만료된 경우 0 또는 음수가 반환됩니다.

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"
}

이 함수는 유효 기간을 포맷하고 반환하거나 만료되었을때에는 구독하지 않습니다.

사용자가 임의 올빼미 이미지들을 구입할수 있는 두가지 방법으로부터(구독 하거나, 5개를 구입하거나) 임의 올빼미 제품을 볼수 있는지 결정하는 편리한 방법이 필요합니다. 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
}

이 코드는 구독 기간이 남아 있는지 확인합니다. 남아 있지 않으면 사용자가 다른 올빼미 이미지를 구입했는지 확인합니다. 끝나면 setRandomProduct(with:)을 호출하고 paid up 상태를 반환합니다.



Parse Server Sync

앞서 언급했듯이 로컬 및 원격 서버 구독 날짜를 동기화 해야합니다. paidUp()아래에 이를 다루기 위해 다음 코드를 추가합니다.

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)
  }
}

위 코드는 Parse Server와 통신하고 로그인한 현재 유저의 데이터를 검색합니다. 이 함수의 목표는 로컬에 저장된 구독 날짜를 원격 서버와 동기화 시키는 것입니다. 함수의 첫번째 매개변수는 로컬의 날짜, 두번째 매개변수는 Paser에 의해 반환된 PFObject를 전달하는 completion handler 입니다.

더 최근의 날짜를 결정하기 위해 로컬과 서버의 날짜를 비교하는 해야합니다. 유저가 어떤 한 장치에서 구독을 갱신하고 그후 다른 장치에서 갱신을 시도할때 발생할수 있는 문제의 가능성을 피하게 만들어줍니다. Parse 콜백 내부의 TODO: 표시에 다음을 추가합니다.

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)
  }
}

첫번째 줄은 Parse에서 expirationDateKey값을 가져옵니다. 다음으로 로컬과 원격의 최신 구독 날짜를 결정합니다. 그 다음 로컬의 UserSetting’s 동기화된 날짜를 업데이트 합니다. 마지막으로 구독이 유효하다면 setRandomProduct을 호출합니다.

이제 기초가 마련되었고 구독을 구매하는 매소드를 작성할수 있습니다. 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)
  }    
}

새로운 각 구독 구매 마다 months 매개변수에 의해서 만료 날짜가 증가합니다.

  1. syncExpiration은 최신 만료 날짜를 UserSettings 과 동기화 합니다.
  2. increaseRandomExpirationDate는 로컬의 만료 날짜를 n 개월 만큼 증가 시킵니다.
  3. setRandomProduct는 임의의 제품 식별자와 관련된 변수 및 제품 배열을 업데이트 합니다.
  4. Parse 원격 서버는 새로운 구독 값을 받습니다.
  5. 새로운 구매에 대한 내용을 듣고 있는 객체에게 알림을 보냅니다.

New Product ID Handling

현재 IAPHelper 에서 새로 구매한 항목은 OwlProducts.handlePurchase을 통해서 전달되지만, 시작 프로젝트 코드는 non-consumables만 지원 했습니다. 이것을 고칠것입니다. 기존 내용을 다음으로 대체합니다.

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)
}

구입한 제품 타입 기반으로:

  • Consumables: 임의 이미지들을 5개씩 증가 시킵니다
  • Non-Renewing 3 months: 3달씩 구독을 증가 합니다
  • Non-Renewing 6 months: 6달씩 구독을 증가합니다.
  • Non-Consumables: IAPHelper store의 purchasedProducts를 업데이트 합니다.

이 모든 경우에, 코드는 이 함수에서 직접적으로 또는 간접적으로 PurchaseNotification을 발행합니다. setRandomProduct는 non-consumables를 제외한 모든 타입을 포함한 임의 올빼미 이미지 제품을 다루는 경우에만 호출 됩니다.


Progress Check!

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

좋은 소식! 새로운 구독과 재구매 가능한 제품이 보여집니다. 지금은 non-renewing subscription을 구매해도 임의 올빼미 이미지가 보이지 않습니다. 다음에 이 문제를 해결 합니다.

구독 처리 코드가 완성 되었습니다. 이제 GUI으로 가고 유저에게 새로운 콘텐츠를 보여줍니다.


Providing Subscription Content

이 시점에서 새로운 구독과 재 구매 가능한 제품은 목록에 보이지만 해당 상품을 선택했을때 임의 올빼미 이미지가 보이지 않습니다. 이것을 고칠 것입니다.

MasterViewController.swift를 열고 tableView(_:didSelectRowAt:) 코드를 다음과 같이 변경합니다.

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)
}

이제 유저는 임의 올빼미와 관련된 제품을 선택했을때 RandomViewController로 segue 합니다.

다음으로, 복원 기능의 함수를 업데이트 합니다. 복원 버튼은 다른 기기 또는 앱의 이전 설치에서 구매 내역을 검색합니다. 현재 이 프로젝트는 재 구매 가능한 제품만 복원합니다. restoreTapped(_:) 버튼에 다음을 추가합니다.

// Restore Non-Renewing Subscriptions Date saved in Parse
OwlProducts.syncExpiration(local: UserSettings.shared.expirationDate) {
  object in
      
  DispatchQueue.main.async {
    self.tableView.reloadData()
  }
}

이 코드는 Parse Server의 expirationDate를 동기화 시키기위해 syncExpiration을 호출 합니다. 앱이 최신 날짜를 가지면, completion handler는 각 제품의 최신 날짜 상태로 제품을 열거합니다.

Note: 복원이 재 구매 가능한 제품을 리로드 하지 않는다는걸 알아 챘을것입니다. 이것은 의도 한것입니다. 앱을 삭제하면 재 구매할수 있는 제품을 기기간에 양도하거나 보관할수 없습니다.

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


임의 이미지가… 아직 비어있내..

닫습니다. 보람없이 끝난것 같지만. 임의 콘텐츠는 RandomViewController에서 여전히 누락되어 있습니다. RandomViewcontroller.swift를 열고 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

위의 코드에 대해서. 처음 몇개의 라인은 유저가 제품을 구매 했는지 확인합니다. 구매 하지 않았다면, 임의 이미지는 화면에 표시되지 않습니다. 다음 코드는 모든것이 맞다면 다음 임의 이미지를 재사용할수 있도록 합니다. 마지막으로 arc4random을 사용하여 이미지의 색인을 임의로 지정합니다. 또한 코드가 동인한 이미지를 두번 연속으로 표시 하지 않도록 설정합니다.

refresh()는 추가적인 기능이 필요합니다. 만료날짜를 유저에게 표시하기 위한것이 필요하고 사용자가 사용한 제품을 추가하기 위한 것도 필요합니다. 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)
  }
}

이 코드는 사용자의 구독 만료 날짜를 확인합니다. 제품을 구입하면 IbIExpiration을 업데이트 하고 존재하는 구독이 유효하지 않으면 남아 있는 consumables한 수를 줄입니다. 앱은 매시간 refresh()를 호출하고 이때마다 재 구매 가능한 제품을 소비합니다. 구매한 모든 이미지가 사라지면, 더이상 아무것도 표시하지 않습니다.

Note: btnRandom누르면 refresh()를 호출하고 또한 구입한 이미지를 사용합니다.

처리를 보기위해 앱을 빌드하고 실행합니다. 구독을 구입하고 다른 올빼미들을 확인합니다.


Clean Up

구독을 이미 구매한 경우 테이블뷰에 표시된다면 좋을것입니다. 추가로 Random Owl 아이템은 우리의 특별 제품임을 조금 강조할 필요가 있습니다. ProductCell.swift를 열고 다음으로 임의 이미지의 가시성을 다음과같이 변경합니다.

imgRandom.isHidden = (product.productIdentifier != OwlProducts.randomProductID)

테이블 뷰의 Random Owl 옆에 특별한 아이콘을 보여줍니다.

다음으로 TODO: 표시에 다음 코드를 추가합니다.

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)
}

위의 코드는 구독이 유효하면 Buy 버튼을 Renew으로 설정합니다. 유저가 추가적으로 구독을 구매하길 원하지만. 구매한 항목을 명확하게 표시해야 합니다. 마지막으로 마지막 두줄의 코드는 구독에 대한 가격을 보여줍니다.

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


제품 목록은 올바르게 행동 해야합니다. 모두다 구매했다면, 구독 버튼은 모두 Renew로 변경되어야 하고 Buy 버튼은 체크 마크로 변해야 합니다.


Where to Go from Here?

여기 완성된 프로젝트가 있습니다.

축하합니다. 비-자동갱신 구독튜토리얼을 구현 했습니다.

간단한 앱인 경우에 위의 방법이면 충분합니다. 하지만 IAP 에 대해 더 알고싶다면 Introducing Expanded Subscriptions in iTunes Connect 이 비디오가 도움 될것입니다.